fix: aarch64-linux port of the M:1 fiber runtime (sched.sx)

Port library/modules/std/sched.sx to run on aarch64-linux alongside
aarch64-macOS, validated byte-identical on both via Apple `container`.

Per-OS bits are comptime-branched:
- MAP_AP (mmap MAP_ANON flag): linux 0x22 / macOS 0x1002.
- fd-readiness backend: epoll on linux, kqueue on darwin (epoll import
  scoped to the linux branch). block_on_fd, the run-loop Mode-2 drain,
  and cancel_io_waiter_for each branch; the epoll paths EPOLL_CTL_DEL on
  fire and on early-wake (EPOLLONESHOT only disables a registration;
  kqueue EV_ONESHOT auto-removes it).
- first-entry trampoline: a per-OS hand-written global-asm symbol becomes
  a naked sx fn fib_tramp (mov x0,x19; br x20) + register-indirect
  dispatch (spawn presets regs[1] == x20 == &fib_dispatch), dropping the
  per-OS .global symbol entirely.

Fixes issue 0193 Bug A: the trampoline redesign bus-errored on the
go/wait/sleep capstone (1817) until `export "fib_dispatch"` was restored.
Without the export, fib_dispatch reverts to sx's internal ABI (x0 =
implicit context, first arg self shifted to x1) while the trampoline
hands self over in x0 (C-ABI); on first entry the body runs (x1 happens
to alias self) but the closure then loads regs[1] == &fib_dispatch as its
first capture and re-invokes fib_dispatch forever -> stack overflow ->
bus error. The export pins fib_dispatch to the C-ABI (self in x0),
matching the trampoline. Root cause found via lldb on an AOT build;
confirmed against the compiler source.

Bug B (a top-level asm block wrapped in inline-if is dropped during the
comptime-conditional flatten) is carved out to issue 0194 (OPEN) -- no
live trigger remains, since the naked-fn trampoline sidesteps it.

1811/1814/1816/1817 run byte-identical on the aarch64-macOS host and in
an aarch64-linux container; full suite green (817/0). Documents the fiber
runtime in readme.md.
This commit is contained in:
agra
2026-06-26 11:32:01 +03:00
parent 7218280bf0
commit 22f4719e83
5 changed files with 370 additions and 65 deletions

View File

