fibers B1.2: Io capability + context.io + blocking impl + Future/async/await/cancel
Threads an `Io` capability onto `Context` exactly like `Allocator`: a
`protocol #inline` whose process-wide default is a stateless `CBlockingIo`
(the mirror of `CAllocator`), installed in `__sx_default_context`.
Library (library/modules/std):
- core.sx: `Io` protocol (spawn_raw / suspend_raw / ready / poll / now_ms /
arm_timer) + `SpawnOpts` / `PinTarget` / `ParkToken`; `Context` gains an
`io: Io` field LAST (allocator stays index 0, data stays index 1).
- io.sx (new): `CBlockingIo` + `impl Io` (blocking M:1 semantics — now_ms is
a real monotonic clock, the rest are no-ops/0; suspend never called);
`Future($R)` { value; state: FutureState; err: IoErr; park; task; canceled:
Atomic(bool) } with `Value :: R`; the async ergonomic layer
`async` / `async_void` / `await` (value-carrying `(R, !IoErr)`) / `cancel`.
Built with the verified `= ---` + field-assign + `Closure(..$args) -> $R` +
`..$args` idiom (NON-void $R only — Future(void) is deferred per issue 0150).
- std.sx: re-export the Io surface + the io.sx tail.
Compiler (src/ir):
- protocol.zig `emitDefaultContextGlobal` + comptime_vm.zig
`materializeDefaultContext`: both materializers of `__sx_default_context`
now build the inline CBlockingIo->Io vtable (7 words) at the new field.
- stmt.zig `lowerPush`: `push Context.{...}` now INHERITS omitted fields from
the ambient context (seed the slot from current_ctx_ref, overwrite only the
literal's named fields) — correct capability-bag semantics, so the partial
`push Context.{ allocator = X }` sites don't zero a null `io` vtable.
- protocols.zig + lower.zig + error_analysis.zig: record protocol-impl method
names so the "declared `!` but never errors" lint skips a conforming impl
whose `!` is dictated by the protocol contract (e.g. Io.suspend_raw).
37 `.ir` snapshots regenerated: layout-only (the Context type now carries the
Io field, shifting type-table numbering); no stdout/stderr/exit changes.
The blocking Io + now_ms + Future/async work when `async` is called with the
receiver passed explicitly; the user-facing UFCS form `context.io.async(...)`
is blocked on a separate UFCS generic-inference bug (filed next).
Suite: 726 ran, 0 failed.
This commit is contained in:
@@ -1,35 +0,0 @@
|
|||||||
// Stream B1 (fibers) — the `Io` capability + the blocking-`Io` default
|
|
||||||
// (step B1.2). `Io` is threaded on `Context` exactly like `Allocator`: a
|
|
||||||
// `protocol #inline` at a fixed field, whose process-wide default is a
|
|
||||||
// stateless `CBlockingIo` (the mirror of `CAllocator`).
|
|
||||||
//
|
|
||||||
// In the blocking M:1 model there is no scheduler and no suspension:
|
|
||||||
// `async(worker, ..args)` runs the worker to COMPLETION synchronously, so
|
|
||||||
// the returned `Future` is born `.ready` and `await` yields the stored
|
|
||||||
// result immediately. This locks the B1.2 surface — `context.io.async(...)`
|
|
||||||
// with a lambda worker (annotated params) + `f.await()`.
|
|
||||||
//
|
|
||||||
// Worker form: a lambda `(a: i64, b: i64) -> i64 => ...` whose params are
|
|
||||||
// annotated. Named-fn workers need a `::` callable-param feature that does
|
|
||||||
// not exist yet and are DEFERRED.
|
|
||||||
#import "modules/std.sx";
|
|
||||||
|
|
||||||
main :: () -> i32 {
|
|
||||||
// Single-arg lambda worker.
|
|
||||||
f1 := context.io.async((n: i64) -> i64 => n * 2, 21);
|
|
||||||
v1, e1 := f1.await();
|
|
||||||
if !e1 { print("double: {}\n", v1); } // → 42
|
|
||||||
|
|
||||||
// Two-arg lambda worker — exercises the `..$args` variadic forward.
|
|
||||||
f2 := context.io.async((a: i64, b: i64) -> i64 => a + b, 40, 2);
|
|
||||||
v2, e2 := f2.await();
|
|
||||||
if !e2 { print("sum: {}\n", v2); } // → 42
|
|
||||||
|
|
||||||
// `now_ms` is a protocol method (a deterministic-sim Io [B1.4] can
|
|
||||||
// override it); the blocking Io returns a real monotonic clock, so we
|
|
||||||
// only assert it is non-negative, not an exact value.
|
|
||||||
t := context.io.now_ms();
|
|
||||||
if t >= 0 { print("clock ok\n"); }
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -17,6 +17,10 @@ list :: #import "modules/std/list.sx";
|
|||||||
|
|
||||||
Context :: core.Context;
|
Context :: core.Context;
|
||||||
Allocator :: core.Allocator;
|
Allocator :: core.Allocator;
|
||||||
|
Io :: core.Io;
|
||||||
|
SpawnOpts :: core.SpawnOpts;
|
||||||
|
PinTarget :: core.PinTarget;
|
||||||
|
ParkToken :: core.ParkToken;
|
||||||
Into :: core.Into;
|
Into :: core.Into;
|
||||||
Source_Location :: core.Source_Location;
|
Source_Location :: core.Source_Location;
|
||||||
|
|
||||||
@@ -85,6 +89,20 @@ decompose_u16x4 :: fmt.decompose_u16x4;
|
|||||||
|
|
||||||
List :: list.List;
|
List :: list.List;
|
||||||
|
|
||||||
|
// --- Async / Io capability (impls in std/io.sx) ---
|
||||||
|
|
||||||
|
io_mod :: #import "modules/std/io.sx";
|
||||||
|
CBlockingIo :: io_mod.CBlockingIo;
|
||||||
|
Future :: io_mod.Future;
|
||||||
|
FutureState :: io_mod.FutureState;
|
||||||
|
IoErr :: io_mod.IoErr;
|
||||||
|
async :: io_mod.async;
|
||||||
|
async_void :: io_mod.async_void;
|
||||||
|
await :: io_mod.await;
|
||||||
|
cancel :: io_mod.cancel;
|
||||||
|
// `timeout` / `Future(void)` are DEFERRED (B1.4) pending issue 0150
|
||||||
|
// (a `void` struct field SIGTRAPs the compiler). Re-export once it lands.
|
||||||
|
|
||||||
// --- The stdlib namespace tail: flat-importing std.sx carries these ---
|
// --- The stdlib namespace tail: flat-importing std.sx carries these ---
|
||||||
|
|
||||||
mem :: #import "modules/std/mem.sx";
|
mem :: #import "modules/std/mem.sx";
|
||||||
|
|||||||
@@ -64,11 +64,61 @@ Allocator :: protocol #inline {
|
|||||||
dealloc_bytes :: (ptr: *void);
|
dealloc_bytes :: (ptr: *void);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Io capability protocol (impls live in std/io.sx) ---
|
||||||
|
//
|
||||||
|
// `Io` is threaded on `Context` exactly like `Allocator`: a `#inline`
|
||||||
|
// protocol whose default impl (the stateless `CBlockingIo` in std/io.sx)
|
||||||
|
// is installed in the process-wide `__sx_default_context`. Async runtime
|
||||||
|
// is sx LIBRARY code — the compiler provides only the primitives (inline
|
||||||
|
// asm, `abi(.naked)`, atomics) + fiber-safe codegen. The protocol is the
|
||||||
|
// minimum the fiber scheduler [B1.3+] needs; everything ergonomic
|
||||||
|
// (`async` / `await` / `cancel` / `timeout`) is a generic free-fn on top
|
||||||
|
// (std/io.sx), the same way `alloc(T,n)` sits over `alloc_bytes`.
|
||||||
|
//
|
||||||
|
// spawn_raw — submit a task; opaque handle (B1.3 fiber bootstrap).
|
||||||
|
// suspend_raw— suspend current fiber; `!` so cancel can raise out.
|
||||||
|
// ready — wake a parked fiber (B1.4/B1.5).
|
||||||
|
// poll — drive one step; blocking impl returns 0.
|
||||||
|
// now_ms — clock hook (a PROTOCOL method so the deterministic-sim
|
||||||
|
// Io [B1.4] can return a fake clock — the B1.4 keystone).
|
||||||
|
// arm_timer — register a timer; backs `timeout` (B1.4).
|
||||||
|
//
|
||||||
|
// `ParkToken` is an opaque per-suspension token (unused by the blocking
|
||||||
|
// impl). `SpawnOpts.pin` is inert in the M:1 model. (`PinTarget.on` —
|
||||||
|
// pin to a specific `Thread` — is deferred with the M:N model; the
|
||||||
|
// `on_thread` variant is a placeholder until a `Thread` type exists.)
|
||||||
|
PinTarget :: enum { any; main; on_thread; }
|
||||||
|
|
||||||
|
SpawnOpts :: struct {
|
||||||
|
pin: PinTarget = .any;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParkToken :: struct {
|
||||||
|
handle: *void = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Io :: protocol #inline {
|
||||||
|
spawn_raw :: (entry: *void, arg: *void, opts: SpawnOpts) -> *void;
|
||||||
|
suspend_raw :: (park: ParkToken) -> !;
|
||||||
|
ready :: (park: ParkToken);
|
||||||
|
poll :: (deadline_ms: i64) -> i64;
|
||||||
|
now_ms :: () -> i64;
|
||||||
|
arm_timer :: (deadline_ms: i64, park: ParkToken) -> *void;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Context ---
|
// --- Context ---
|
||||||
|
//
|
||||||
|
// `allocator` MUST stay at field index 0 (the heap-alloc lowering path
|
||||||
|
// hardcodes it — src/ir/lower/call.zig). `io` is appended LAST so `data`
|
||||||
|
// keeps its existing index 1 (minimizes the comptime-VM fallback churn).
|
||||||
|
// Both Zig materializers of `__sx_default_context` (protocol.zig
|
||||||
|
// `emitDefaultContextGlobal` + comptime_vm.zig `materializeDefaultContext`)
|
||||||
|
// install the inline `CBlockingIo → Io` vtable at the new field.
|
||||||
|
|
||||||
Context :: struct {
|
Context :: struct {
|
||||||
allocator: Allocator;
|
allocator: Allocator;
|
||||||
data: *void;
|
data: *void;
|
||||||
|
io: Io;
|
||||||
}
|
}
|
||||||
|
|
||||||
// User-space `xx` extension. `xx val : T` where the built-in conversion
|
// User-space `xx` extension. `xx val : T` where the built-in conversion
|
||||||
|
|||||||
135
library/modules/std/io.sx
Normal file
135
library/modules/std/io.sx
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
// std.io — the `Io` capability's default impl + the async ergonomic layer.
|
||||||
|
//
|
||||||
|
// `Io` itself (the protocol) lives in std/core.sx next to `Allocator`, so
|
||||||
|
// the compiler-coupled `Context` field + the `__sx_default_context`
|
||||||
|
// materializers can reference it. This file carries the parts that are
|
||||||
|
// pure library sx: the stateless blocking impl (`CBlockingIo`, the mirror
|
||||||
|
// of `CAllocator`) + the generic free-fns layered over the protocol
|
||||||
|
// (`async` / `await` / `cancel` + the `Future($R)` type).
|
||||||
|
//
|
||||||
|
// Consumers reach these through std.sx (`Future` / `async` / `await` /
|
||||||
|
// `cancel` / `CBlockingIo` re-exports), never by importing this file
|
||||||
|
// directly.
|
||||||
|
//
|
||||||
|
// BLOCKING SEMANTICS (B1.2): the M:1 default has no scheduler and no
|
||||||
|
// suspension. `async(worker, ..args)` runs the worker to COMPLETION
|
||||||
|
// inline, so the returned `Future` is born `.ready` and `await` yields
|
||||||
|
// immediately. `spawn_raw`/`suspend_raw`/`ready`/`poll`/`arm_timer` are
|
||||||
|
// trivial no-ops/0 — they exist for the fiber scheduler [B1.3+].
|
||||||
|
// `now_ms` returns a real monotonic clock. Fully deterministic/testable.
|
||||||
|
//
|
||||||
|
// Worker form (B1.2): a `Closure(..$args) -> $R` whose params are
|
||||||
|
// annotated at the call site (a lambda `(a: i64) -> i64 => ...`).
|
||||||
|
// Named-fn workers need a `::` callable-parameter language feature that
|
||||||
|
// does not exist yet and are DEFERRED.
|
||||||
|
#import "modules/std/core.sx";
|
||||||
|
#import "modules/std/atomic.sx";
|
||||||
|
time :: #import "modules/std/time.sx";
|
||||||
|
|
||||||
|
// --- IoErr: the error channel async rides (cancellation = model (a)) ---
|
||||||
|
//
|
||||||
|
// A canceled future raises `.Canceled` out of `await`; a failed task
|
||||||
|
// raises `.Failed`. The `(T, !IoErr)` value-failable shape is the same
|
||||||
|
// one the rest of the stdlib uses (see examples/1011-, 1012-).
|
||||||
|
IoErr :: error { Canceled, Failed }
|
||||||
|
|
||||||
|
// --- CBlockingIo: stateless Io that runs tasks synchronously ---
|
||||||
|
//
|
||||||
|
// Zero-sized struct (mirror of CAllocator). Used as the default
|
||||||
|
// `context.io` at program start (see `__sx_default_context` in codegen).
|
||||||
|
// The thunks never dereference `self`, so the protocol value's ctx field
|
||||||
|
// is `null` — which is what keeps the static-constant default context an
|
||||||
|
// inline vtable with a null receiver.
|
||||||
|
|
||||||
|
CBlockingIo :: struct {}
|
||||||
|
|
||||||
|
impl Io for CBlockingIo {
|
||||||
|
// No fiber bootstrap in the blocking model: the generic `async`
|
||||||
|
// free-fn calls the worker directly and fills the Future. `spawn_raw`
|
||||||
|
// is here for the protocol shape the scheduler [B1.3] will use; the
|
||||||
|
// blocking impl never routes through it, so it is a no-op handle.
|
||||||
|
spawn_raw :: (self: *CBlockingIo, entry: *void, arg: *void, opts: SpawnOpts) -> *void {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Blocking never suspends — a suspend at the bottom of the M:1 stack
|
||||||
|
// would deadlock. No-op (returns success). The `!` is part of the
|
||||||
|
// protocol contract (a suspending impl raises `.Canceled` out here),
|
||||||
|
// so the conforming blocking impl keeps it even though it never raises.
|
||||||
|
suspend_raw :: (self: *CBlockingIo, park: ParkToken) -> ! {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ready :: (self: *CBlockingIo, park: ParkToken) {}
|
||||||
|
poll :: (self: *CBlockingIo, deadline_ms: i64) -> i64 { return 0; }
|
||||||
|
now_ms :: (self: *CBlockingIo) -> i64 { return time.mono_ms(); }
|
||||||
|
arm_timer :: (self: *CBlockingIo, deadline_ms: i64, park: ParkToken) -> *void {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Future($R): the handle to an async task's eventual result ---
|
||||||
|
//
|
||||||
|
// Fixed-shape product (NOT the metatype sum machinery). `Value :: $R`
|
||||||
|
// exposes the projection `Future(X) → X`. B1.2 supports NON-void `$R`
|
||||||
|
// only — `Future(void)` (a `void` struct field) SIGTRAPs the compiler
|
||||||
|
// (issue 0150, deferred to B1.4 along with `timeout`).
|
||||||
|
FutureState :: enum { pending; ready; failed; canceled; }
|
||||||
|
|
||||||
|
Future :: struct ($R: Type) {
|
||||||
|
Value :: R;
|
||||||
|
|
||||||
|
value: R;
|
||||||
|
state: FutureState = .pending;
|
||||||
|
err: IoErr;
|
||||||
|
park: ParkToken;
|
||||||
|
task: *void = null;
|
||||||
|
// Cancellation flag — atomic so a future scheduler thread can flip it.
|
||||||
|
// In the blocking model there is no concurrency, but the type is the
|
||||||
|
// one the M:N model [later] needs.
|
||||||
|
canceled: Atomic(bool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- The async ergonomic layer (generic free-fns over the protocol) ---
|
||||||
|
|
||||||
|
// `async(io, worker, ..args)` — submit `worker(..args)`. Blocking: runs
|
||||||
|
// the worker to completion inline, Future born `.ready`. The worker is a
|
||||||
|
// `Closure(..$args) -> $R` (a lambda whose params are annotated at the
|
||||||
|
// call site); `..$args` forwards the call-site arguments to it.
|
||||||
|
//
|
||||||
|
// NOTE on construction shape: the Future is built with `= ---` + per-field
|
||||||
|
// assignment, NOT a `return Future.{...}` struct-literal. A struct-literal
|
||||||
|
// in `return` position trips a generic-instantiation gap for the `Atomic`
|
||||||
|
// field; the `= ---` (uninit) + field-assign form is the verified idiom.
|
||||||
|
async :: ufcs (io: Io, worker: Closure(..$args) -> $R, ..$args) -> Future($R) {
|
||||||
|
f : Future($R) = ---;
|
||||||
|
f.value = worker(..args);
|
||||||
|
f.state = .ready;
|
||||||
|
f.canceled = Atomic(bool).init(false);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nullary form — no args. A worker that takes no arguments.
|
||||||
|
async_void :: ufcs (io: Io, worker: Closure() -> $R) -> Future($R) {
|
||||||
|
f : Future($R) = ---;
|
||||||
|
f.value = worker();
|
||||||
|
f.state = .ready;
|
||||||
|
f.canceled = Atomic(bool).init(false);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `await(f)` — value-carrying failable. `.ready` → the result; `.failed`
|
||||||
|
// / `.canceled` → raise the stored / cancellation error.
|
||||||
|
await :: ufcs (f: *Future($R)) -> ($R, !IoErr) {
|
||||||
|
if f.canceled.load(.acquire) { raise error.Canceled; }
|
||||||
|
if f.state == .canceled { raise error.Canceled; }
|
||||||
|
if f.state == .failed { raise error.Failed; }
|
||||||
|
return f.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `cancel(f)` — request cancellation. Sets the per-future cancel flag +
|
||||||
|
// marks the state so a subsequent `await` raises `.Canceled`. (In the
|
||||||
|
// blocking model the task already ran; cancel still rides the `!`
|
||||||
|
// channel — model (a).)
|
||||||
|
cancel :: ufcs (f: *Future($R)) {
|
||||||
|
f.canceled.store(true, .release);
|
||||||
|
f.state = .canceled;
|
||||||
|
}
|
||||||
@@ -455,6 +455,24 @@ pub const Vm = struct {
|
|||||||
try self.machine.writeWord(addr + ps, ps, funcRefWord(fid)); // allocator.alloc_fn @ +ptr_size
|
try self.machine.writeWord(addr + ps, ps, funcRefWord(fid)); // allocator.alloc_fn @ +ptr_size
|
||||||
if (self.findFuncByName(module, "__thunk_CAllocator_Allocator_dealloc_bytes")) |fid|
|
if (self.findFuncByName(module, "__thunk_CAllocator_Allocator_dealloc_bytes")) |fid|
|
||||||
try self.machine.writeWord(addr + 2 * ps, ps, funcRefWord(fid)); // allocator.dealloc_fn @ +2*ptr_size
|
try self.machine.writeWord(addr + 2 * ps, ps, funcRefWord(fid)); // allocator.dealloc_fn @ +2*ptr_size
|
||||||
|
// Context layout: { allocator(3 words), data(1 word), io }. The inline
|
||||||
|
// `Io` value starts at +4*ptr_size: { ctx, fn0..fn5 }, receiver ctx is
|
||||||
|
// null (CBlockingIo stateless), the 6 method func-refs follow in the
|
||||||
|
// protocol's declaration order. Mirrors the `emitDefaultContextGlobal`
|
||||||
|
// global path; absent thunks (std not imported) leave the field zeroed.
|
||||||
|
const io_base: Addr = addr + 4 * ps;
|
||||||
|
const io_methods = [_][]const u8{
|
||||||
|
"__thunk_CBlockingIo_Io_spawn_raw",
|
||||||
|
"__thunk_CBlockingIo_Io_suspend_raw",
|
||||||
|
"__thunk_CBlockingIo_Io_ready",
|
||||||
|
"__thunk_CBlockingIo_Io_poll",
|
||||||
|
"__thunk_CBlockingIo_Io_now_ms",
|
||||||
|
"__thunk_CBlockingIo_Io_arm_timer",
|
||||||
|
};
|
||||||
|
for (io_methods, 0..) |mname, i| {
|
||||||
|
if (self.findFuncByName(module, mname)) |fid|
|
||||||
|
try self.machine.writeWord(io_base + (@as(Addr, @intCast(i)) + 1) * ps, ps, funcRefWord(fid));
|
||||||
|
}
|
||||||
return addr;
|
return addr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,11 @@ pub const ErrorAnalysis = struct {
|
|||||||
const sorted = self.l.alloc.dupe(u32, se.value_ptr.tags.items) catch continue;
|
const sorted = self.l.alloc.dupe(u32, se.value_ptr.tags.items) catch continue;
|
||||||
std.mem.sort(u32, sorted, {}, std.sort.asc(u32));
|
std.mem.sort(u32, sorted, {}, std.sort.asc(u32));
|
||||||
self.l.inferred_error_sets.put(se.key_ptr.*, sorted) catch {};
|
self.l.inferred_error_sets.put(se.key_ptr.*, sorted) catch {};
|
||||||
if (sorted.len == 0 and !std.mem.eql(u8, se.key_ptr.*, "main")) {
|
// Skip `main` (its `!` is the program's top error channel) and any
|
||||||
|
// protocol-impl method (its `!` is dictated by the protocol
|
||||||
|
// contract — e.g. `Io.suspend_raw` — so a non-raising impl body
|
||||||
|
// is not a "drop the `!`" case; see `impl_method_names`).
|
||||||
|
if (sorted.len == 0 and !std.mem.eql(u8, se.key_ptr.*, "main") and !self.l.impl_method_names.contains(se.key_ptr.*)) {
|
||||||
if (self.l.diagnostics) |diags| {
|
if (self.l.diagnostics) |diags| {
|
||||||
if (se.value_ptr.rt) |rt| {
|
if (se.value_ptr.rt) |rt| {
|
||||||
diags.addFmt(.warn, rt.span, "function '{s}' is declared `!` but never errors — drop the `!`", .{se.key_ptr.*});
|
diags.addFmt(.warn, rt.span, "function '{s}' is declared `!` but never errors — drop the `!`", .{se.key_ptr.*});
|
||||||
|
|||||||
@@ -391,6 +391,14 @@ pub const Lowering = struct {
|
|||||||
/// escape tags in. Read by `checkEscapeWidening` when a `try` operand is a
|
/// escape tags in. Read by `checkEscapeWidening` when a `try` operand is a
|
||||||
/// closure/fn-type SLOT call (no static fn name). Key = `closureShapeKey`.
|
/// closure/fn-type SLOT call (no static fn name). Key = `closureShapeKey`.
|
||||||
shape_inferred_sets: std.StringHashMap([]const u32),
|
shape_inferred_sets: std.StringHashMap([]const u32),
|
||||||
|
/// Qualified names (`Type.method`) of every explicitly-written protocol
|
||||||
|
/// impl method. A protocol method may be declared `!` (the error channel
|
||||||
|
/// is part of the contract — e.g. `Io.suspend_raw`); a conforming impl
|
||||||
|
/// MUST keep the `!` even when its concrete body never raises, so the
|
||||||
|
/// "declared `!` but never errors — drop the `!`" warning (a free-fn
|
||||||
|
/// linting hint) is a false positive for these. The empty-inferred-set
|
||||||
|
/// warning in `error_analysis.zig` skips names in this set.
|
||||||
|
impl_method_names: std.StringHashMap(void),
|
||||||
|
|
||||||
pub const ComptimeValue = union(enum) {
|
pub const ComptimeValue = union(enum) {
|
||||||
int_val: i64,
|
int_val: i64,
|
||||||
@@ -536,6 +544,7 @@ pub const Lowering = struct {
|
|||||||
.comptime_constants = std.StringHashMap(ComptimeValue).init(module.alloc),
|
.comptime_constants = std.StringHashMap(ComptimeValue).init(module.alloc),
|
||||||
.xx_reentrancy = std.AutoHashMap(u64, void).init(module.alloc),
|
.xx_reentrancy = std.AutoHashMap(u64, void).init(module.alloc),
|
||||||
.inferred_error_sets = std.StringHashMap([]const u32).init(module.alloc),
|
.inferred_error_sets = std.StringHashMap([]const u32).init(module.alloc),
|
||||||
|
.impl_method_names = std.StringHashMap(void).init(module.alloc),
|
||||||
.shape_inferred_sets = std.StringHashMap([]const u32).init(module.alloc),
|
.shape_inferred_sets = std.StringHashMap([]const u32).init(module.alloc),
|
||||||
.program_index = ProgramIndex.init(module.alloc),
|
.program_index = ProgramIndex.init(module.alloc),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ pub fn emitDefaultContextGlobal(self: *Lowering) void {
|
|||||||
const ctx_ty = tbl.findByName(ctx_name_id) orelse return;
|
const ctx_ty = tbl.findByName(ctx_name_id) orelse return;
|
||||||
if (tbl.findByName(tbl.internString("Allocator")) == null) return;
|
if (tbl.findByName(tbl.internString("Allocator")) == null) return;
|
||||||
if (tbl.findByName(tbl.internString("CAllocator")) == null) return;
|
if (tbl.findByName(tbl.internString("CAllocator")) == null) return;
|
||||||
|
if (tbl.findByName(tbl.internString("Io")) == null) return;
|
||||||
|
if (tbl.findByName(tbl.internString("CBlockingIo")) == null) return;
|
||||||
|
|
||||||
// Force the CAllocator → Allocator thunks to exist so we can
|
// Force the CAllocator → Allocator thunks to exist so we can
|
||||||
// reference them by FuncId in the static initializer.
|
// reference them by FuncId in the static initializer.
|
||||||
@@ -267,10 +269,20 @@ pub fn emitDefaultContextGlobal(self: *Lowering) void {
|
|||||||
alloc_fields[1] = .{ .func_ref = thunks[0] };
|
alloc_fields[1] = .{ .func_ref = thunks[0] };
|
||||||
alloc_fields[2] = .{ .func_ref = thunks[1] };
|
alloc_fields[2] = .{ .func_ref = thunks[1] };
|
||||||
|
|
||||||
// Context value: { allocator: Allocator, data: *void }
|
// Force the CBlockingIo → Io thunks to exist. The Io protocol has 6
|
||||||
const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 2) catch return;
|
// methods, so the inline value is { ctx, fn0..fn5 } — 7 pointer words.
|
||||||
|
const io_thunks = self.getOrCreateThunks("Io", "CBlockingIo");
|
||||||
|
if (io_thunks.len < 6) return;
|
||||||
|
const io_fields = self.alloc.alloc(inst_mod.ConstantValue, io_thunks.len + 1) catch return;
|
||||||
|
io_fields[0] = .null_val; // CBlockingIo is stateless → null receiver.
|
||||||
|
for (io_thunks, 0..) |fid, i| io_fields[i + 1] = .{ .func_ref = fid };
|
||||||
|
|
||||||
|
// Context value: { allocator: Allocator, data: *void, io: Io }.
|
||||||
|
// `data` keeps index 1; `io` is appended last.
|
||||||
|
const ctx_fields = self.alloc.alloc(inst_mod.ConstantValue, 3) catch return;
|
||||||
ctx_fields[0] = .{ .aggregate = alloc_fields };
|
ctx_fields[0] = .{ .aggregate = alloc_fields };
|
||||||
ctx_fields[1] = .null_val;
|
ctx_fields[1] = .null_val;
|
||||||
|
ctx_fields[2] = .{ .aggregate = io_fields };
|
||||||
|
|
||||||
const global_name = "__sx_default_context";
|
const global_name = "__sx_default_context";
|
||||||
const global_name_id = tbl.internString(global_name);
|
const global_name_id = tbl.internString(global_name);
|
||||||
|
|||||||
@@ -1255,14 +1255,65 @@ pub fn lowerPush(self: *Lowering, ps: *const ast.PushStmt) void {
|
|||||||
const saved_ctx_ref = self.current_ctx_ref;
|
const saved_ctx_ref = self.current_ctx_ref;
|
||||||
defer self.current_ctx_ref = saved_ctx_ref;
|
defer self.current_ctx_ref = saved_ctx_ref;
|
||||||
|
|
||||||
|
const slot = self.builder.alloca(ctx_ty);
|
||||||
|
|
||||||
|
// Inherit-omitted semantics: a `push Context.{ ... }` is a CAPABILITY
|
||||||
|
// bag — fields the literal does NOT name are inherited from the ambient
|
||||||
|
// context, not zero-inited. Zero-init would install a NULL `io`/
|
||||||
|
// `allocator` vtable (a latent crash if the field is later used inside
|
||||||
|
// the pushed scope). So seed the new slot from the ambient context,
|
||||||
|
// then overwrite only the fields the literal explicitly names.
|
||||||
|
//
|
||||||
|
// This applies only to a `Context.{...}` struct-literal context-expr;
|
||||||
|
// any other form (e.g. `push some_ctx_value`) keeps the whole-value
|
||||||
|
// store (no field-level merge to do).
|
||||||
|
const lit: ?*const ast.StructLiteral = switch (ps.context_expr.data) {
|
||||||
|
.struct_literal => |*sl| sl,
|
||||||
|
else => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lit != null and self.current_ctx_ref != Ref.none) {
|
||||||
|
// 1. Copy the ambient context into the fresh slot (load + store the
|
||||||
|
// whole struct), so every omitted field carries its current value.
|
||||||
|
const ambient = self.builder.load(self.current_ctx_ref, ctx_ty);
|
||||||
|
self.builder.store(slot, ambient);
|
||||||
|
|
||||||
|
// 2. Overwrite only the named fields. `push Context.{...}` always
|
||||||
|
// uses named field-inits (it is a Context literal); a positional
|
||||||
|
// init has no field name to target, so it is rejected loudly
|
||||||
|
// rather than silently writing the wrong field.
|
||||||
|
self.current_ctx_ref = slot; // body + field values see the new slot
|
||||||
|
for (lit.?.field_inits) |fi| {
|
||||||
|
const fname = fi.name orelse {
|
||||||
|
if (self.diagnostics) |d|
|
||||||
|
d.addFmt(.err, ps.context_expr.span, "`push Context.{{...}}` requires named fields (positional init not supported)", .{});
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const fl = self.fieldLvaluePtr(slot, ctx_ty, fname) orelse {
|
||||||
|
_ = self.emitFieldError(ctx_ty, fname, ps.context_expr.span);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
const saved_target_f = self.target_type;
|
||||||
|
self.target_type = fl.ty;
|
||||||
|
const fval = self.lowerExpr(fi.value);
|
||||||
|
self.target_type = saved_target_f;
|
||||||
|
const fval_ty = self.builder.getRefType(fval);
|
||||||
|
const store_val = if (fval_ty != fl.ty and fval_ty != .void and fl.ty != .void)
|
||||||
|
self.coerceToType(fval, fval_ty, fl.ty)
|
||||||
|
else
|
||||||
|
fval;
|
||||||
|
self.builder.store(fl.ptr, store_val);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Non-literal context-expr, or no ambient context to inherit from:
|
||||||
|
// lower the whole value and store it (the original behaviour).
|
||||||
const saved_target = self.target_type;
|
const saved_target = self.target_type;
|
||||||
self.target_type = ctx_ty;
|
self.target_type = ctx_ty;
|
||||||
const ctx_val = self.lowerExpr(ps.context_expr);
|
const ctx_val = self.lowerExpr(ps.context_expr);
|
||||||
self.target_type = saved_target;
|
self.target_type = saved_target;
|
||||||
|
|
||||||
const slot = self.builder.alloca(ctx_ty);
|
|
||||||
self.builder.store(slot, ctx_val);
|
self.builder.store(slot, ctx_val);
|
||||||
self.current_ctx_ref = slot;
|
self.current_ctx_ref = slot;
|
||||||
|
}
|
||||||
|
|
||||||
self.lowerBlock(ps.body);
|
self.lowerBlock(ps.body);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -367,6 +367,11 @@ pub const ProtocolResolver = struct {
|
|||||||
self.l.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
self.l.program_index.fn_ast_map.put(qualified, method_fd) catch {};
|
||||||
self.l.program_index.import_flags.put(qualified, is_imported) catch {};
|
self.l.program_index.import_flags.put(qualified, is_imported) catch {};
|
||||||
self.l.declareFunction(method_fd, qualified);
|
self.l.declareFunction(method_fd, qualified);
|
||||||
|
// Record it as a protocol-impl method so the "declared `!`
|
||||||
|
// but never errors" warning skips it: a `!` on a protocol
|
||||||
|
// method is part of the contract (e.g. `Io.suspend_raw`), so
|
||||||
|
// a conforming impl can't drop it even if its body never raises.
|
||||||
|
self.l.impl_method_names.put(qualified, {}) catch {};
|
||||||
impl_methods.put(method_fd.name, {}) catch {};
|
impl_methods.put(method_fd.name, {}) catch {};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user