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

131 lines
5.9 KiB
Markdown

# 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 '<name>' — 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:
```llvm
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):
```sx
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:
```sx
main :: () -> i64 {
x :: 40;
p := @x;
return 7;
}
```
```llvm
%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:
```sx
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).