Two new workstreams:
- ASM: inline assembly — asm { "tmpl", "=r" -> T, "r" = expr, clobbers(.…) },
multi-return tuples; lowers via the existing llvm_api.c (no shim).
- FFI-linkage: add extern/export postfix keywords, migrate every #foreign onto
them, then purge 'foreign' from the tree (end-state invariant).
Drop current/ from .gitignore so plans + checkpoints are tracked normally
(the dir was ignored; only checkpoints had been force-added). Includes
docs/inline-asm-design.md. specs.md change left uncommitted.
5.5 KiB
sx Inline Assembly — Implementation Plan (ASM stream)
Design source of truth: docs/inline-asm-design.md. This plan turns that doc's §II.7 stage-map + §II.8 phasing into ordered, commit-sized, testable steps. Read the design doc first — this file is the how/when, not the what/why.
Surface (decided):
asm volatile { "template", "=r" -> T, "r" = expr, clobbers(.cc, .memory) }
— brace block; -> output / = input; clobbers(.…) dot-name list; N -> Type
outputs return a tuple; templates are pure AT&T (via LLVM).
Feasibility (confirmed): sx links LLVM@19; src/llvm_api.zig @cImports
llvm-c/Core.h, so llvm_api.c.* already exposes LLVMGetInlineAsm (9-arg),
LLVMInlineAsmDialectATT, LLVMBuildCall2, LLVMAppendModuleInlineAsm. No shim.
Relationship to other streams:
- Phases A–E (the inline-asm expression) are independent of EXTERN-EXPORT.
- Phase F (global asm) consumes
extern/exportto import/expose asm symbols — do it afterPLAN-EXTERN-EXPORT.mdPhase 2.
Cadence (IMPASSIBLE)
No commit may both add a test AND make it pass. Each feature step is either a
behavior-locking PASSING test, or an xfail test the next commit turns green.
Arch-pinned tests live in examples/16xx-platform-asm-* (must declare target=).
Never regenerate snapshots while red.
Phase A — keyword + AST + parser (parses; no codegen)
| Step | Commit | What | Files |
|---|---|---|---|
| A.0 | lock | add kw_asm keyword + map entry; unit lex test asm → kw_asm |
src/token.zig, src/lexer.zig + .test.zig |
| A.1 | xfail | parse asm { … } → AsmExpr/AsmOperand in parsePrimary; pin an AST/sx ir parse snapshot; lowering still bailDetail("inline asm codegen unimplemented") |
src/ast.zig (:85 union arm, :721 structs), src/parser.zig (parsePrimary), src/ir/interp.zig |
| A.2 | green | parse-shape snapshot lands green; the unimplemented bail is loud + named | — |
Phase B — sema / typing
| Step | Commit | What | Files |
|---|---|---|---|
| B.0 | xfail | result-type rule (0→void / 1→T / N→named-or-positional tuple) + checklist (no-output⇒volatile, layout, comptime-string template) — pin error messages |
src/ir/expr_typer.zig |
| B.1 | green | typing + diagnostics implemented; .unresolved sentinel on failure (no silent default) |
src/ir/expr_typer.zig, src/ir/semantic_diagnostics.zig |
Phase C — IR op + lowering
| Step | Commit | What | Files |
|---|---|---|---|
| C.0 | lock | add inline_asm: InlineAsm to Op + AsmOperand (role/name/constraint/operand) + interp bailDetail arm; unit tests for the IR shape |
src/ir/inst.zig (:80), src/ir/interp.zig |
| C.1 | xfail→green | lowerAsmExpr in lowerExpr dispatch — interns template/constraints/clobber-names, lowers input Refs, sets result TypeId |
src/ir/lower/expr.zig |
Phase D — LLVM emit (single value-output; the core)
| Step | Commit | What | Files |
|---|---|---|---|
| D.0 | xfail | examples/16xx-platform-asm-syscall-write.sx + …-register-read.sx + …-no-output-volatile.sx + …-missing-volatile.sx (expected compile error) — all red |
examples + expected/ markers |
| D.1 | green | emitInlineAsm: port FuncGen.airAssembly — constraint-string assembler (outputs =/+, inputs, clobbers(.name)→~{name}), %[name]→${N} / %% / %= template rewriter, LLVMGetInlineAsm+LLVMBuildCall2, sideeffect=volatile, AT&T dialect |
src/ir/emit_llvm.zig (emitInst dispatch + handler) |
| D.2 | green | lock the template-rewrite + constraint string via an expected/*.ir snapshot on …-template-subst.sx |
examples |
Phase D verification: zig build test; the syscall example runs on
x86_64-linux; IR snapshot matches the design doc's worked sys_write lowering.
Phase E — multi-return tuples + clobbers(.…)
| Step | Commit | What | Files |
|---|---|---|---|
| E.0 | xfail | …-asm-multi-return.sx (divmod→(quot,rem), cpuid→4-tuple) red |
examples |
| E.1 | green | N out_value → LLVM struct return + extractvalue i → sx tuple (named when operands named); clobbers(.name) dot-name lowering finalized |
src/ir/emit_llvm.zig, src/ir/lower/expr.zig |
Phase F — global asm (needs EXTERN-EXPORT Phase 2)
| Step | Commit | What | Files |
|---|---|---|---|
| F.0 | xfail | top-level asm { … } decl parsed (reject operands/volatile); …-asm-global.sx (defines a symbol, imported via extern) red |
src/parser.zig, src/ast.zig |
| F.1 | green | lower asm_global → c.LLVMAppendModuleInlineAsm; comptime-call guard (dlsym-miss is loud); blocks concatenate in source order |
src/ir/lower/decl.zig, src/ir/emit_llvm.zig, src/ir/interp.zig |
Phase G — later (own steps when scheduled)
-> @place write-through + read-write ("+r" -> @place) + indirect-memory
("=*m") outputs · %= unique-id · output-to-const rejection · Intel-dialect
opt-in · naked functions (callconv(.naked), coordinate with EXTERN-EXPORT).
Open decisions (design doc §II.10)
Dialect (AT&T-only v1, recommended) · volatile contextual-keyword (recommended)
· brace separator comma (recommended) · clobbers(.name) dot-name sugar now →
checked per-arch Clobber enum later (Phase 4 of the design doc).
End-to-end verification (per phase)
zig build && zig build test; for arch-pinned examples confirm they run on a
matching host or assert on sx ir/.s snapshots. After intentional output
changes only: zig build test -Dupdate-goldens, then review the diff.