Introduce the welded comptime `compiler` library (`#library "compiler"` +
`abi(.zig) extern compiler`), per design/comptime-compiler-api.md, and unify
`callconv(...)` into the new `abi(...)` annotation.
abi(...) replaces callconv(...):
- New ABI enum { default, c, zig, pure }; `abi(.c|.zig|.pure)` parses in the
postfix slot before extern/export (and standalone). `kw_callconv` -> `kw_abi`.
- Migrated 52 sx files, the call-convention-mismatch diagnostic, and docs
(readme/specs) from `callconv(.c)` to `abi(.c)`.
Phase 1 — welded compiler library (parse -> registry -> validation -> bridge):
- `abi(.zig) extern compiler` parses on fn decls (carries abi/extern_lib) and
struct decls (StructDecl.abi/extern_lib).
- `#library "compiler"` is the comptime-only internal surface — never dlopen'd.
- src/ir/compiler_lib.zig: the binding registry (the safety boundary). `Field`
welded to StructInfo.Field with layout baked from the real Zig type
(@offsetOf/@sizeOf); `findType`/`findFn`. Welded structs are layout-validated
at registration (field set + total size) as a header checked against the impl.
- Host-call bridge: a `fn abi(.zig) extern compiler` dispatches under the
comptime interp to its registered Zig handler (intern/text_of round-trip),
never dlsym. IR Function.compiler_welded; validated in declareFunction.
- Comptime-only enforcement: a runtime call to a welded fn is a clean
build-gating error (emitCall), not an undefined-symbol link failure.
Phase 2.1 — byte-layout weld foundation:
- Decision: full byte-layout weld (sx struct laid out byte-identically to the
bound Zig type). Registered StructInfo (first non-natural / Zig-reordered
layout). `computeWeldPlan` — pure offset-ordered element plan + padding +
sx-field->LLVM-element remap; unit-tested. Emit/interp wiring is the next
sub-step (2.2+, see current/CHECKPOINT-COMPILER-API.md).
Examples: 0625/0626 (welded struct + fn round-trip), 1183/1184/1185
(layout-mismatch, unexported-fn, runtime-call diagnostics).
Constructed-type companion to examples/1178 (source form): a declare/
define enum whose variant references itself BY VALUE is rejected by the
same checkInfiniteSize guard ('infinitely sized'). Pins the use-before-
define corner of the validation story — by-value self-reference is the
one self-ref shape that isn't legal; *L (pointer) is fine (see 0618).
No compiler change (locks existing behavior).
make_enum from dirs[0..2] — mints Axis from a comptime SUBSLICE of a
local EnumVariant array. Locks the interp subslice-over-non-string-
aggregate fix (d22037c); previously bailed.
make_enum(name, variants: []EnumVariant) -> Type mints a nominal enum
from a variant list passed as a VALUE, not a hardcoded literal — the
open-ended form the channel-result constructors are special cases of.
Pure sx over declare/define; no compiler machinery.
Because variants is an ordinary comptime value, a non-generic builder
can ASSEMBLE it in a local before minting. examples/0620: build_level
fills a local array, then make_enum mints Level from it — exercising
define decoding a value-arg SLICE (decodeVariantElements' slice branch),
vs. the inline .[ … ] array the 0614-0618 examples pass directly.
No compiler change (locks existing capability). Suite green (678).
Move the issue 0140 repro into the feature suite as a regression test.
Asserts the build-gating diagnostic 'comptime type construction failed:
comptime define(): enum has no variants' at the construction site, exit
1 — locking out the prior 'unresolved type reached LLVM emission' panic.
type_info reflects an enum / tagged-union INTO a TypeInfo value — the
inverse of define's decode — so define(declare(n), type_info(T)) mints
a byte-identical copy with NO literal variant list.
- inst.zig: new BuiltinId.type_info (comptime-only, like declare/define).
- lower/call.zig: replace the 'not yet implemented' bail. Resolve $T at
lower time, reject non-enum/non-tagged-union loudly with a good span,
emit callBuiltin(.type_info, [const_type], TypeInfo).
- interp.zig: reflectTypeInfo builds the exact nested-aggregate Value
defineEnum decodes — variant {name,payload}, slice {data,len}, EnumInfo
{variants}, TypeInfo {tag0, EnumInfo}. tagged_union reflects field.ty
(tagless already void); payloadless `enum` reflects void per variant.
- emit: unchanged — type_info is always comptime-evaluated, the existing
comptime-only else arm (shared with declare/define) never fires.
0619 turns green: a source enum (circle:f64 / rect:i64 / empty) reflected
and reconstructed, constructs and matches like the original.
type_info($T) is still unimplemented, so the round-trip
define(declare("ShapeCopy"), type_info(Shape)) bails with
"type_info is not yet implemented" plus the downstream
enum-inference cascade. Snapshot pins that current behavior;
the next commit implements type_info and turns this green.
A nominal aggregate that contains itself (or a mutual peer) BY VALUE has no
finite layout and infinite-recursed typeSizeBytes into a stack overflow —
for SOURCE enums/structs as well as comptime-constructed types.
New `checkInfiniteSize` pass (lower/decl.zig, Pass 1g — after type
registration, before body lowering): walks the by-VALUE containment graph
(pointer/slice/optional payloads break the cycle, so `*Self` stays valid);
on a back-edge it emits a loud diagnostic — "type 'X' is infinitely sized
(it contains itself by value); use a pointer ('*X') to break the cycle" —
and poisons the offending field to `.unresolved` so sizing can't recurse
before the build halts on the error. Covers source + declare/define types,
direct + mutual recursion.
examples/1178 locks the diagnostic; issue 0139 marked RESOLVED. This also
completes METATYPE PLAN F5's by-value-self-reference rejection. Full suite
green (675).
examples/0618 mints a recursive `List` enum (`cons: *List; nil`) via
declare("List")/define, builds a 3-node list, matches the pointer payload
directly and via deref, and counts it recursively. Locks the self-reference
capability. Full suite green (674).
declare now takes the type's NAME — `declare(name) -> Type` — because the
compiler needs it at compile time to register the forward type, which is
what makes self-reference resolve. EnumInfo drops `name` (it lives on
declare now); define completes the handle's body in place (the slot is
already named).
Self-reference mechanism (evalComptimeType): before lowering a comptime
type expression, preregisterForwardTypes scans it (and a called ctor fn's
body) for `declare("Name")` calls and registers each as an empty forward
nominal type AND binds it as a type alias. The alias is essential: a
`Name :: ctor()` decl makes `Name` a const_decl author, so a `*Name`
self-reference resolves through the forward-ALIAS path
(type_aliases_by_source), which a bare findByName registration doesn't
satisfy. With both in place `*Name` resolves to the forward slot at lower
time; the interp's declare returns that same slot; define fills it.
List :: make_list();
make_list :: () -> Type {
h := declare("List");
return define(h, .enum(.{ variants = .[
EnumVariant.{ name = "cons", payload = *List },
EnumVariant.{ name = "nil", payload = void } ] }));
}
Verified: cons/nil construct + match (direct and through the pointer),
multi-node list traversal via a recursive `count(*List)`. meta.sx
RecvResult/TryResult + examples 0614/0615/0617 updated to declare(name);
full suite green (673).
The compiler concept is declare/define (comptime type construction); the
old "reify" framing is gone from the entire repo.
- Rename: PLAN-REIFY → PLAN-METATYPE, CHECKPOINT-REIFY → CHECKPOINT-METATYPE,
PLAN-POST-REIFY → PLAN-POST-METATYPE (both rewritten around declare/define);
examples 0614/0615/0617 → comptime-metatype-* (+ their expected/ triplets),
headers rewritten.
- Scrub reify from design/execution-evolution-roadmap.md (§7 step 3 contracts,
§8.1, §9 decisions, §10 gates) → declare/define / comptime type construction.
- core.sx prelude pointer + parser.test.zig surface lock updated to the
declare/define builtins (define(handle, info) -> Type; EnumInfo.name).
No behavior change; renamed examples match their renamed snapshots. Full
suite green (673), all unit tests pass. Zero `reify` tokens remain in
src/docs/sx/examples.
Per the directive to strip reify entirely: the sx `reify(info)` one-shot is
removed. `define(handle, info)` now RETURNS the (completed) handle, so the
one-shot constructor chains as a single expression:
T :: define(declare(), .enum(.{ name = "T", variants = ... }));
- meta.sx: drop reify; RecvResult/TryResult use `define(declare(), …)`.
- interp .define returns the handle type_tag (was void); call.zig lowers it
with `Type` result and sets the info arg's target type to TypeInfo so the
intercepted call still infers the `.enum(…)` literal.
- returnExprMintsType: a type-fn body that returns `define(…)` (or a bodied
non-generic Type-returning sx helper) is comptime-evaluated.
- examples 0614 (direct) + 0615 (type-fn) use `define(declare(), …)`.
Full suite green (673). Files/docs still carry the old reify naming — the
rename sweep is the next commit.
Second slice of the re-architecture — the compiler now has ZERO type-
construction code beyond declare/define.
- instantiateTypeFunction: a type-fn body returning a computed Type (a call
to a non-generic, bodied, Type-returning fn) is comptime-evaluated with the
type bindings active, then renamed to the mangled instantiation name for
identity (renameNominalType). Replaces the old reify-call pattern-matching.
- DELETED: reifyType (lower/nominal.zig), findReturnReifyCall (lower/generic.zig),
and the stale inline-position reify gate in resolveTypeCallWithBindings.
- evalComptimeType (was evalComptimeTypeNamed): pure eval, no rename; the
type-fn caller renames explicitly. renameReifiedType → renameNominalType.
- The TYPE NAME now travels in the data: EnumInfo gains `name`, and define()
names the slot from it (the compiler derives no name from a binding LHS).
examples/0614/0615 carry `name = "..."`; RecvResult/TryResult set it too.
- field_type stays a reflection #builtin (reads a type); only construction
moved out. All reify mentions stripped from compiler source.
examples 0614/0615/0617 run on the floor. Full suite green (673).
REIFY Phase 3.1. Add RecvResult($T) and TryResult($T) to meta.sx as
type-fns over reify (value-or-closed; value-or-empty-or-closed). They
need NO new compiler machinery — reify-of-a-literal in a type-fn body is
exactly the Phase 1 path — so the channel result types are pure sx
library code. examples/0617 green (both construct + match, incl.
payload-less .closed / .empty). Suite green (673 examples, 447 unit).
make_enum(variants) (3.2) and type_info (2.2) remain — both blocked on a
generalized reify reader (reifyType currently AST-walks a literal
TypeInfo). Plan/checkpoint updated.
REIFY Phase 3.0. Add examples/0617 using RecvResult(i64) / TryResult(i64)
(construct + match, plus payload-less .closed / .empty). Seed an empty
expected/*.exit marker. RED by design — the type-fns aren't defined yet
("unresolved RecvResult"); Phase 3.1 adds them to meta.sx as type-fns
over reify and turns this green.
REIFY Phase 2.1. fieldTypeOf (lower/generic.zig, re-exported on Lowering)
returns the i-th member type of T: struct field / tagged-union + union
variant payload (.void for a tagless variant) / tuple element / array +
vector element. Out-of-range and memberless types poison to .unresolved
with a loud diagnostic (never a silent default). Wired into
resolveTypeCallWithBindings (replacing the Phase-2 bail); since it folds
to a TypeId at lower time it composes inside type_eq / type_name / any
type-arg slot.
examples/0616 green: struct fields (name via field_name + type via
field_type), type_eq fold, tagged-union payloads incl. quit -> void.
Suite green (672 examples, 447 unit).
type_info($T) -> TypeInfo (reflect into a value, inverse of reify) is
NOT done — still bails loudly; it's the larger Phase 2.2 step (widen the
TypeInfo data model + comptime value construction). Plan/checkpoint updated.
REIFY Phase 2.0. Add examples/0616: reflect a struct's fields (name via
field_name, type via field_type) and a tagged-union's variant payloads,
including field_type composed inside type_eq / type_name. Seed an empty
expected/*.exit marker. RED by design — field_type still bails ("not yet
implemented"); Phase 2.1 implements it over the type table and turns
this green.
REIFY Phase 1.1 (Phase 1 complete). instantiateTypeFunction detects a
type-fn body that returns reify(...) (findReturnReifyCall) and routes it
to reifyType under the instantiation's name — mangled for inline use,
the alias name for `Foo :: Box(i64)` — with the type-arg bindings active
so reify payloads (`payload = T`) resolve against the instantiation args.
Placed before the general case, whose resolveTypeWithBindings would
route the reify call to the inline-position loud bail.
Registering under the mangled name lets the top-of-instantiation cache
return the SAME TypeId on a second instantiation, so Box(i64) resolved
at two independent sites is ONE type (Contract 1). examples/0615 green
(build()->consume() cross-site + `b : Box(i64) = .none`). Suite green
(671 examples, 447 unit).
REIFY Phase 1.0. Add examples/0615: a type-fn `Box :: ($T)->Type {
return reify(...) }` used at two independent sites (a return type and a
parameter type); consume(build()) typechecks only if both sites resolve
to ONE TypeId. RED by design — reify in a type-fn body still bails
("only supported in a :: binding"). Phase 1.1 routes a reify-returning
type-fn body through reifyType under the mangled instantiation name so
identity holds, turning this green.
REIFY Phase 0.2 (Phase 0 complete). Lowering.reifyType (lower/nominal.zig)
reads the flat-enum TypeInfo literal off the AST, synthesizes an
ast.EnumDecl, and feeds it through the SAME type_bridge.buildEnumInfo
path source enums use — so the minted type is byte-identical to a
hand-written `enum { value: i64; closed; }` and flows through enum
codegen (layout / construct / match) UNMODIFIED (Contract 2).
Wired at the `E :: reify(...)` const-decl hook in lower/decl.zig
(replacing the Phase-0.0 loud bail). Unsupported argument shapes bail
loudly via reifyBail — never a silent default. The generic.zig inline
reify path now reports it's only supported in a `::` binding (Phase 0).
examples/0614 green: reify a {value: i64, closed} enum, construct
.value(3) and .closed, match both -> "value 3" / "closed". Full suite
green (670 examples, 447 unit).
After a leading `.` (enum literal `.enum`, field access `x.enum` /
`E.struct`, match arm `case .enum:`) a reserved keyword is unambiguously
the member/variant NAME — the dot rules out the keyword reading — so no
backtick escape is needed. A declaration of such a variant still needs
the backtick (enum { `enum: i64 }), since the decl site has no dot.
Adds Parser.dotMemberName() (identifier OR identifier-shaped keyword)
and routes the leading-dot enum-literal and postfix field-access sites
through it. readme updated. The reify example 0614 now uses the cleaner
reify(.enum(...)) spelling (still xfail — reify lands next commit).
REIFY Phase 0.1. Add the end-to-end Phase-0 example: reify a flat enum
(value: i64, closed) from a TypeInfo literal, construct E.value(3) /
E.closed, and match both arms. Seed an empty expected/*.exit marker.
RED by design (reify still bails -> "unparseable expected exit"); the
next commit (0.2) implements reify and turns it green. Satisfies the
no-commit-both-adds-a-test-and-passes cadence.
Drive a bundled `zig` as `zig cc` for the AOT link step, supplying lld + CRT
+ libc (musl/glibc/mingw) so `sx build` produces native binaries with no host
toolchain. Default Linux output is static musl (portable-anywhere).
- src/zig_backend.zig: discover zig ($SX_ZIG / bundled-next-to-exe / PATH);
bundled-vs-PATH provenance gates auto-activation.
- src/target.zig: selectZigLinker + emitZigLinkArgv + zigTargetTriple, dispatched
before the per-OS branches; macOS/Linux/Windows in scope.
- src/ir/emit_llvm.zig: LLVMNormalizeTargetTriple so vendor-less zig triples
(e.g. x86_64-windows-gnu) parse to the correct OS/object format (COFF not ELF).
- src/main.zig: --self-contained / --no-self-contained; linux-musl, linux-musl-arm,
windows-gnu shorthands; de-vendor linux/linux-arm to match the corpus runner.
- examples/1660: Windows Win32 print-42 + exit(0) via kernel32 (ir-only off-Windows).
Auto-activates only for a bundled zig; a PATH-only zig engages under
--self-contained, so native dev/CI builds are never silently rerouted.
Docs: readme Cross-Compilation, design/bundled-zig-link-backend-design.md, current/PLAN-DIST.md.
A `%[name]` that references a symbol ("s") operand without an explicit
modifier now lowers to `${N:c}` (LLVM 'bare constant — no punctuation')
instead of `${N}`. This makes `bl %[fn]` / `call %[fn]` portable across
targets with no per-arch knowledge: x86 would otherwise render `$cb`
(an invalid call target, requiring a hand-written `:P`); aarch64 is
unaffected. Verified `:c` is equivalent to `:P` for x86-64 calls (both
emit R_X86_64_PLT32), and correct for branch targets, RIP-relative
addressing, and `$`-prefixed absolute immediates.
renderAsmTemplate injects `:c` only for symbol operands lacking an
explicit modifier (asmNamedIsSymbol helper); an explicit `%[name:X]`
still wins (escape hatch). x86 example 1659 drops its `:P` for the same
plain `%[fn]` as aarch64 1656. Snapshots regen to `${N:c}`. zig build
test green (668 corpus, 446 unit).
Adds ir-only x86_64 examples mirroring the aarch64 feature examples, so
each emit path is locked on both arches:
- 1657 read-write `+` → "incq ${0}", "=r,0" (tied input)
- 1658 indirect `=*m` → "movq $$42, ${0}", "=*m"(ptr elementtype i64)
- 1659 symbol `"s"` → "call ${2:P}", direct call to an exported sx fn
Each is x86-pinned (ir-only on this aarch64 host — the .ir is the
assertion; runs on x86_64-linux, main returns 0 on success / 1 if the
asm misbehaved). x86 templates validated by cross-emitting an object
(LLVM's integrated assembler accepts them; objdump confirms 1659 is a
direct `call` reloc to cb). Note: x86 direct calls need the `P` operand
modifier (`%[fn:P]`); aarch64 `bl %[fn]` needs none. Pure additive
locks, no compiler change. zig build test green (668 corpus, 446 unit).
A `"s"` input operand feeds a function/global symbol; the template's
%[name] emits the platform-mangled name, so `bl %[fn]` / `call %[fn]`
branches DIRECTLY to it (PC-relative, no register load — one fewer
indirection than register-indirect `blr`).
Lowering: an `"s"` input lowers its RHS normally (a function name →
`ptr @fn`); the rejection added last commit is removed. Emit: a symbol
operand is passed with its OWN llvm type (LLVMTypeOf) and no coercion —
the function value is a `ptr`, and the old coerce-to-register-int path
mistyped it and failed the verifier. New asmIsSymbol helper.
Verified on aarch64: examples/1656 (sx → asm → bl _cb → sx → 42); the
emitted asm is a direct `bl <_cb>` (objdump-confirmed), IR constraint
`...,s,...`(ptr @cb). Flipped 1656 from the rejection lock to a runnable
aarch64 example. zig build test green (665 corpus, 446 unit).
A symbol operand (constraint "s") feeds a function/global symbol whose
mangled name the template emits — enabling a DIRECT `bl %[fn]` (one
fewer indirection than register-indirect `blr`). Until now `"s" = fn`
fell through to emit and produced an LLVM-verifier crash (param type
mismatch). Reject it at lowering with a clear diagnostic instead, and
lock that with examples/1656-platform-asm-symbol-operand.sx. The next
commit implements it and flips the example to run (→ 42).
Adds examples/1655-platform-asm-callback-into-sx.sx: a global-asm
trampoline (_caller) that `bl _cb` back into an `export`ed sx function.
Demonstrates the sx → asm → sx round trip and that `export` (external
linkage + stable C symbol + C ABI) is what makes the callback symbol
resolvable — `callconv(.c)` alone leaves it internal and it DCE's away.
Runs under the JIT on aarch64-macos (→ 42); ir-only elsewhere. Locks
current behavior; no compiler change.
Adds examples/1654-platform-asm-global-comptime-call.sx — the comptime
guard. A module-asm symbol only exists after assemble+link; the comptime
interpreter resolves extern calls via host dlsym, where it's absent, so
`#run my_add(…)` fails with a clear diagnostic ("comptime extern call:
symbol not found via dlsym") rather than misfiring. Runtime calls work
(1648/1653). dlsym-miss precedes asm assembly, so arch-independent — no
.build. Locks current behavior; no compiler change.
Adds examples/1653-platform-asm-global-jit.sx — a module-scope asm { … }
block executed via `sx run` (no `aot`). sx run compiles the module to an
in-memory object (the integrated assembler assembles the `module asm`
into it), then ORC relocates and runs it, so a module-asm symbol IS
resolvable at JIT main execution — the long-assumed "AOT only" limit was
stale. Sibling of 1648 (same feature via AOT). Locks current behavior
(exit 42); no compiler change.
Implements indirect-memory (`=*m`) `-> @place` outputs — the last
substantive asm feature. Unlike a write-through `=` output (which
returns a value that is then stored), an indirect output passes the
place ADDRESS to the asm and the asm writes through it; there is no
return slot.
emitInlineAsm:
- indirect outputs are excluded from the LLVM return type;
- their pointer is passed as an opaque `ptr` call arg, placed FIRST
(the arg-consuming constraint order is: output-section indirect
pointers, then inputs, then read-write tied seeds);
- each indirect arg gets an `elementtype(T)` call-site attribute
(required in the opaque-pointer era), T = the pointee type;
- the store-back loop skips indirect outputs (already written).
New asmIsIndirect helper. Lowering stops rejecting `*` (constraint kept
verbatim; `=*m` reaches the constraint string as-is). asmOperandIndex
is unchanged — indirect outputs still count as operands, so `%[name]`
${N} numbering holds.
Verified by running on aarch64: store-through-pointer (str x9, %[out]
→ 42, IR `=*m,~{x9}` with `ptr elementtype(i64)`) and a mixed case
(indirect + value output + input → `=*m,=r,r`, indirect ptr arg first,
${0}/${1}/${2} correct). 1652 flipped from the rejection lock to a
runnable aarch64 example (ir-only elsewhere). zig build test green
(661 corpus, 446 unit).
Adds examples/1652-platform-asm-indirect-mem.sx exercising a `=*m -> @x`
indirect-memory place output. Currently rejected loudly at lowering
("not yet implemented"); this locks that behavior as a passing test.
The next commit implements indirect-memory outputs and flips this
example to run end-to-end (store-through-pointer → 42).
Adds examples/1651-platform-asm-x86-syscall-write.sx — the canonical
inline-asm use case: `write(2)` via a raw x86_64 `syscall` (SYS_write
in rax, fd/buf/count pinned to rdi/rsi/rdx, rcx+r11+memory clobbered,
byte count returned in rax). Exercises register-pinned inputs, a pinned
value output, a pointer input (*u8 -> rsi), and clobbers(.…) lowering
together.
x86-pinned via .build { "target": "x86_64-linux" }: ir-only on this
aarch64 host (the .ir snapshot locks the exact constraint string
`={rax},{rax},{rdi},{rsi},{rdx},~{rcx},~{r11},~{memory}` — the §II.11
silent-miscompile risk zone), runs natively on x86_64-linux printing
"ok\n" (hand-authored .stdout, asserted only in execute mode).
Pure additive test coverage — no compiler change (lock commit).
zig build test green (660 corpus, 446 unit).
A scalar `::` constant folds to its value and has no storage. The
unary `.address_of` lowering (src/ir/lower/expr.zig) skipped the
alloca path (is_alloca == false) and resolveGlobalRef (scalar consts
get no storage global), falling through to the generic addr_of arm,
which reinterpreted the folded value as a pointer:
`inttoptr (i64 <value> to ptr)`. That wild pointer segfaulted on
deref and emitted invalid stores for inline-asm `-> @const`.
Diagnose instead, in the address_of(identifier) path: a non-alloca,
non-ref-capture, non-pack-elem scope binding (local scalar const) and
a module_const_map name not backed by storage (module scalar const)
both report "cannot take the address of constant '<name>' — a scalar
'::' constant has no storage …" and return a placeholder Ref. Chose
diagnose over materializing read-only storage (consistent with the
fold-only scalar model). Array/struct consts keep real storage and
stay addressable (@K/@LIT unchanged).
Also gives the ASM stream's planned output-to-const rejection for
free — asm `-> @const` lowers through the same path. Regression:
examples/1177-diagnostics-addr-of-const-rejected.sx. Resolves 0138.
Implements read-write (`+r` / `+{reg}`) `-> @place` outputs. LLVM has
no `+` constraint, so a read-write place lowers to:
- an output `=` constraint (return slot, stored back through the
place after the call), with the leading `+` rewritten to `=`; plus
- a TIED input constraint (the decimal index of that output) appended
after the regular inputs, seeded with the place's loaded value
passed as a call arg.
Tied inputs are appended last so existing operand indices (%[name] ->
${N}) are undisturbed; asmOperandIndex stays correct. Lowering no longer
rejects `+` (indirect `*` still rejected). emitInlineAsm grows the
arg/param arrays by the rw count, loads each seed, and emits the tied
constraint.
Verified by running: increment-in-place (41 -> 42) and a mixed case
(rw place + regular input + value output) producing the textbook
"=r,=r,r,0" constraint with correct ${N} indices. 1650 flipped from
the rejection lock to a runnable aarch64-pinned example (ir-only
elsewhere). zig build test green (658 corpus, 446 unit).
Adds examples/1650-platform-asm-rw-place.sx exercising a `+r -> @x`
read-write place output. Currently rejected loudly at lowering
("not yet implemented"); this locks that behavior as a passing test.
The next commit implements read-write outputs and flips this example
to run end-to-end (increment-in-place → 42).
An asm result can be STORED through a place (a local / struct field) instead of
returned; the place output does not join the result tuple.
- parser.zig: `-> @place` parses `@place` as an ordinary address-of expression
→ an out_place operand (the in-function form; reuses the existing `@` prefix).
- inst.zig: AsmOperand gains out_ty (the output slot's value type) so emit can
build the combined return struct without re-deriving from Inst.ty.
- lower/expr.zig: out_place operand = the lowered @place address, out_ty = the
pointee. Read-write (`+`) and indirect-memory (`*`) constraints rejected loudly
(not yet implemented) rather than miscompiled.
- ops.zig emitInlineAsm: the LLVM return type is built from ALL outputs
(out_value + out_place); after the call, out_place slots are stored through
their address and out_value slots rebuild the sx result. Fast path when there
are no place outputs (the struct return IS the result — pure-value asm IR
unchanged).
Verified: write-to-local (42), struct field, mixed value+place (v=10 b=20), `+`
rejected. Locked with 1649-platform-asm-place-output (mixed, runs on aarch64).
zig build test green (657 corpus, 446 unit).
A top-level `asm { "tmpl", };` block (template only) lowers to LLVM `module asm`;
a lib-less `extern` declaration calls into the symbols it defines (the import
direction reuses the existing C-FFI extern path — no new surface).
- ast.zig: asm_global node (AsmGlobal { template }).
- parser.zig: parseAsmGlobal, dispatched from parseTopLevel on kw_asm — rejects
`volatile` and any operands/clobbers (template only). The in-function asm
expression form stays in parsePrimary.
- module.zig: Module.global_asm list; lower/decl.zig captures each template in
lowerMainAndComptime (the real top-level pass — lowerDecls is dead for
top-level); emit_llvm.zig emit() appends each via LLVMAppendModuleInlineAsm in
source order.
- the new node forced asm_global arms in sema.zig (analyzeNode +
findNodeAtOffset) and semantic_diagnostics.zig (checkBindingNames).
Verified end-to-end: an aarch64 `_my_add` global routine, called via `extern`,
returns 42 — AOT only (the ORC JIT doesn't link module-asm symbols; global-asm
symbols live in the final linked binary). Locked with 1648-platform-asm-global
({ "aot": true, "target": "macos" } → AOT build+run on aarch64, ir-only else).
zig build test green (656 corpus, 446 unit).
Replaces the N>1 "Phase E" bail with a shared asmResultType helper (lowering +
inferType) that derives the result type from the out_value operands: 0→void,
1→T, N→a named tuple (fields named via the §II.5 effective-name rule).
Key realization: toLLVMType(tuple) already produces a literal struct {T1,…,Tn} —
exactly what LLVM's multi-output inline asm returns — so emit needs NO change.
Building the op with a tuple result type makes the asm call return the struct,
which IS sx's tuple value (destructured by the normal tuple_get path).
inferType's .asm_expr arm now also delegates to asmResultType (single owner), so
`return asm`, `x := asm`, and `q, r := asm` all agree on the type.
Verified end-to-end on aarch64: split(0x1234)→(lo=52,hi=18), a udiv/msub
divmod→(3,2). IR: `call { i64, i64 } asm "divq ${4}",
"={rax},={rdx},{rax},{rdx},r,~{cc}"(…)` → extractvalue → tuple.
1640 → the x86_64 multi-output IR lock (ir-only); 1647 → a multi-output example
that runs on aarch64.
zig build test green (655 corpus, 446 unit).
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).
Extends lowerAsmExpr with a pinnedRegister(constraint) helper and two §II.5
operand-naming checks, in the compile path before the codegen bail:
- reject the echo form `[eax] "={eax}"` — a label identical to the register its
own constraint pins is redundant (the operand is already auto-named after the
register); the useful form is a label that differs (`[quot] "={rax}"`);
- reject duplicate operand names (ambiguous %[name] / result field).
Locked with 1643-platform-asm-echo-name and 1644-platform-asm-duplicate-name.
zig build test green (652 corpus, 445 unit).
Restructures the .asm_expr lowering arm into lowerAsmExpr, which validates the
asm shape with specific named diagnostics BEFORE the not-yet-implemented codegen
bail, so the user sees the real problem first. Two checklist items enforced:
- template must be a compile-time-known string ("..." or #string), not a
runtime expression;
- an asm with no value outputs must be `volatile` (else its effects could be
deleted) — mirrors Zig's rule.
Valid shapes still bail with the "codegen not yet implemented" message. Result-
type derivation + the operand auto-naming rule stay deferred to Phase C, where a
real IR op makes the result type observable/testable.
Locked with 1641-platform-asm-missing-volatile (the volatile error) and
1642-platform-asm-nop-volatile (no-output + volatile accepted → codegen bail).
zig build test green (650 corpus, 445 unit).
`asm volatile? { "tmpl", [name]? "constraint" (-> Type | = expr), …,
clobbers(.…) }` now parses into a flat-operand AsmExpr/AsmOperand (ast.zig +
parser.zig parseAsmExpr, dispatched from parsePrimary on .kw_asm). `volatile`
and `clobbers` are recognized contextually (not reserved). `-> @place`
write-through is rejected with a clear "Phase 2" parse error.
Codegen is not implemented yet (IR op + LLVM emit are Phases C–E), so lowering
bails LOUD + named via an explicit .asm_expr arm in lower/expr.zig (not the
generic unknown_expr else) — emitPlaceholder makes hasErrors() abort the build
on the message.
The new asm_expr tag forced (and got) arms in three exhaustive Node.Data
switches: sema.zig analyzeNode + findNodeAtOffset, semantic_diagnostics.zig
checkBindingNames — each recurses into template + operand payloads.
Design: adopted the operand auto-naming rule (design §II.5) — name auto-derived
from a {reg} pin, explicit [name] only when it differs or for register-class
operands, echo form rejected. Typing-stage rule; parser stores name: ?[]const u8.
Locked with examples/1640-platform-asm-parse.sx (multi-output divmod: named
operands, register pins, clobbers — parses then bails, called from main).
Also files issue 0137 (pre-existing, orthogonal: `sx run` with no `main`
segfaults via an unguarded JIT entry lookup in target.zig — not an asm bug).
zig build test green (648 corpus, 445 unit).
When a `.build` target doesn't match the host, the runner can't execute the
example here, so it verifies via `sx ir --target` only: asserts exit + the `.ir`
snapshot (stdout) + diagnostics (stderr), never `.stdout`. An `.ir` snapshot is
REQUIRED in ir-only mode — its absence is a loud failure, never a silent pass.
- corpus_run.test.zig: ir_only flag (target set & !hostMatchesTarget); first
dispatch arm runs `sx ir`, sets act_exit/act_err/act_ir; skip stdout in both
update and verify modes; require ir_raw.
- lock fixture 1639-platform-target-cross (asm-free main, target x86_64-linux,
checked-in .ir). Verified: corrupt .ir => IR mismatch; delete .ir => require
failure.
Test-infra only; no compiler code. zig build test green (647 corpus, 444 unit).