From a7499d5f51938a0c95398d8920019530667c8674 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 21 Jun 2026 05:45:27 +0300 Subject: [PATCH] =?UTF-8?q?fibers=20B1.2:=200152=20fixed=20=E2=86=92=20Ato?= =?UTF-8?q?mic(bool)=20works;=20blocked=20on=200153=20(re-export=20value-f?= =?UTF-8?q?ailable=20loses=20!=20channel)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With 0151 + 0152 fixed, the async surface is callable and Atomic(bool) works. Building the async examples isolated the TRUE remaining blocker (the earlier 'secondary or PHI' symptom, confirmed NOT an Atomic cascade): a re-exported generic value-failable ($R, !E) fn loses its ! error channel at the call site — the result types as a plain tuple, so await(...) or { ... } / try ...await() fail / build a malformed i1 PHI. await/IoErr are re-exported via std.sx, so the async surface hits it. Narrowed to the generic + re-export co-requirement (non-generic re-export OK; direct generic import OK). Filed issues/0153 with a minimal co-located 2-file repro + a single-file stdlib-await repro + investigation prompt (root cause: the monomorphized return-type's error-set, reached via the re-export alias, resolves to a non-.error_set TypeId, so errorChannelOf misses the channel). Per the STOP rule, paused B1.2's async examples pending the 0153 fix. --- current/CHECKPOINT-FIBERS.md | 103 ++++++++++------ ...eric-value-failable-loses-error-channel.md | 110 ++++++++++++++++++ ...eric-value-failable-loses-error-channel.sx | 20 ++++ .../lib.sx | 8 ++ 4 files changed, 207 insertions(+), 34 deletions(-) create mode 100644 issues/0153-reexport-generic-value-failable-loses-error-channel.md create mode 100644 issues/0153-reexport-generic-value-failable-loses-error-channel.sx create mode 100644 issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx diff --git a/current/CHECKPOINT-FIBERS.md b/current/CHECKPOINT-FIBERS.md index 69c082ef..37a50f9c 100644 --- a/current/CHECKPOINT-FIBERS.md +++ b/current/CHECKPOINT-FIBERS.md @@ -4,11 +4,25 @@ Companion to [PLAN-FIBERS.md](PLAN-FIBERS.md). Update after every step (one step per the cadence rule). New corpus category: `18xx` concurrency. ## Last completed step -**issue 0151 FIXED — generic `$T` now infers through generic-struct / pointer / UFCS-pack -params.** `await`/`cancel`/`async` are CALLABLE again. The async SURFACE is now blocked on a -DIFFERENT, freshly-exposed codegen bug: **issue 0152 — `Atomic(bool)` emits a sub-byte (i1) -atomic that LLVM rejects** (the `Future.canceled: Atomic(bool)` field hits it). Suite green -**728/0**, master clean. +**issues 0151 + 0152 FIXED.** The async surface is CALLABLE and `Atomic(bool)` works. One LAST +blocker remains, freshly isolated: **issue 0153 — a re-exported generic value-failable +`($R, !E)` loses its `!` error channel at the call site** (typed as a plain tuple), so +`await(...) or { … }` builds a malformed PHI. `await`/`IoErr` are re-exported via `std.sx`, +so the async surface hits it. Suite green **729/0**, master clean. +- **0152 fix (committed):** the atomic load/store emitters in `src/backend/llvm/ops.zig` now + byte-promote a sub-byte (`bool`→`i1`) access to its `i8` storage type and `trunc`/`zext` the + value at the boundary (new `atomicByteType` helper). rmw/cmpxchg left as-is (a `bool` rmw/CAS + is rejected at the sx level — integer-only — so a sub-byte element never reaches them). + Regression: `examples/1705-atomics-bool-byte-promoted.sx`. Issue 0152 marked RESOLVED. +- **0153 filed (the remaining blocker):** generic + re-export is the co-requirement — a + non-generic re-export keeps the channel, and a directly-imported generic value-failable keeps + it; only the combination drops the `!`. Minimal co-located 2-file repro + a single-file + stdlib-`await` repro + investigation prompt (root cause: the monomorphized return-type's + error-set, reached via the re-export alias, resolves to a non-`.error_set` TypeId, so + `errorChannelOf` no longer sees the failable channel). See `issues/0153-...`. + +### Earlier — issue 0151 FIXED (committed `362674f`) +Generic `$T` now infers through generic-struct / pointer / UFCS-pack params — details below. - **0151 fix (committed):** four gaps closed on the inference + UFCS-dispatch path — (1) `extractTypeParam`/`matchTypeParam(Static)` got a `parameterized_type_expr` arm (recover the arg instance's recorded per-param bindings via `struct_instance_bindings` + @@ -101,26 +115,28 @@ body); closed + locked. The review's `.naked`-lambda CRITICAL was a false positi (unparseable — `isLambda` breaks on the `abi` keyword). ## Current state -**B1.2 Io capability is LANDED; the async surface is now CALLABLE but blocked on issue 0152.** -Master GREEN (728/0), installed `sx` clean. -- **0151 fixed (this session, committed):** the generic-inference engine now binds `$T` - through a generic-struct param head (`Box($T)`), through a pointer (`*Box($T)`, incl. the - UFCS auto-address-of receiver), and through a closure-return-via-pack on the UFCS dot-call - path. So `async`/`await`/`cancel` in `library/modules/std/io.sx` — all `ufcs`, with - `await`/`cancel` taking `*Future($R)` — are callable in every form. Details in the - "Last completed step" block above; regression tests `examples/0214` + `0215`. -- **NEW blocker — issue 0152 (`Atomic(bool)` → sub-byte i1 atomic):** with the surface - callable, building the async examples now trips LLVM verification: `Atomic($T)` lowers a - `bool` element to `i1` and emits an `i1` atomic load/store, which LLVM rejects ("atomic - memory access' size must be byte-sized"). `Future.canceled: Atomic(bool)` hits this in - `async`/`cancel`/`await`. Standalone repro + investigation prompt filed at - `issues/0152-atomic-bool-sub-byte-atomic-llvm-reject.md`. The fix is codegen-level - (`src/backend/llvm/ops.zig` atomic emitters — promote sub-byte atomics to `i8` storage with - `trunc`/`zext` at the value boundary). A secondary `or`-merge PHI i1/i64 mismatch appeared in - the same probe — likely entangled with the malformed `Atomic(bool)` field; re-check after 0152. +**B1.2 Io capability LANDED; async surface CALLABLE + `Atomic(bool)` works; ONE blocker left +(issue 0153).** Master GREEN (729/0), installed `sx` clean. Three of the four B1.2 surface bugs +are now closed: +- **0151 fixed + committed** (`362674f`): generic `$T` infers through generic-struct / pointer + / UFCS-pack params. `async`/`await`/`cancel` dispatch correctly. Regression `0214` + `0215`. +- **0152 fixed + committed**: `Atomic(bool)` load/store byte-promoted to `i8` in the codegen + emitters. `Future.canceled: Atomic(bool)` works. Regression `1705`. +- **0153 (the remaining blocker, filed):** a re-exported generic value-failable `($R, !E)` + loses its `!` channel at the call site (plain tuple), so `await(...) or { … }` / `try …await()` + reject it / build a malformed `i1` PHI. `await`/`IoErr` are re-exported via `std.sx`, so the + async examples hit it. The earlier "secondary `or` PHI" symptom was THIS, not an `Atomic` + cascade (it persists after 0152). Minimal co-located repro + investigation prompt at + `issues/0153-reexport-generic-value-failable-loses-error-channel.{md,sx}`. Root cause: the + monomorphized return-type's error-set, reached via the re-export alias, resolves to a + non-`.error_set` TypeId, so `errorChannelOf` (`lower/error.zig:148`) no longer sees the channel. - Issue **0150** (`void` struct field → SIGTRAP) remains DEFERRED — only `Future(void)` / `timeout`, which are B1.4. +Verified live (with all three patches in tree, before reverting nothing — 0151/0152 are +committed, 0153 outstanding): `async`/`await` dispatch + build correct Futures; the ONLY failure +is the lost `!` channel from 0153. A working dispatch probe is at `.sx-tmp/async_surface.sx`. + ### B1.2 Io capability — what is LANDED + verified (commit 45d869d) - `Io :: protocol #inline { spawn_raw; suspend_raw -> !; ready; poll; now_ms; arm_timer; }` in `core.sx` next to `Allocator`, with `SpawnOpts{ pin: PinTarget }` + `ParkToken{ handle }`. @@ -178,17 +194,17 @@ fibers/Io/scheduler code yet. Grounded floor facts: boundary; a sharper sx diagnostic for it is a candidate polish, not a blocker. ## Next step -**BLOCKED on issue 0152 (`Atomic(bool)` sub-byte atomic).** Once it lands, complete B1.2's -async surface: +**BLOCKED on issue 0153 (re-exported generic value-failable loses its `!` channel).** It is the +LAST B1.2 surface blocker — 0151 + 0152 are fixed. Once 0153 lands, complete B1.2's async +surface: 1. **Add the async examples:** `examples/1805-concurrency-io-blocking-async.sx` (`context.io.async((a:i64,b:i64)->i64 => a+b, 40, 2).await() or {…}` → 42; `double:`/`sum:`) + `1806-...-io-cancel.sx` (cancel → `await` raises `.Canceled`). The async surface is already - callable (0151 fixed); a working probe lives at `.sx-tmp/async_surface.sx` — it builds the - Futures and dispatches `async`/`await`/`cancel` correctly, only the `Atomic(bool)` codegen - (0152) stops it. lock→green, then regen `.ir` (confirm layout-only). - - Note: `await` returns a value-failable `($R, !IoErr)`; use `f.await() or { default }` (a - braced `or`, not `catch `). Re-verify the `or`-merge PHI i1/i64 symptom is gone once - 0152 lands (it was likely a cascade from the malformed `Atomic(bool)` field). + callable + `Atomic(bool)` works; a working dispatch probe lives at `.sx-tmp/async_surface.sx` + — it builds the Futures and dispatches `async`/`await`/`cancel` correctly; ONLY the lost `!` + channel (0153) on `await(...) or { … }` stops it. lock→green, then regen `.ir` (layout-only). + - `await` returns a value-failable `($R, !IoErr)`; use `f.await() or { default }` (a braced + `or`, not `catch `). 2. Then B1.2 is truly done → proceed to **B1.3 (fiber runtime)**. **Deferred (do NOT block B1.2 on these):** issue **0150** (`void` struct field SIGTRAP) — only @@ -200,10 +216,14 @@ done, inference incomplete); a dedicated effort; lambda workers are the B1.2 idi `call.zig:1229`, io last). Io protocol + materializers + push-inherit are LANDED + reviewed. ## Known issues / capability gaps -- **🔴 B1.2 BLOCKER — issue 0152** (`Atomic(bool)` → sub-byte i1 atomic load/store; LLVM - rejects "atomic memory access' size must be byte-sized"). Blocks the async examples via - `Future.canceled: Atomic(bool)`. Standalone repro + fix prompt: `issues/0152-...`. Codegen- - level (`src/backend/llvm/ops.zig` atomic emitters; promote sub-byte → i8 storage). +- **🔴 B1.2 BLOCKER — issue 0153** (re-exported generic value-failable `($R, !E)` loses its `!` + channel; the call result types as a plain tuple, so `await(...) or { … }` / `try …await()` + fail / build a malformed i1 PHI). `await`/`IoErr` re-exported via `std.sx` hit it. Co-located + repro + fix prompt: `issues/0153-...`. Root cause: the monomorphized return-type's error-set, + reached via the re-export alias, resolves to a non-`.error_set` TypeId (so `errorChannelOf` + misses it). The LAST B1.2 surface blocker. +- **✅ issue 0152 — FIXED this session** (`Atomic(bool)` sub-byte i1 atomic → byte-promoted to i8 + in the load/store emitters). Regression: `examples/1705`. Unblocked `Future.canceled`. - **✅ issue 0151 — FIXED this session** (generic `$T` through generic-struct / pointer / UFCS-pack params). Regression: `examples/0214` + `0215`. Was the original B1.2 surface blocker. - **issue 0150** (deferred) — a `void` struct field crashes the compiler (unsized-type SIGTRAP @@ -319,3 +339,18 @@ done, inference incomplete); a dedicated effort; lambda workers are the B1.2 idi i1 atomic, LLVM reject; `Future.canceled` hits it). Filed with standalone repro + fix prompt. Per the STOP rule: shipped the 0151 fix, filed 0152, STOPPED. Resume the async examples (1805/1806) after 0152. +- **0152 FIXED** — the atomic load/store emitters (`src/backend/llvm/ops.zig`) byte-promote a + sub-byte (`bool`→`i1`) access to its `i8` storage type and `trunc`/`zext` the value at the + boundary (new `atomicByteType` helper). rmw/cmpxchg left as-is (a `bool` rmw/CAS is rejected + at the sx level — integer-only — so a sub-byte element never reaches them; comments record + this). Regression `examples/1705-atomics-bool-byte-promoted.sx` (load/store round-trip). Issue + 0152 marked RESOLVED. Suite green 729/0. With `Atomic(bool)` working, the async surface + exposed the TRUE remaining blocker — **issue 0153**: a re-exported generic value-failable + `($R, !E)` loses its `!` channel at the call site (the earlier "secondary `or` PHI" symptom + was this, NOT an `Atomic` cascade — confirmed it persists after 0152). Narrowed to the + generic+re-export co-requirement (non-generic re-export OK; direct generic import OK; only the + combination drops `!`). Root cause: the monomorphized return-type's error-set, reached via the + re-export alias, resolves to a non-`.error_set` TypeId, so `errorChannelOf` + (`lower/error.zig:148`) misses the channel. Filed `issues/0153-...` with a minimal co-located + 2-file repro + a single-file stdlib-`await` repro + investigation prompt. Per the STOP rule: + shipped the 0152 fix, filed 0153, STOPPED. Resume the async examples after 0153. diff --git a/issues/0153-reexport-generic-value-failable-loses-error-channel.md b/issues/0153-reexport-generic-value-failable-loses-error-channel.md new file mode 100644 index 00000000..59733ee2 --- /dev/null +++ b/issues/0153-reexport-generic-value-failable-loses-error-channel.md @@ -0,0 +1,110 @@ +# 0153 — a re-exported generic value-failable `($R, !E)` loses its `!` error channel + +## Symptom + +A generic function returning a value-failable `($R, !E)` keeps its error +channel when called from the module that declares it, but **loses it when +the function is reached through a re-export alias** (`get :: lib.get;`). At +the consumer the call result is typed as a plain **tuple** (last field is a +*non*-`.error_set` type), so: + +- `try f()` → `error: `try` requires a failable expression; operand has type 'tuple'` +- `f() or { default }` → LLVM verification failure — the `or` merge PHI is + typed `i1` (the lost-channel discriminant) but carries the `i64` default: + ``` + PHI node operands are not the same type as the result! + %bp = phi i1 [ true, %entry ], [ -1, %or.rhs.0 ] + ``` + +It requires **both** conditions — drop either and it works: + +- non-generic re-exported value-failable + `or` → **works** +- generic value-failable imported **directly** (no re-export) + `or` → **works** +- generic value-failable, **direct call** (no UFCS) through a re-export → **fails** too + (so it is NOT UFCS-specific) + +## Reproduction + +Co-located minimal repro (two files, no project deps beyond `modules/std.sx`): +`issues/0153-...sx` (consumer) + `issues/0153-.../lib.sx` (impl). Run the +consumer; expect `r=42`, get the PHI verification failure. + +```sx +// lib.sx +#import "modules/std.sx"; +LE :: error { Bad } +Box :: struct ($R: Type) { v: R; } +get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; } +``` +```sx +// main +#import "modules/std.sx"; +lib :: #import ".../lib.sx"; +Box :: lib.Box; // re-export the generic struct, +get :: lib.get; // the generic value-failable fn, +LE :: lib.LE; // AND its error set (the std.sx facade pattern) +main :: () -> i32 { + b : Box(i64) = .{ v = 42 }; + r := b.get() or { -1 }; // ← PHI i1/i64 mismatch + print("r={}\n", r); + return 0; +} +``` + +Real-world one-liner (same bug, via the stdlib facade — `await`/`IoErr` are +re-exported from `std/io.sx` through `std.sx`): + +```sx +#import "modules/std.sx"; +#import "modules/std/atomic.sx"; +main :: () -> i32 { + f : Future(i64) = ---; + f.value = 42; f.state = .ready; f.canceled = Atomic(bool).init(false); + r := f.await() or { -1 }; // ← same PHI mismatch + print("r={}\n", r); + return 0; +} +``` + +## Impact + +Blocks the B1.2 async surface (the LAST blocker after 0151 + 0152). `await` +returns `($R, !IoErr)` and is re-exported via `std.sx` +(`await :: io_mod.await; IoErr :: io_mod.IoErr;`), so every +`context.io.async(...).await() or { … }` / `try …await()` hits this. The +async runtime itself is correct (Futures build, `$R` infers, the value is +right) — only the call-site failable typing is wrong. + +## Investigation prompt + +A value-failable `(T, !E)` is represented as a **tuple whose LAST field is +an `.error_set` TypeId** — that is exactly what `Lowering.errorChannelOf` +(`src/ir/lower/error.zig:148`) keys on. The bug is that the call-result type +inferred for a re-exported generic fn is a tuple whose last field is NOT an +`.error_set`, so `errorChannelOf` returns null (→ "plain tuple"). + +Suspect: the generic return-type resolution +(`inferGenericReturnType` / `buildTypeBindings` in `src/ir/generics.zig` + +`monomorphizeFunction` in `src/ir/lower/generic.zig`) resolves the fn's +return-type AST `($R, !LE)` in a module context where the error-set name +reached through the re-export alias (`LE :: lib.LE`) resolves to a TypeId +that is NOT tagged `.error_set` (a duplicate/plain interning of the aliased +error type, or the alias is followed to a non-error-set placeholder). The +"generic + re-export" co-requirement points at the monomorphized return-type +path specifically — a non-generic re-export keeps the channel (its return +type isn't re-resolved per-instance), and a direct generic import keeps it +(the error set resolves in its own module). + +Steps: +1. At the consumer call site, dump the inferred call-result TypeId for + `b.get()` and inspect its last tuple field's `TypeInfo` — confirm it is + NOT `.error_set` (vs the direct-import case, where it IS). +2. Trace where the aliased error-set name (`LE` / `IoErr`) is resolved during + the instance's return-type construction; ensure it resolves to the SAME + `.error_set` TypeId the declaring module interned (follow the re-export + alias to the original error set, don't re-intern a plain type). + +Verification: run the co-located repro; expect `r=42`. Then restore the B1.2 +async examples (`examples/1805-concurrency-io-blocking-async.sx` + +`1806-...-io-cancel.sx`) per CHECKPOINT-FIBERS and confirm +`sum: 42` / `double: 42` / cancel raises `.Canceled`. diff --git a/issues/0153-reexport-generic-value-failable-loses-error-channel.sx b/issues/0153-reexport-generic-value-failable-loses-error-channel.sx new file mode 100644 index 00000000..300eefb5 --- /dev/null +++ b/issues/0153-reexport-generic-value-failable-loses-error-channel.sx @@ -0,0 +1,20 @@ +// Repro for issue 0153 — a generic value-failable fn `($R, !E)` reached +// through a RE-EXPORT alias loses its `!` error channel at the call site: +// the result is typed as a plain tuple, so `try`/`or` reject it / build a +// malformed PHI. Needs BOTH generic + re-export: a non-generic re-export +// works, and a directly-imported (non-re-exported) generic value-failable +// works. Mirrors std.sx's `await :: io_mod.await` (+ `IoErr :: io_mod.IoErr`). +#import "modules/std.sx"; +lib :: #import "issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx"; + +// Re-export the generic fn AND its error set (the std.sx facade pattern). +Box :: lib.Box; +get :: lib.get; +LE :: lib.LE; + +main :: () -> i32 { + b : Box(i64) = .{ v = 42 }; + r := b.get() or { -1 }; // BUG: PHI i1/i64 mismatch (was: clean → r=42) + print("r={}\n", r); + return 0; +} diff --git a/issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx b/issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx new file mode 100644 index 00000000..848f3c22 --- /dev/null +++ b/issues/0153-reexport-generic-value-failable-loses-error-channel/lib.sx @@ -0,0 +1,8 @@ +// Implementation module: a generic value-failable `ufcs` fn + its error set. +#import "modules/std.sx"; + +LE :: error { Bad } +Box :: struct ($R: Type) { v: R; } + +// Returns `($R, !LE)` — a value-failable. `$R` is inferred from the arg. +get :: ufcs (b: *Box($R)) -> ($R, !LE) { return b.v; }