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.
This commit is contained in:
agra
2026-06-16 06:29:36 +03:00
parent c760b92548
commit 2a954ceeb6
6 changed files with 81 additions and 1 deletions

View File

@@ -0,0 +1,18 @@
// Taking the address of a scalar `::` constant is a compile error: a scalar
// constant folds to its value and has NO storage (only array/struct constants
// are immutable globals with a real address — see 0177). Covers a module-scope
// const, a local const, and an inline-asm `-> @const` write-through (the path
// that surfaced the bug). Before the fix, `@N` lowered to `inttoptr (i64 40 to
// ptr)` — a wild pointer that segfaulted on deref and emitted invalid stores
// for asm `-> @const`. Regression (issue 0138).
takes :: (p: *i64) {}
N :: 40;
main :: () {
takes(@N); // module scalar const — no storage
x :: 7;
takes(@x); // local scalar const — no storage
asm volatile { "mov %[c], #99", [c] "=r" -> @N }; // write-through to a const
}

View File

@@ -0,0 +1 @@
1

View File

@@ -0,0 +1,17 @@
error: cannot take the address of constant 'N' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)
--> examples/1177-diagnostics-addr-of-const-rejected.sx:14:11
|
14 | takes(@N); // module scalar const — no storage
| ^^
error: cannot take the address of constant 'x' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)
--> examples/1177-diagnostics-addr-of-const-rejected.sx:16:11
|
16 | takes(@x); // local scalar const — no storage
| ^^
error: cannot take the address of constant 'N' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)
--> examples/1177-diagnostics-addr-of-const-rejected.sx:17:49
|
17 | asm volatile { "mov %[c], #99", [c] "=r" -> @N }; // write-through to a const
| ^^

View File

@@ -1,6 +1,25 @@
# 0138 — `@const` (address-of a `::` comptime constant) yields a wild pointer # 0138 — `@const` (address-of a `::` comptime constant) yields a wild pointer
**Status:** OPEN **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 ## Symptom

View File

@@ -2000,6 +2000,21 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
const ptr_ty = self.module.types.ptrTo(binding.ty); const ptr_ty = self.module.types.ptrTo(binding.ty);
break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty); break :blk self.builder.emit(.{ .addr_of = .{ .operand = binding.ref } }, ptr_ty);
} }
// A non-storage value binding — a scalar `::` constant
// folded to its value (`is_alloca == false`, not a
// ref-capture pointer or pack-element alias). It has NO
// address: array/struct consts get real storage (reached
// via `resolveGlobalRef` below), but a scalar const does
// not. Taking `@const` would otherwise fall through to the
// generic `addr_of` arm and reinterpret the folded value
// as a pointer — `inttoptr (i64 <value> to ptr)`, a wild
// pointer that segfaults on deref and emits invalid stores
// for asm `-> @const` (issue 0138). Diagnose loudly.
if (!binding.is_ref_capture and binding.pack_elem == null) {
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "cannot take the address of constant '{s}' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)", .{id_name});
break :blk self.emitPlaceholder("addr_of_const");
}
} }
} }
// address_of(global) → emit global_addr (pointer to global, not load) // address_of(global) → emit global_addr (pointer to global, not load)
@@ -2007,6 +2022,15 @@ pub fn lowerExpr(self: *Lowering, node: *const Node) Ref {
const ptr_ty = self.module.types.ptrTo(gi.ty); const ptr_ty = self.module.types.ptrTo(gi.ty);
break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty); break :blk self.builder.emit(.{ .global_addr = gi.id }, ptr_ty);
} }
// A module-scope scalar `::` constant (not in lexical `scope`, and
// not a storage-backed array/struct const — those resolve above).
// Same defect as the local case: without storage, `@FORTY` would
// become `inttoptr (i64 <value> to ptr)`. Diagnose (issue 0138).
if (self.program_index.module_const_map.get(id_name) != null) {
if (self.diagnostics) |d|
d.addFmt(.err, node.span, "cannot take the address of constant '{s}' — a scalar '::' constant has no storage (use a '=' variable or a local copy for mutable data)", .{id_name});
break :blk self.emitPlaceholder("addr_of_const");
}
} }
// Fold a negated integer literal into one constant: `-128` must // Fold a negated integer literal into one constant: `-128` must
// range-check as -128, not as an out-of-range +128 intermediate. // range-check as -128, not as an out-of-range +128 intermediate.