@@ -1,8 +1,35 @@
# Issue 0193 — linux fiber-runtime port (sched.sx) + a wrapped top-level `asm` drop
Status: **OPEN.** Two intertwined items uncovered while porting `library/modules/std/sched.sx`
(the M:1 fiber runtime) to aarch64-linux. The WIP sched.sx port is preserved in
`git stash` (`stash@{0}`, "WIP on fix/0192-qualified-import-const-comptime") — pop it to resume.
> **RESOLVED — port landed on aarch64-linux.**
>
> **Bug A (register-indirect trampoline bus-errors on 1817): FIXED.** Root cause found via lldb on an
> AOT macOS build (the bug reproduced on macOS too, so no container needed): the WIP port had dropped
> `export "fib_dispatch"` from `fib_dispatch`. Without the export the fn reverts to sx's INTERNAL
> calling convention, which reserves x0 for the implicit `context` pointer and shifts the first real
> arg `self` to x1 — but the trampoline (`mov x0, x19; br x20`) hands the fiber over in x0, C-ABI
> style. On first entry x1 coincidentally aliases `&fiber.ctx == self` (left there by the scheduler's
> prior `swap_context(from, to)`, x1 = to), so the body runs once; but inside it the closure loads
> `[Fiber+8] == ctx.regs[1] == &fib_dispatch` as its "first capture" and re-invokes `fib_dispatch`
> forever → stack overflow → bus error. **Fix:** restore `export "fib_dispatch"` so the fn keeps the
> C-ABI (`self` in x0), matching what the trampoline supplies — a one-line library change, no compiler
> change. The register-indirect naked-fn trampoline design is kept (it sidesteps Bug B's hand-written
> per-OS global-asm symbol). Adversarially reviewed against the compiler source (`src/ir/lower/decl.zig`
> `funcWantsImplicitCtx`/`wants_ctx`/`CallingConvention.c`); root cause + fix confirmed CORRECT.
>
> **Validation:** 1811 / 1814 / 1816 / 1817 (the go/wait/sleep capstone) all run **byte-identical** on
> the aarch64-macOS host AND in an aarch64-linux Apple `container` (`sum: 123`, completion order
> `2@10 3@20 1@30`, etc.). Full `zig build test` macOS suite GREEN (817/0).
>
> **Bug B (wrapped top-level `asm` dropped): carved out to `issues/0194-wrapped-toplevel-asm-dropped.md`
> as an OPEN compiler bug.** It is no longer triggered anywhere in the tree (the port no longer uses a
> wrapped global-asm block), so it does not block anything — but it is a real defect and stays filed.
>
> Original writeup below for history.
---
Status: **(historical — see RESOLVED banner above).** Two intertwined items uncovered while porting
`library/modules/std/sched.sx` (the M:1 fiber runtime) to aarch64-linux.
The epoll *bindings* + `std.event.Loop` epoll backend are already committed (`cc137002`) and
**runtime-validated on real Linux** via Apple `container` (see the event.sx VALIDATION note / the

View File

@@ -0,0 +1,99 @@
# Issue 0194 — a top-level global `asm` block wrapped in `inline if` / `case` is DROPPED
Status: **OPEN.** Carved out of issue 0193 (the linux fiber-runtime port). The port itself is
RESOLVED — it sidesteps this bug entirely by using a naked-sx-fn trampoline (`fib_tramp`) plus a
register-indirect `br x20` instead of a hand-written global-asm symbol, so there is **no live trigger
for this bug in the tree today.** It is filed standalone so the compiler defect is not lost.
## Symptom
A top-level global `asm { … }` block that defines a symbol (e.g. `.global _foo` / `_foo: …`) is
**not emitted** when it is wrapped in a comptime `inline if OS == { case … }` (or
`inline if OS == .linux { asm } else { asm }`). `nm main.o` shows the symbol as `U` (undefined) and
the link fails on both platforms. A PLAIN, unwrapped top-level `asm { … }` emits fine.
- **Observed:** symbol undefined, link error.
- **Expected:** the `asm` block in the taken comptime arm emits its template into the module's global
asm exactly as an unwrapped block would (the comptime-conditional pre-pass already surfaces the
taken arm's *other* top-level decls — fns, consts, imports — correctly; only the `asm_global` node
is lost).
## Reproduction
**Not yet reproducible in isolation.** During the 0193 port, minimal/medium repros ALL emitted +
linked correctly: a top-level `asm` in a single `case`; two `case` blocks; a `case` asm in an
imported module; a naked fn + `case` asm with `bl` to an exported fn; a one-sided
`inline if .linux { #import }` before the asm. **Only the full `library/modules/std/sched.sx`
dropped it** — so the trigger is an interaction with something else in that module, not the wrapped
`asm` alone.
The exact form that triggered it (now replaced on the branch, recoverable from history): the original
global trampoline
```sx
asm {
#string T
.global _fib_tramp
_fib_tramp:
mov x0, x19
bl _fib_dispatch
brk #0
T,
};
fib_tramp :: () extern;
```
wrapped as
```sx
inline if OS == {
case .linux: asm { #string T
fib_tramp:
mov x0, x19
bl fib_dispatch
br x30
T, };
case .macos: asm { #string T
.global _fib_tramp
_fib_tramp:
mov x0, x19
bl _fib_dispatch
brk #0
T, };
}
```
dropped the asm in BOTH arms (whichever was taken). See `issues/0193-linux-fiber-port.patch` for the
full module context that triggers it, and the 0193 writeup for the larger investigation history.
## Investigation prompt (ready to paste)
> A top-level global `asm` block defining a symbol is dropped when wrapped in a comptime
> `inline if OS == { case … }` — but only inside the full `library/modules/std/sched.sx`; it can't be
> reproduced in isolation. Find where the surfaced `asm_global` node is lost between the
> comptime-conditional flatten and IR lowering.
>
> Key files:
> - `src/imports.zig` — `flattenComptimeConditionals` (line ~38) + `appendBranchDecls` (line ~72): the
> pre-pass that surfaces a taken comptime arm's top-level decls. It *appears* correct — it appends
> every node of the taken branch's block, `asm_global` included — so confirm the flattened slice
> actually carries the `asm_global` node (dump `flat_decls` at `src/imports.zig:932`).
> - `src/ir/lower/decl.zig` — `lowerMainAndComptime` (line ~1494), whose `.asm_global` arm (line ~1503)
> appends the verbatim template to `self.module.global_asm`. **Prime suspect:** does the lowering
> entry point feed `lowerMainAndComptime` the *flattened* decl list, or a pre-flatten `root.decls`
> that never contains the surfaced (formerly-nested) `asm_global`? If the asm-emission pass walks a
> different decl list than the one flattening wrote to, a surfaced `asm_global` is silently skipped.
> - `src/ir/emit_llvm.zig:384` — where `module.global_asm` is concatenated into the LLVM module. If the
> node never reached `global_asm`, it never emits.
>
> Steps: (1) build sched.sx's wrapped-asm variant (recover from `issues/0193-linux-fiber-port.patch`
> or git history of branch `fix/0192-qualified-import-const-comptime`), (2) instrument
> `flattenComptimeConditionals` to log whether the `asm_global` node survives into `flat_decls`,
> (3) instrument `lowerMainAndComptime` to log whether it ever *sees* an `asm_global`, (4) bisect what
> else in sched.sx must be present for the drop to occur (the isolation repros lacked it).
> Verification: `nm` the object shows the wrapped-asm symbol DEFINED (not `U`); the wrapped form links
> and runs identically to a plain unwrapped `asm`.
>
> **Verify it isn't a syntax issue first:** it reproduces with both the `case` and `if/else` forms,
> and plain unwrapped asm emits fine — so the wrapping, not the asm itself, is the trigger. That points
> to the flatten/lowering interaction, not user error.