Files
sx/issues/0138-address-of-comptime-const-yields-wild-pointer.md
agra 2a954ceeb6 fix(0138): diagnose @scalar-const address-of (no storage)
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.
2026-06-16 06:29:36 +03:00

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_of lowering: a scalar :: constant binds a folded value (is_alloca == false, no storage), so it skipped both the alloca path and resolveGlobalRef, then fell through to the generic addr_of arm which reinterpreted the value as a pointer (inttoptr (i64 <value> to ptr)). Fixed by diagnosing in the address_of(identifier) path — both the lexical case (a non-alloca, non-ref-capture, non-pack-elem scope binding) and the module case (a name in module_const_map that resolveGlobalRef did 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/@LIT via global_addr, unchanged). This also gives the ASM stream's planned "output-to-const rejection" for free — asm -> @const lowers @place through 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 -> @const write-through). zig build test green (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: @x of a const lowers to inttoptr (i64 <value> to ptr) — a pointer whose numeric address IS the constant's value. Dereferencing it segfaults (@x of x :: 40 → wild pointer 0x28). Using it as a store destination (e.g. inline-asm -> @x write-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 -> @place write-through to a const.

Suspected area: src/ir/lower/expr.zig, the unary .address_of lowering. The clean address_of(identifier) path (~line 1994) only handles binding.is_alloca locals and globals (resolveGlobalRef). A :: const is neither, so it falls through to the generic .address_of arm (~line 2057), which does addr_of(self.lowerExpr(uop.operand)) — and lowerExpr of a const identifier folds to the constant value, so addr_of of an i64 constant emits inttoptr.

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 via self.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 against specs.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 future p.* = …) 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 under issues/expected/ (or migrate to examples/ once the behavior is decided).

Notes

  • Discovered mid-ASM-stream while starting the planned output-to-const rejection 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).