Files
sx/issues/0194-wrapped-toplevel-asm-dropped.md
agra 22f4719e83 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.
2026-06-26 11:32:01 +03:00

4.7 KiB

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

asm {
    #string T
.global _fib_tramp
_fib_tramp:
    mov x0, x19
    bl _fib_dispatch
    brk #0
T,
};
fib_tramp :: () extern;

wrapped as

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.zigflattenComptimeConditionals (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.ziglowerMainAndComptime (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.