feat(asm): Phase C.1 + D — inline asm codegen (runs end-to-end)
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).
This commit is contained in:
@@ -6,7 +6,31 @@ 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.0** — IR op `inline_asm` (lock; no behavior change). Added `inline_asm:
|
||||
**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
|
||||
@@ -88,40 +112,40 @@ guards fire: corrupting the `.ir` → IR mismatch; deleting it → the require-f
|
||||
`src/corpus_run.test.zig`, `examples/1639-*`.
|
||||
|
||||
## Current state
|
||||
Phase A underway: `asm { … }` lexes (A.0) and **parses** into `AsmExpr` (A.1);
|
||||
lowering bails LOUD + named (no IR op / emit yet). Result-type derivation, the
|
||||
operand auto-naming rule, and the validation checklist are **Phase B** (not yet
|
||||
implemented — any asm reaching lowering errors out). The adopted **operand
|
||||
auto-naming rule** (design §II.5, decided this session): name auto-derived from a
|
||||
`{reg}` pin; explicit `[name]` only when it differs or for register-class (`=r`)
|
||||
operands; echo form `[eax] "={eax}"` rejected. Parser stores `name: ?[]const u8`;
|
||||
the rule is a Phase-B (typing) concern, so the parser needs no change for it.
|
||||
**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 B–E feasibility already confirmed against the live tree
|
||||
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
|
||||
**C.1 + D together** (must land as one green step) — wire `lowerAsmExpr` to BUILD
|
||||
the `inline_asm` op (intern template + constraints + clobber names; resolve each
|
||||
operand's effective name via the §II.5 auto-naming rule; lower input `Ref`s;
|
||||
compute the result `TypeId` from the `out_value` operands — 0→void, 1→T, N→tuple,
|
||||
named) AND implement `emitInlineAsm` in `src/ir/emit_llvm.zig` (replacing the
|
||||
`@panic` tripwire) — the port of Zig's `airAssembly`: assemble the LLVM constraint
|
||||
string (outputs `=`/`+`, inputs, `clobbers`→`~{name}`), rewrite `%[name]`→`${N}` /
|
||||
`%%` / `%=`, `LLVMGetInlineAsm` + `LLVMBuildCall2`, AT&T dialect. They land
|
||||
together because the moment lowering stops bailing, emit is reached — a half-step
|
||||
would hit the tripwire. First target: the single-value-output syscall on
|
||||
`x86_64-linux` (ir-only via a `.build` `{ "target": "x86_64-linux" }` + `.ir`
|
||||
snapshot, since the host is aarch64). Result-type derivation for `expr_typer.zig`
|
||||
(`inferType` `.asm_expr` arm) also lands here — now observable. Then E (multi-
|
||||
return tuples) + remaining validation (`%[name]` references a real operand). See
|
||||
`PLAN-ASM.md` Phases C–E + design §II.6.
|
||||
**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.
|
||||
@@ -151,6 +175,12 @@ return tuples) + remaining validation (`%[name]` references a real operand). See
|
||||
- (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
|
||||
|
||||
Reference in New Issue
Block a user