fibers B1.2: UNBLOCKED — remove invalid issue 0151, correct the async idiom
The B1.2 "blockers" were not real: - Issue 0151 was INVALID: its repro used the non-idiomatic `($A) -> $R` bare-fn-ptr form. The canonical higher-order pack idiom `Closure(..$args) -> $R` + `..$args` (see examples/0543-packs-canonical-map) infers $R fine and runs today with no compiler change. Removed 0151. - The correct async idiom is verified working live (42 42 for homo + hetero args): async :: (io, worker: Closure(..$args) -> $R, ..$args) -> Future($R) with a lambda worker (annotated params) + a `result = ---; result.v = ...` build form. No compiler change needed. Issue 0150 (void struct field -> SIGTRAP exit 133) IS a real bug but is only reached via Future(void) (void-returning worker / timeout) — deferred to B1.4; B1.2 supports non-void workers. Updates the PLAN/CHECKPOINT B1.2 status to UNBLOCKED with the corrected idiom and the resume plan. No compiler/library code changed in this commit.
This commit is contained in:
@@ -59,13 +59,21 @@ body); closed + locked. The review's `.naked`-lambda CRITICAL was a false positi
|
|||||||
(unparseable — `isLambda` breaks on the `abi` keyword).
|
(unparseable — `isLambda` breaks on the `abi` keyword).
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
**B1.2 is BLOCKED on two newly-filed compiler bugs (issues 0150 + 0151). Master is GREEN
|
**B1.2 is UNBLOCKED.** Master GREEN (726/0), installed `sx` clean. The earlier "blockers"
|
||||||
(726/0); all B1.2 working changes were REVERTED so the installed `zig-out/bin/sx` is clean.**
|
were NOT real: issue **0151 was INVALID** (its repro used the non-idiomatic `($A)->$R`
|
||||||
See the "B1.2 attempt (BLOCKED)" subsection below + "Known issues" for the full story. The
|
bare-fn-ptr form) — **removed**. The correct `async` idiom **works today with no compiler
|
||||||
B1.2 *design* is validated end-to-end (the `Io` protocol on `Context`, the blocking
|
change** (verified live): `spawn :: (worker: Closure(..$args) -> $R, ..$args) -> Wrap($R)`
|
||||||
`CBlockingIo` default, and `context.io.now_ms()` all WORK — verified live); only the two
|
with a **lambda worker** + the `w : Wrap($R) = ---; w.v = worker(..args);` build form —
|
||||||
generic/void compiler gaps stop the `async`/`await`/`timeout` ergonomic layer. WIP saved at
|
mirrors the canonical `examples/0543-packs-canonical-map.sx`. Ran `42 42` for homogeneous +
|
||||||
`.sx-tmp/b12-wip/` (io.sx + the compiler+lib diff + the 1805 example) for a fast resume.
|
heterogeneous args. Caveats (work within them, not "bugs"): lambda params must be annotated
|
||||||
|
(`(a: i64, b: i64) -> i64 => …`); a bare *named* fn passed as the worker is non-idiomatic —
|
||||||
|
use a lambda; build the result struct with `= ---` + field-assign, not a struct-literal in
|
||||||
|
`return`. Issue **0150** (`void` struct field → SIGTRAP exit 133) is a **real** bug but only
|
||||||
|
reached via `Future(void)` (void-returning worker / `timeout`) — **DEFERRED**: B1.2 supports
|
||||||
|
non-void workers; revisit `Future(void)` in B1.4 (or fix 0150 standalone). The B1.2 *design*
|
||||||
|
(Io protocol on Context, blocking `CBlockingIo`, `context.io.now_ms()`) was validated live;
|
||||||
|
WIP at `.sx-tmp/b12-wip/` has the working Io/Context/materializer parts — reuse those, rewrite
|
||||||
|
the async layer to the pack-lambda idiom above.
|
||||||
|
|
||||||
### B1.2 attempt (BLOCKED — design proven, two compiler bugs filed)
|
### B1.2 attempt (BLOCKED — design proven, two compiler bugs filed)
|
||||||
What was built + verified WORKING (then reverted to keep master green):
|
What was built + verified WORKING (then reverted to keep master green):
|
||||||
@@ -132,27 +140,29 @@ fibers/Io/scheduler code yet. Grounded floor facts:
|
|||||||
boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker.
|
boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker.
|
||||||
|
|
||||||
## Next step
|
## Next step
|
||||||
**B1.2 is BLOCKED — resume only AFTER issues 0150 + 0151 are fixed.** Then re-land B1.2 from
|
**B1.2 — resume now (UNBLOCKED, no compiler fix needed).** Re-land from the saved WIP
|
||||||
the saved WIP (`.sx-tmp/b12-wip/`): the `Io` protocol + `Context.io` + both materializers +
|
(`.sx-tmp/b12-wip/`): keep the verified-working parts — the `Io` protocol on `Context`, both
|
||||||
the push-inherit fix + the `!`-impl-warning fix all WORK as-is; restore `timeout ->
|
`__sx_default_context` materializers (protocol.zig + comptime_vm.zig), the push-inherit-omitted
|
||||||
Future(void)` (needs 0150) and `async`/`await` (needs 0151), add `examples/1805-concurrency-
|
fix (stmt.zig `lowerPush` — omitted `push Context.{...}` fields inherit the ambient ctx; the
|
||||||
io-blocking-async.sx` (lock→green) + `1806-concurrency-io-cancel.sx` (cancel→`await` raises
|
correct fix, NOT per-site `io = context.io` edits across the 17 sites), and the
|
||||||
`.canceled`). Regen `.ir` snapshots ONLY after green (`-Dupdate-goldens`) — adding `Io` to the
|
`!`-impl-warning fix. **Rewrite the async/await layer to the CORRECT idiom** (verified live):
|
||||||
prelude shifts many `.ir` type tables; confirm the diff is ONLY layout/numbering + the new
|
|
||||||
vtable, NO error text. The `Context` layout decision is settled: `{ allocator; data; io; }`
|
|
||||||
(allocator index 0 fixed by `call.zig:1229`, `io` last).
|
|
||||||
|
|
||||||
**API CORRECTION (user, 2026-06-20):** `async`'s args are a **variadic heterogeneous
|
async :: (io: Io, worker: Closure(..$args) -> $R, ..$args) -> Future($R) {
|
||||||
comptime pack** `..$args: []Type` ([specs.md:1383](../specs.md#L1383)), NOT a single `$A`.
|
f : Future($R) = ---; // `= ---` + field-assign, NOT a struct-literal return
|
||||||
Shape: `async(io, worker, ..args)` forwards a per-position-typed pack to `worker` (so
|
f.value = worker(..args); // blocking impl: run to completion
|
||||||
`context.io.async(fn, a, b, c)` works at any arity). The WIP's single-`arg: $A` form is
|
f.state = .ready;
|
||||||
superseded — re-land async with the pack. This is ORTHOGONAL to issue 0151 (which is about
|
return f;
|
||||||
the *return* type-var `$R` not binding in the body — needed regardless of arg shape); also
|
}
|
||||||
verify packs-in-fn-pointer-param-signatures work when re-landing (surface a new issue if not).
|
|
||||||
|
|
||||||
NOTE for the resume: do NOT add `io = context.io` to the 17 partial `push Context.{...}` sites
|
Worker is a **lambda** with **annotated params** (`(a: i64, b: i64) -> i64 => …`); name it
|
||||||
— the push-inherit-omitted fix (in the WIP diff) makes omitted fields inherit from the ambient
|
`async` (NOT `run` — `run` collides with `process.run` re-exported by std.sx and is silently
|
||||||
context, which is the correct fix and was verified to compile. Use that, not per-site edits.
|
shadowed). `Future($R)` is a parameterized `struct($T)` (so the bare-`-> $R`-return inference
|
||||||
|
gap is auto-avoided). **Avoid `Future(void)`** (issue 0150 SIGTRAP) — B1.2 supports non-void
|
||||||
|
workers; `timeout`/`Future(void)` defer to B1.4. Add `examples/1805-concurrency-io-blocking-
|
||||||
|
async.sx` (lock→green) + `1806-concurrency-io-cancel.sx`. Regen `.ir` ONLY after green
|
||||||
|
(`-Dupdate-goldens`) — adding `Io` to the prelude shifts many `.ir` type tables; confirm the
|
||||||
|
diff is ONLY layout/numbering + the new vtable, NO error text. `Context` layout settled:
|
||||||
|
`{ allocator; data; io; }` (allocator index 0 fixed by `call.zig:1229`, `io` last).
|
||||||
|
|
||||||
## Known issues / capability gaps
|
## Known issues / capability gaps
|
||||||
- **🔴 B1.2 BLOCKERS (both filed, both standalone-reproducible, both independent of the Io
|
- **🔴 B1.2 BLOCKERS (both filed, both standalone-reproducible, both independent of the Io
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
# PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler)
|
# PLAN-FIBERS — Stream B1 (fibers + Io + M:1 scheduler)
|
||||||
|
|
||||||
> **STATUS: 🚧 in progress.** B1.0 (`abi(.naked)` codegen) ✅ + B1.1 (per-fiber `context`
|
> **STATUS: 🚧 in progress.** B1.0 (`abi(.naked)`) ✅ + B1.1 (per-fiber `context`) ✅. **B1.2**
|
||||||
> root — zero compiler change, library convention) ✅ complete. **B1.2** (`Io` interface +
|
> (`Io` interface) is **UNBLOCKED** — the earlier "blockers" were artifacts of non-idiomatic
|
||||||
> `context.io` + `Future` + `cancel()`) is **🔴 BLOCKED on compiler issues 0150 (`void`
|
> syntax + a worker's dirty binary. Issue **0151 was INVALID** (the `($A)->$R` bare-fn-ptr
|
||||||
> struct field SIGTRAP) + 0151 (type-var from a fn-ptr-return not bound in body)** — the Io
|
> form is not idiomatic sx) and is **removed**. The correct `async` idiom **works today, no
|
||||||
> design is proven (the protocol-on-`Context` + blocking default + `context.io.now_ms()` work
|
> compiler change**: `async :: (io, worker: Closure(..$args) -> $R, ..$args) -> Future($R)`
|
||||||
> live), but `Future(void)`/`timeout` and the `async`/`await` generics hit those two bugs.
|
> with a **lambda worker** + the `result : Future($R) = ---; result.v = worker(..args);` build
|
||||||
> Resume B1.2 after both land (WIP saved at `.sx-tmp/b12-wip/`); see `CHECKPOINT-FIBERS.md`.
|
> form (mirrors the canonical `examples/0543-packs-canonical-map.sx`). Caveats: lambda params
|
||||||
|
> must be annotated; passing a bare *named* fn as the worker is non-idiomatic (use a lambda).
|
||||||
|
> Issue **0150** (`void` struct field SIGTRAP, exit 133) is a **real** bug but only hit by
|
||||||
|
> `Future(void)`/`timeout` — **deferred** (avoid void Futures in B1.2; revisit in B1.4). Resume
|
||||||
|
> B1.2 with the corrected idiom (the WIP at `.sx-tmp/b12-wip/` has the Io-protocol/Context/
|
||||||
|
> materializer parts that WORK; rewrite the async layer to the pack-lambda form above).
|
||||||
|
|
||||||
Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the
|
Carved from [PLAN-POST-METATYPE.md](PLAN-POST-METATYPE.md) Stream B (§B1) + the
|
||||||
design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
|
design-of-record [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md)
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
# 0151 — a type-var inferred from a fn-pointer parameter's RETURN type is not bound in the function body
|
|
||||||
|
|
||||||
## Status
|
|
||||||
OPEN — blocks Stream B1 (fibers) B1.2's `async(io, worker: ($A) -> $R, arg: $A)`
|
|
||||||
free-fn (it needs `Future(R)` in its body). Independent of the fibers work
|
|
||||||
(reproduces with a tiny `Wrap($R)` standalone).
|
|
||||||
|
|
||||||
## Symptom
|
|
||||||
A generic free function whose type-var `$R` is introduced **inside a
|
|
||||||
fn-pointer parameter's return type** (`worker: ($A) -> $R`) infers `$R` fine for
|
|
||||||
the call's type-checking, but `R` is **not in scope as a usable type name in the
|
|
||||||
function body**. Referencing `Wrap(R)` (or any `R`) in the body errors:
|
|
||||||
|
|
||||||
```
|
|
||||||
error: unknown type 'R'
|
|
||||||
--> repro.sx:4:14
|
|
||||||
|
|
|
||||||
4 | w : Wrap(R) = .{ v = worker(arg) };
|
|
||||||
```
|
|
||||||
|
|
||||||
By contrast a type-var introduced **directly** by a parameter (`arg: $A`) IS
|
|
||||||
usable in the body (`Wrap(A)` works — see the second repro). So the gap is
|
|
||||||
specific to type-vars that appear only nested in a fn-pointer (or closure)
|
|
||||||
parameter's signature.
|
|
||||||
|
|
||||||
Observed: `error: unknown type 'R'` for the body reference.
|
|
||||||
Expected: `R` binds to the worker's return type (here `i64`), so `Wrap(R)`
|
|
||||||
resolves to `Wrap(i64)`, exactly as `$A` → `A` does.
|
|
||||||
|
|
||||||
## Reproduction (fails)
|
|
||||||
```sx
|
|
||||||
#import "modules/std.sx";
|
|
||||||
|
|
||||||
Wrap :: struct($T: Type) { v: T; }
|
|
||||||
|
|
||||||
runit :: (worker: ($A) -> $R, arg: $A) -> Wrap($R) {
|
|
||||||
w : Wrap(R) = .{ v = worker(arg) }; // error: unknown type 'R'
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
|
|
||||||
dbl :: (n: i64) -> i64 { return n * 2; }
|
|
||||||
|
|
||||||
main :: () -> i32 {
|
|
||||||
r := runit(dbl, 21);
|
|
||||||
print("{}\n", r.v); // want: 42
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Reproduction (works — shows the contrast)
|
|
||||||
```sx
|
|
||||||
#import "modules/std.sx";
|
|
||||||
Wrap :: struct($T: Type) { v: T; }
|
|
||||||
runit :: (arg: $A) -> Wrap($A) {
|
|
||||||
w : Wrap(A) = .{ v = arg }; // OK — `A` (direct param type-var) binds
|
|
||||||
return w;
|
|
||||||
}
|
|
||||||
main :: () -> i32 { r := runit(21); print("{}\n", r.v); return 0; } // prints 21
|
|
||||||
```
|
|
||||||
|
|
||||||
## Suspected area
|
|
||||||
Generic monomorphization / type-binding collection (the pass that walks a generic
|
|
||||||
function's parameter signatures to discover `$X` type-vars and record the
|
|
||||||
caller-inferred binding for use in the body). It descends into direct param types
|
|
||||||
(`arg: $A` → binds `A`) but does NOT descend into a fn-pointer / closure
|
|
||||||
parameter's nested signature (`($A) -> $R`) to also bind `$R` from the matched
|
|
||||||
argument function's return type (and likewise `$A` from its params, if only named
|
|
||||||
there). Look for where `$`-type-params are gathered from `fd.params` and the
|
|
||||||
per-instance `type_bindings` map is seeded — likely in `src/ir/generic.zig`
|
|
||||||
and/or the call-site argument→param type-var unifier in `src/ir/lower/call.zig`.
|
|
||||||
The unifier already infers `$R` well enough to type-check the call (the error is
|
|
||||||
only in the BODY), so the binding exists at the call site but isn't propagated
|
|
||||||
into the monomorphized body's `type_bindings`.
|
|
||||||
|
|
||||||
## Investigation prompt (paste into a fresh session)
|
|
||||||
> A type-var that appears only inside a fn-pointer parameter's signature
|
|
||||||
> (`worker: ($A) -> $R`) is inferred at the call site (the call type-checks) but
|
|
||||||
> is NOT available as a type name in the generic function's BODY — `Wrap(R)` in
|
|
||||||
> the body errors `unknown type 'R'`, while a direct `arg: $A` makes `A` usable.
|
|
||||||
> Repro: `issues/0151-...` (the failing + the working contrast are both inline in
|
|
||||||
> the `.md`; the `.sx` is the failing one). Fix the generic type-binding pass so
|
|
||||||
> that when a generic fn is monomorphized, type-vars discovered inside a
|
|
||||||
> fn-pointer/closure parameter's nested signature (its params AND its return
|
|
||||||
> type) are added to the instance's `type_bindings` from the matched argument
|
|
||||||
> function's concrete signature — mirroring how direct param type-vars are bound.
|
|
||||||
> Suspected sites: `src/ir/generic.zig` (binding collection from `fd.params`) +
|
|
||||||
> the call-site unifier in `src/ir/lower/call.zig` (it already infers `$R` for
|
|
||||||
> overload/type-check, so reuse that result to seed the body bindings). Verify:
|
|
||||||
> the failing repro prints `42`; then move it to `examples/` as a regression test.
|
|
||||||
|
|
||||||
## Why this matters for B1 (fibers)
|
|
||||||
`async(io, worker: ($A) -> $R, arg: $A) -> Future($R)` is the central B1.2
|
|
||||||
ergonomic free-fn; its body builds `Future(R)`. Without this fix `async` can't be
|
|
||||||
written in its spec-faithful form. Routing around it (an explicit `$R: Type`
|
|
||||||
param the caller must pass) would change the surface and HIDE the gap — not done.
|
|
||||||
The rest of the B1.2 Io surface (the `Io` protocol on `Context`, the blocking
|
|
||||||
`CBlockingIo` default, `context.io.now_ms()`) works; only the `async`/`await`
|
|
||||||
generics are blocked by this. Saved WIP: `.sx-tmp/b12-wip/`.
|
|
||||||
Reference in New Issue
Block a user