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.
5.9 KiB
0138 — @const (address-of a :: comptime constant) yields a wild pointer
Status: RESOLVED
Resolution. Root cause was in
src/ir/lower/expr.zig's unary.address_oflowering: a scalar::constant binds a folded value (is_alloca == false, no storage), so it skipped both the alloca path andresolveGlobalRef, then fell through to the genericaddr_ofarm which reinterpreted the value as a pointer (inttoptr (i64 <value> to ptr)). Fixed by diagnosing in theaddress_of(identifier)path — both the lexical case (a non-alloca, non-ref-capture, non-pack-elem scope binding) and the module case (a name inmodule_const_mapthatresolveGlobalRefdid not back with storage) now emit "cannot take the address of constant '' — a scalar '::' constant has no storage …" and return a placeholder Ref. Chose diagnose over materializing read-only storage (confirmed with the user): consistent with the fold-only scalar model; array/struct consts keep their real storage and stay addressable (@K/@LITviaglobal_addr, unchanged). This also gives the ASM stream's planned "output-to-constrejection" for free — asm-> @constlowers@placethrough the same path, so it now reports the clean diagnostic instead of an LLVM verifier failure. Regression:examples/1177-diagnostics-addr-of-const-rejected.sx(module const, local const, and asm-> @constwrite-through).zig build testgreen (659 corpus).
Symptom
Taking the address of a ::-bound comptime constant (@x where x :: 40)
does not produce a real address. The address-of lowering falls through to
the generic addr_of arm, which takes the folded constant value and
reinterprets it as a pointer:
store ptr inttoptr (i64 40 to ptr), ptr %alloca, align 8
- Observed:
@xof a const lowers tointtoptr (i64 <value> to ptr)— a pointer whose numeric address IS the constant's value. Dereferencing it segfaults (@xofx :: 40→ wild pointer0x28). Using it as a store destination (e.g. inline-asm-> @xwrite-through) emits invalid IR that only the LLVM verifier catches:Store operand must be a pointer / store i64 %asm, i64 40. - Expected: either a clean compile diagnostic ("cannot take the address of
comptime constant
x") or materialization of read-only backing storage so the address is real. Never a silent reinterpret-value-as-pointer (a textbook silent-miscompile per CLAUDE.md).
This is not inline-asm-specific — it was discovered while implementing the
ASM stream's planned "output-to-const rejection for -> @place", but the
root cause is in the general address-of path. The same -> @place-to-const
rejection falls out for free once @const is handled correctly (asm lowers
@place through the same address-of path).
Reproduction
Segfault on deref (no inline asm needed, no project deps):
main :: () -> i64 {
x :: 40; // comptime constant — no runtime storage
p := @x; // lowers to `inttoptr (i64 40 to ptr)` — wild pointer
return p.*; // segfault (deref of 0x28)
}
The IR for just p := @x (no deref) shows the defect directly:
main :: () -> i64 {
x :: 40;
p := @x;
return 7;
}
→
%alloca = alloca ptr, align 8
store ptr inttoptr (i64 40 to ptr), ptr %alloca, align 8 ; <-- bug
ret i32 7
Inline-asm write-through to a const (the path that surfaced it) — invalid IR caught by the verifier instead of a sx diagnostic:
FORTY :: 40;
main :: () -> i64 {
asm volatile { "mov %[c], #99", [c] "=r" -> @FORTY };
return FORTY;
}
→ LLVM verification failed: Store operand must be a pointer.
Investigation prompt
@const(address-of a::-bound comptime constant) miscompiles: instead of a real address it reinterprets the constant's value as a pointer (inttoptr (i64 <value> to ptr)), segfaulting on deref and producing invalid stores for inline-asm-> @placewrite-through to a const.Suspected area:
src/ir/lower/expr.zig, the unary.address_oflowering. The cleanaddress_of(identifier)path (~line 1994) only handlesbinding.is_allocalocals and globals (resolveGlobalRef). A::const is neither, so it falls through to the generic.address_ofarm (~line 2057), which doesaddr_of(self.lowerExpr(uop.operand))— andlowerExprof a const identifier folds to the constant value, soaddr_ofof an i64 constant emitsinttoptr.Fix likely needs to: detect, in the
address_of(identifier)path, that the resolved binding is a comptime constant with no storage. Then either (a) emit a clear diagnostic viaself.diagnostics.addFmt(.err, span, "cannot take the address of comptime constant{s}", .{name})and return a dedicated sentinel (NOT a folded value) — matches CLAUDE.md's no-silent-default rule; or (b) materialize a read-only global/alloca for the const and return its real address. Decide which againstspecs.md(does sx intend::consts to be addressable at all?). Coordinate with PLAN-CONST-AGG's "const-write rejection" — a write through@const(asm-> @place, or a futurep.* = …) must also be rejected; the read-only-storage option (b) still needs the write rejected.Verification: run the three repros above. Expect: repro 1 (
return p.*) either fails to compile with the diagnostic, or returns 40 (if::consts become addressable); repro 3 (asm-> @FORTY) reports a clean sx diagnostic, NOT an LLVM verifier failure. Add a pinned regression underissues/expected/(or migrate toexamples/once the behavior is decided).
Notes
- Discovered mid-ASM-stream while starting the planned output-to-
constrejection step. Read-write+place outputs (the prior ASM step) shipped green before this surfaced. - Not covered by any existing issue or by
current/PLAN-CONST-AGG.md(which addresses const writes via assignment, not address-of).