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.
131 lines
5.9 KiB
Markdown
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).
|