# 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).
