lowerAsmExpr stops bailing and builds the inline_asm op: resolves each operand's
effective name (§II.5 — explicit [name] else the {reg} pin), interns
template/constraints/clobbers, lowers input Refs, derives the result TypeId
(0→void, 1→T). Adds the last deferred validation (every %[name] must name an
operand). Multi-output (N>1) bails with a named "Phase E" diagnostic.
emitInlineAsm (backend/llvm/ops.zig) ports Zig's airAssembly: assembles the LLVM
constraint string (outputs → inputs → ~{clobber}, ',' → '|'), rewrites the
template (%[name]→${N}, %%→%, $→$$, %=→${:uid}), then LLVMGetInlineAsm +
LLVMBuildCall2 (AT&T dialect). Dispatch wired in emit_llvm.zig (replacing the C.0
@panic tripwire).
inferType gains an .asm_expr arm (expr_typer.zig) so a bare `x := asm {…-> T}`
binding types correctly — without it the binding inferred .unresolved and
silently produced 0.
llvm_shim.c: LLVMInitializeNativeAsmParser() — the JIT must assemble inline asm
at run time.
Verified end-to-end on the aarch64 host: `mov`/`add` with register-class inputs
and a value output run (exit 42/99), `nop volatile` runs (exit 0). IR is
textbook: `call i64 asm "add ${0},${1},${2}", "=r,r,r"(…)`.
Locked with 1645 (aarch64 add, runs; ir-only on non-aarch64) + 1646 (:= binding).
Updated 1640 (now Phase-E bail) + 1642 (now runs).
zig build test green (654 corpus, 446 unit).
189 lines
12 KiB
Markdown
189 lines
12 KiB
Markdown
# sx Inline Assembly — Checkpoint (ASM stream)
|
||
|
||
Companion to `current/PLAN-ASM.md`; design in
|
||
[docs/inline-asm-design.md](../docs/inline-asm-design.md). Update after every
|
||
commit, one step at a time per the cadence rule (no commit may both add a test
|
||
and make it pass).
|
||
|
||
## Last completed step
|
||
**C.1 + D** — inline asm CODEGEN (lowering builds the op + LLVM emit). **Inline
|
||
assembly now runs end-to-end.** `lowerAsmExpr` (`src/ir/lower/expr.zig`) stops
|
||
bailing: it resolves each operand's effective name (§II.5 auto-naming), interns
|
||
template/constraints/clobbers, lowers input `Ref`s, derives the result `TypeId`
|
||
(0→void, 1→T), and builds the `inline_asm` op. Added a `%[name]`-references-a-
|
||
real-operand check (the last deferred validation). Multi-output (N>1) still bails
|
||
loudly ("Phase E"). `emitInlineAsm` (`src/backend/llvm/ops.zig`, port of Zig's
|
||
`airAssembly`): assembles the LLVM constraint string (outputs→inputs→`~{clobber}`,
|
||
`,`→`|`), rewrites the template (`%[name]`→`${N}`, `%%`→`%`, `$`→`$$`, `%=`→
|
||
`${:uid}`), then `LLVMGetInlineAsm` + `LLVMBuildCall2` (AT&T). Dispatch wired
|
||
(`emit_llvm.zig`, replacing the C.0 `@panic`). **`llvm_shim.c`**: added
|
||
`LLVMInitializeNativeAsmParser()` — the JIT must assemble inline asm at run time.
|
||
Verified end-to-end: aarch64 `add`/`mov` run on the host (exit 42), `nop volatile`
|
||
runs (1642 now exit 0), IR is textbook (`call i64 asm "add ${0},${1},${2}",
|
||
"=r,r,r"(…)`). Locked with `examples/1645-platform-asm-aarch64-add.sx` (runs on
|
||
aarch64, ir-only elsewhere via `.build` + `.ir`). Also added the `inferType`
|
||
`.asm_expr` arm (`src/ir/expr_typer.zig`, 0→void / 1→T) — without it a bare
|
||
`x := asm {…-> T}` binding inferred `.unresolved` and silently produced 0;
|
||
regression-locked with `examples/1646-platform-asm-value-binding.sx`. Updated
|
||
1640 (now Phase-E bail) + 1642 (now runs). `zig build test` green (654 corpus,
|
||
446 unit). Files: `src/ir/lower/expr.zig`, `src/backend/llvm/ops.zig`,
|
||
`src/ir/emit_llvm.zig`, `src/ir/expr_typer.zig`, `llvm_shim.c`,
|
||
`examples/164{0,2,5,6}-*`.
|
||
|
||
Prior: **C.0** — IR op `inline_asm` (lock; no behavior change). Added `inline_asm:
|
||
InlineAsm` to the IR `Op` union + the `InlineAsm` struct (`template: StringId`,
|
||
`operands: []const AsmOperand` {role/name/constraint/operand}, `clobbers:
|
||
[]const StringId`, `has_side_effects`) in `src/ir/inst.zig` — all strings
|
||
interned, operands in source order, result on `Inst.ty`. The new variant forced
|
||
(and got) arms in two exhaustive `Op` switches: `src/ir/interp.zig` (loud
|
||
`bailDetail` — inline asm is never comptime-evaluable) and `src/ir/print.zig`
|
||
(IR dump). `src/ir/emit_llvm.zig` gets a `@panic` **tripwire** — emit lands in
|
||
Phase D, and until then `lowerAsmExpr` still bails so no `inline_asm` op is ever
|
||
created (reaching emit would be a lowering-switched-over-too-early bug). Unit
|
||
test `inline_asm op shape` in `src/ir/inst.test.zig`. `zig build test` green
|
||
(652 corpus, 446 unit). Files: `src/ir/inst.zig`, `src/ir/interp.zig`,
|
||
`src/ir/print.zig`, `src/ir/emit_llvm.zig`, `src/ir/inst.test.zig`.
|
||
|
||
Prior: **B.1** — operand-name validation (design §II.5 auto-naming rule). Extended
|
||
`lowerAsmExpr` with a `pinnedRegister(constraint)` helper (`"={eax}"`→`eax`,
|
||
`"+{rax}"`→`rax`, `"=r"`→null) and two checks: (1) **reject the echo form**
|
||
`[eax] "={eax}"` — a label identical to its own pinned register is redundant
|
||
(the operand is already auto-named after the register); (2) **reject duplicate
|
||
operand names** (ambiguous `%[name]` / result field). Locked with
|
||
`examples/1643-platform-asm-echo-name.sx` + `1644-platform-asm-duplicate-name.sx`.
|
||
`zig build test` green (652 corpus, 0 failed; 445 unit). Files:
|
||
`src/ir/lower/expr.zig`.
|
||
|
||
Prior: **B.0** — asm shape validation (compile-path diagnostics). Restructured the
|
||
`.asm_expr` lowering arm into `lowerAsmExpr` (`src/ir/lower/expr.zig`, mixed into
|
||
`Lowering` in `src/ir/lower.zig`): it validates BEFORE the not-yet-implemented
|
||
codegen bail, so the user sees the real problem first. Two checklist items now
|
||
enforced with named diagnostics: (1) **template must be a compile-time-known
|
||
string** (`"..."` / `#string`); (2) **no value outputs ⇒ must be `volatile`**
|
||
(mirrors Zig — a result-less asm could be deleted). Valid shapes still bail with
|
||
the "codegen not yet implemented" message. Result-type derivation + auto-naming
|
||
stay deferred to a later step (observable only once Phase C produces a real IR
|
||
op). Locked with `examples/1641-platform-asm-missing-volatile.sx` (volatile
|
||
error) + `1642-platform-asm-nop-volatile.sx` (volatile no-output accepted →
|
||
codegen bail). `zig build test` green (650 corpus, 0 failed; 445 unit). Files:
|
||
`src/ir/lower/expr.zig`, `src/ir/lower.zig`, `examples/164{1,2}-*`.
|
||
|
||
Prior: **A.1** — parse `asm { … }` + loud lowering bail (folded A.1+A.2 into one honest
|
||
lock commit, since the loud bail IS current correct behavior — cadence option
|
||
(a)). Added `AsmExpr`/`AsmOperand` to `src/ast.zig` + the `asm_expr` `Node.Data`
|
||
arm; `parseAsmExpr` in `src/parser.zig` (`parsePrimary` `.kw_asm` dispatch) —
|
||
parses the template, flat operand list (`[name]? "constraint" -> Type` value
|
||
output / `= expr` input), and `clobbers(.…)`; `volatile`/`clobbers` recognized
|
||
contextually via `isContextualWord`. The new `asm_expr` tag forced (and got)
|
||
arms in three exhaustive `Node.Data` switches: `src/sema.zig` `analyzeNode` +
|
||
`findNodeAtOffset`, `src/ir/semantic_diagnostics.zig` `checkBindingNames` (all
|
||
recurse into template + operand payloads). Lowering bails LOUD + named in
|
||
`src/ir/lower/expr.zig` ("inline assembly codegen is not yet implemented…") via
|
||
an explicit `.asm_expr` arm (not the generic `unknown_expr` else) returning
|
||
`emitPlaceholder`. `-> @place` write-through is rejected with a clear "Phase 2"
|
||
parse error. Locked with `examples/1640-platform-asm-parse.sx` (multi-output
|
||
`divmod`, named operands, register pins, clobbers — parses then bails; called
|
||
from `main`). `zig build test` green (648 corpus, 0 failed; 445 unit). Files:
|
||
`src/ast.zig`, `src/parser.zig`, `src/sema.zig`, `src/ir/semantic_diagnostics.zig`,
|
||
`src/ir/lower/expr.zig`, `examples/1640-*`.
|
||
|
||
Prior: **A.0** — `kw_asm` keyword (first compiler code). Added the `kw_asm` `Token.Tag`
|
||
variant + `.{ "asm", .kw_asm }` keyword-map entry in `src/token.zig`; `volatile` /
|
||
`clobbers` deliberately stay OUT of the global table (contextual). New exhaustive
|
||
`Tag` switch in `src/lsp/server.zig` `classifyToken` flagged the missing arm (the
|
||
intended coverage tripwire) — added `.kw_asm` to the keyword group. Lock test in
|
||
new `src/lexer.test.zig` (`asm`→`kw_asm`, `volatile`/`clobbers`→`identifier`),
|
||
wired into the `src/root.zig` barrel as `lexer_tests`. `zig build test` green (648
|
||
corpus, 0 failed; 445 unit, 0 failed — +1). Files: `src/token.zig`,
|
||
`src/lexer.test.zig`, `src/root.zig`, `src/lsp/server.zig`.
|
||
|
||
Prior: **0.2** — CLAUDE.md docs for `<name>.build`; **Phase 0 COMPLETE**.
|
||
**0.1** — corpus runner **ir-only branch** for cross-target examples. Replaced
|
||
0.0's loud placeholder bail: when `cfg.target` doesn't match the host (`ir_only`),
|
||
`sweepRoot` skips run/build/exec and verifies via `sx ir --target` only —
|
||
asserting `.exit` (ir cmd) + `.ir` (normalized stdout) + `.stderr`, never
|
||
`.stdout` (write skipped in update mode, assertion skipped in verify mode). An
|
||
`.ir` snapshot is **required** in ir-only mode — its absence is a loud failure
|
||
("needs an .ir snapshot for ir-only mode"). Locked with
|
||
`examples/1639-platform-target-cross.sx` (asm-free `main :: () -> i64 { return 0;
|
||
}`), `.build` `{ "target": "x86_64-linux" }`, + checked-in `.ir`. Verified both
|
||
guards fire: corrupting the `.ir` → IR mismatch; deleting it → the require-failure.
|
||
`zig build test` green (647 corpus, 0 failed; 444 unit). Files:
|
||
`src/corpus_run.test.zig`, `examples/1639-*`.
|
||
|
||
## Current state
|
||
**Inline assembly works end-to-end for 0/1 value outputs.** Pipeline complete:
|
||
lex (A.0) → parse (A.1) → validate (B.0/B.1 + the `%[name]` check) → IR op (C.0)
|
||
→ lower-builds-op + LLVM emit + JIT asm-parser init (C.1/D). Single-value-output
|
||
and no-output `volatile` asm assemble and execute on the host JIT; the auto-naming
|
||
rule (§II.5) is live (effective name = explicit `[name]` else `{reg}`). **Phase E
|
||
(multi-output tuples) is the remaining feature gap** — N>1 value outputs bail with
|
||
a named "Phase E" diagnostic (1640). `-> @place` write-through outputs are still
|
||
rejected at parse (Phase 2). Global asm (Phase F) not started.
|
||
|
||
Known orthogonal bug: **issue 0137** — `sx run` on a program with no `main`
|
||
segfaults (`src/target.zig:256-273`, unguarded JIT entry lookup). Pre-existing,
|
||
asm-independent; does NOT block the ASM stream (every example has a `main`).
|
||
|
||
Phase E–F feasibility already confirmed against the live tree
|
||
(`LLVMGetInlineAsm` / `LLVMBuildCall2` / `LLVMAppendModuleInlineAsm` in LLVM@19
|
||
`Core.h`; ERR-stream `extractvalue`→tuple in `emit_llvm.zig:726-927`; lib-less
|
||
`extern`, 60 sites; `--target` a global CLI flag).
|
||
|
||
## Next step
|
||
**Phase E** (multi-output tuples) — replace the N>1 "Phase E" bail in
|
||
`lowerAsmExpr`: build a tuple `TypeId` from the `out_value` types (named via the
|
||
effective-name rule), set it as the op result, and in `emitInlineAsm` make the
|
||
LLVM return type an anonymous struct `{T1,…,Tn}`, then `extractvalue i` per
|
||
`out_value` → assemble the sx tuple. Lock with `divmod`→`(quot,rem)` (reuse 1640's
|
||
shape, now running) + `cpuid`→4-tuple, arch-pinned. See `PLAN-ASM.md` Phase E +
|
||
design §II.6 (multi-return). Also worth adding: the x86_64-linux syscall-write
|
||
example (ir-only on this host via `.build { "target": "x86_64-linux" }` + `.ir`)
|
||
to lock the cross-target lowering, per the plan's D verification.
|
||
|
||
Then Phase 2 (`-> @place` write-through / read-write / indirect-memory) and Phase
|
||
F (global asm + `extern` call into asm symbols). Result-type derivation for the
|
||
0/1 cases now lives in BOTH `lowerAsmExpr` (the op's `Inst.ty`) and
|
||
`expr_typer.zig`'s `inferType` (for `:=`/value-position typing); Phase E extends
|
||
both to the tuple case.
|
||
|
||
## Log
|
||
- (init) Plan + design doc written; ASM stream opened.
|
||
- (0.0) Corpus runner target-gating: `<name>.build` JSON config (replaces `.aot`
|
||
marker), `--target` threading, `hostMatchesTarget` execute-gate, loud
|
||
cross-target placeholder bail. Migrated 1226/1227 `.aot`→`.build`; locked with
|
||
1638 fixture + unit tests. `zig build test` green.
|
||
- (0.1) ir-only branch: cross-target examples verify via `sx ir --target` only
|
||
(exit+ir+stderr, no stdout; `.ir` required). Locked with 1639 fixture; verified
|
||
corrupt-.ir → mismatch and missing-.ir → loud failure. `zig build test` green.
|
||
- (0.2) docs: CLAUDE.md documents `<name>.build` JSON sidecar (aot + target +
|
||
ir-only gating), replacing stale `.aot` marker prose. **Phase 0 COMPLETE.**
|
||
- (A.0) `kw_asm` keyword in token.zig (+ map entry); LSP `classifyToken` switch
|
||
coverage; lock test in new `lexer.test.zig` (wired via root.zig). `volatile` /
|
||
`clobbers` stay contextual identifiers. `zig build test` green (445 unit, +1).
|
||
- (A.1) parse `asm { … }` → `AsmExpr` + loud lowering bail; `asm_expr` arms in 3
|
||
exhaustive `Node.Data` switches; `-> @place` rejected (Phase 2). Adopted operand
|
||
auto-naming rule (design §II.5). Locked with 1640 fixture. Filed orthogonal
|
||
issue 0137 (no-`main` JIT segfault). `zig build test` green (648 corpus, 445 unit).
|
||
- (B.0) asm shape validation in `lowerAsmExpr`: comptime-string template +
|
||
no-output⇒volatile, with named diagnostics before the codegen bail. Locked with
|
||
1641 (volatile error) + 1642 (volatile accepted). `zig build test` green (650
|
||
corpus, 445 unit).
|
||
- (B.1) operand-name validation: `pinnedRegister` helper + reject echo form
|
||
(`[eax] "={eax}"`) and duplicate names. Locked with 1643 + 1644. `zig build
|
||
test` green (652 corpus, 445 unit).
|
||
- (C.0) IR op `inline_asm: InlineAsm` + interp `bailDetail` + print arm + emit
|
||
`@panic` tripwire (Phase D). No behavior change (lowering still bails). Unit
|
||
test `inline_asm op shape`. `zig build test` green (652 corpus, 446 unit).
|
||
- (C.1+D) CODEGEN — `lowerAsmExpr` builds the op (effective names, interned
|
||
strings, input Refs, 0/1 result type) + `%[name]` validation; `emitInlineAsm`
|
||
(constraint string + template rewrite + `LLVMGetInlineAsm`/`BuildCall2`, AT&T);
|
||
`inferType` arm; `LLVMInitializeNativeAsmParser` for the JIT. **Inline asm runs
|
||
end-to-end.** N>1 bails (Phase E). Locked with 1645 (aarch64 add, runs) + 1646
|
||
(`:=` binding); updated 1640/1642. `zig build test` green (654 corpus, 446 unit).
|
||
|
||
## Known issues
|
||
- **0137** — `sx run` on a program with no `main` segfaults (unguarded JIT entry
|
||
lookup, `src/target.zig:256-273`). Pre-existing, asm-independent. Filed
|
||
`issues/0137-jit-run-no-main-segfault.md`. Does not block A.1.
|