fix: xx pack[i] to a protocol target heap-copies the element
Erasing a single comptime-pack element to a protocol value (`xx sources[0]` with a protocol target) tripped the pack-as-value error: buildProtocolErasure treated the index_expr as an lvalue and took its address via lowerExprAsPtr, whose .index_expr arm lowers the bare pack as a value (a pack is comptime-only with no runtime storage). isLvalueExpr now reports a comptime pack index as an rvalue, decided via the same packArgNodeAt predicate the value path uses — so the value and lvalue paths can't diverge on what counts as a pack element — and erasure heap-copies the already-materialized element instead. Resolves issue 0135. Regression tests: examples/0547, 0548.
This commit is contained in:
27
examples/0547-packs-xx-pack-index-to-protocol.sx
Normal file
27
examples/0547-packs-xx-pack-index-to-protocol.sx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// `xx <pack>[i]` erased to a protocol-typed local.
|
||||||
|
//
|
||||||
|
// Erasing a single comptime-pack element to a protocol scalar routes through
|
||||||
|
// buildProtocolErasure. A pack index is a comptime rvalue (a pack has no
|
||||||
|
// runtime storage — `sources[i]` resolves to the call-site arg, which only
|
||||||
|
// gets storage when lowered as a value), so the erasure must heap-copy the
|
||||||
|
// materialized element rather than take its address.
|
||||||
|
//
|
||||||
|
// Regression (issue 0135): `xx sources[0]` used to lower the bare pack as a
|
||||||
|
// value and error with "pack 'sources' has no runtime value".
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
VL :: protocol(T: Type) { get :: () -> T; }
|
||||||
|
IntCell :: struct { v: i64; }
|
||||||
|
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
|
||||||
|
|
||||||
|
first :: (..sources: VL) -> i64 {
|
||||||
|
x : VL(i64) = xx sources[0]; // erase element 0 to VL(i64)
|
||||||
|
return x.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> i32 {
|
||||||
|
print("{}\n", first(IntCell.{ v = 7 })); // 7
|
||||||
|
print("{}\n", first(IntCell.{ v = 42 }, IntCell.{ v = 99 })); // 42 (element 0)
|
||||||
|
0
|
||||||
|
}
|
||||||
25
examples/0548-packs-xx-pack-index-two-elements.sx
Normal file
25
examples/0548-packs-xx-pack-index-two-elements.sx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// Erase two DISTINCT comptime-pack elements to protocol locals — each gets
|
||||||
|
// its own heap copy and resolves to its OWN concrete type's method (IntCell.get
|
||||||
|
// vs Doubler.get), proving the per-element erasure picks the right vtable.
|
||||||
|
//
|
||||||
|
// Regression (issue 0135): single-element `xx pack[i]` erasure to a protocol
|
||||||
|
// scalar was unsupported (the bare pack lowered as a value and errored).
|
||||||
|
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
VL :: protocol(T: Type) { get :: () -> T; }
|
||||||
|
IntCell :: struct { v: i64; }
|
||||||
|
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
|
||||||
|
Doubler :: struct { n: i64; }
|
||||||
|
impl VL(i64) for Doubler { get :: (self: *Doubler) -> i64 => self.n * 2; }
|
||||||
|
|
||||||
|
sum_two :: (..sources: VL) -> i64 {
|
||||||
|
a : VL(i64) = xx sources[0]; // erase element 0
|
||||||
|
b : VL(i64) = xx sources[1]; // erase element 1
|
||||||
|
return a.get() + b.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> i32 {
|
||||||
|
print("{}\n", sum_two(IntCell.{ v = 10 }, Doubler.{ n = 16 })); // 10 + (16*2) = 42
|
||||||
|
0
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
7
|
||||||
|
42
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
42
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# 0135 — `xx <pack>[i]` to a protocol target lowers the pack as a value ("pack has no runtime value")
|
||||||
|
|
||||||
|
> **RESOLVED (2026-06-13).** Root cause: `buildProtocolErasure`
|
||||||
|
> (`src/ir/lower/coerce.zig`) treated `pack[i]` as an lvalue (any `index_expr`
|
||||||
|
> returned true from `isLvalueExpr`) and tried to take its address via
|
||||||
|
> `lowerExprAsPtr`, whose `.index_expr` arm lowers the bare pack as a value →
|
||||||
|
> the pack-as-value error. Fix (preferred option 1): `isLvalueExpr` now reports
|
||||||
|
> a comptime pack index as an **rvalue**, so erasure falls into its heap-copy
|
||||||
|
> branch and copies the already-materialized element. It decides pack-ness with
|
||||||
|
> the SAME predicate the value path uses — `packArgNodeAt` (the `pack_arg_nodes`
|
||||||
|
> map) — not `isPackName` (`pack_param_count`), since the comptime-call path
|
||||||
|
> installs only `pack_arg_nodes`; sharing one predicate keeps the value and
|
||||||
|
> lvalue paths from diverging on what counts as a pack element. Regression tests:
|
||||||
|
> [examples/0547-packs-xx-pack-index-to-protocol.sx](../examples/0547-packs-xx-pack-index-to-protocol.sx)
|
||||||
|
> (single element) and
|
||||||
|
> [examples/0548-packs-xx-pack-index-two-elements.sx](../examples/0548-packs-xx-pack-index-two-elements.sx)
|
||||||
|
> (two distinct concrete types, each resolving to its own vtable). This also
|
||||||
|
> unblocked [issue 0133](0133-union-member-struct-literal-assign-unresolved-panic.md),
|
||||||
|
> now landed. The standalone repro `.sx` was removed (superseded by 0547).
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
One-line: erasing a single comptime-pack element to a protocol value —
|
||||||
|
`xx sources[0]` where the target type is a protocol (`VL(i64)`) — spuriously
|
||||||
|
errors with **"pack 'sources' has no runtime value — a pack is comptime-only
|
||||||
|
and can't be used as a value here"**, pointing at the pack name.
|
||||||
|
|
||||||
|
- **Observed:** the pack-as-value diagnostic fires on `sources` in
|
||||||
|
`x : VL(i64) = xx sources[0];`, even though `sources[0]` is a valid
|
||||||
|
compile-time pack index.
|
||||||
|
- **Expected:** `sources[0]` resolves to the call-site arg (the concrete
|
||||||
|
`IntCell`), gets erased to the protocol `VL(i64)`, and the program prints
|
||||||
|
`7`, exit 0.
|
||||||
|
|
||||||
|
This is **PRE-EXISTING** and reproduces on clean `master`, independent of any
|
||||||
|
union / tuple / issue-0133 work. Issue 0053 added the `xx <whole-pack>` →
|
||||||
|
`[]Any`/`[]P` slice bridge (`lowerPackToSlice`), but **single-element**
|
||||||
|
`xx pack[i]` erasure to a protocol scalar was never handled.
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
Minimal, standalone (only `modules/std.sx`):
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
VL :: protocol(T: Type) { get :: () -> T; }
|
||||||
|
IntCell :: struct { v: i64; }
|
||||||
|
impl VL(i64) for IntCell { get :: (self: *IntCell) -> i64 => self.v; }
|
||||||
|
|
||||||
|
make :: (..sources: VL) -> i64 {
|
||||||
|
x : VL(i64) = xx sources[0]; // protocol local <- xx pack[0]
|
||||||
|
return x.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> i32 {
|
||||||
|
print("{}\n", make(IntCell.{ v = 7 })); // 7
|
||||||
|
0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Run: `./zig-out/bin/sx run issues/0135-xx-pack-index-protocol-erasure-lowers-pack-as-value.sx`
|
||||||
|
→ errors today; the fix should make it print `7`, exit 0.
|
||||||
|
|
||||||
|
### Root cause (traced)
|
||||||
|
|
||||||
|
The error chain (from a stack trace at the diagnostic site):
|
||||||
|
|
||||||
|
```
|
||||||
|
lowerAssignment / lowerVarDecl sets target_type = VL(i64) for the RHS
|
||||||
|
lowerExpr(xx sources[0]) unary_op .xx
|
||||||
|
operand = lowerExpr(sources[0]) → packArgNodeAt resolves to the
|
||||||
|
call-site arg IntCell.{v=7} (OK)
|
||||||
|
lowerXX(operand, sources[0]) classifies .erase_protocol
|
||||||
|
buildProtocolErasure(..., operand_node = sources[0], dst = VL(i64))
|
||||||
|
isLvalueExpr(sources[0]) == true (it's an index_expr)
|
||||||
|
concrete_ptr = lowerExprAsPtr(sources[0]) ← HERE
|
||||||
|
lowerExprAsPtr .index_expr arm: lowerExpr(ie.object)
|
||||||
|
lowerExpr(sources) → bare pack name → diagPackAsValue ✗
|
||||||
|
```
|
||||||
|
|
||||||
|
The defect: **`lowerExprAsPtr`'s `.index_expr` arm does NOT perform the
|
||||||
|
pack-arg-node substitution that `lowerIndexExpr` does.** `lowerIndexExpr`
|
||||||
|
intercepts `<pack>[<comptime-int>]` via `packArgNodeAt` and lowers the
|
||||||
|
call-site arg node directly; `lowerExprAsPtr` skips straight to
|
||||||
|
`lowerExpr(ie.object)`, lowering the bare pack `sources` as a value — which is
|
||||||
|
the (correct) pack-as-value error for a context where there is genuinely no
|
||||||
|
pointer to take.
|
||||||
|
|
||||||
|
So the value path (`lowerIndexExpr`) handles a pack index but the
|
||||||
|
address-of path (`lowerExprAsPtr`) does not — a two-resolver divergence on
|
||||||
|
the pack-index case. `buildProtocolErasure` only reaches the address-of path
|
||||||
|
because `isLvalueExpr(sources[0])` returns `true` (any index_expr looks like
|
||||||
|
an lvalue), so it tries to alias the operand's storage instead of heap-copying
|
||||||
|
the already-materialized rvalue.
|
||||||
|
|
||||||
|
## Investigation prompt
|
||||||
|
|
||||||
|
> `xx <pack>[i]` erased to a protocol target spuriously errors with "pack
|
||||||
|
> '<name>' has no runtime value". Repro:
|
||||||
|
> `issues/0135-xx-pack-index-protocol-erasure-lowers-pack-as-value.sx`
|
||||||
|
> (errors today; the fix should make it print `7`, exit 0).
|
||||||
|
>
|
||||||
|
> Root cause: `buildProtocolErasure` (`src/ir/lower/coerce.zig`, ~line 389)
|
||||||
|
> sees `isLvalueExpr(operand_node) == true` for the index_expr `sources[0]`
|
||||||
|
> and takes the alias-the-storage branch:
|
||||||
|
> `concrete_ptr = self.lowerExprAsPtr(operand_node)`. But
|
||||||
|
> `lowerExprAsPtr`'s `.index_expr` arm (`src/ir/lower/stmt.zig`, the
|
||||||
|
> `.index_expr => |ie|` case, `self.lowerExpr(ie.object)` at stmt.zig:1005
|
||||||
|
> on clean master) does
|
||||||
|
> NOT do the pack-arg-node substitution that `lowerIndexExpr`
|
||||||
|
> (`src/ir/lower/expr.zig:1343`, via `packArgNodeAt`) performs. It lowers the
|
||||||
|
> bare pack `sources` as a value → `diagPackAsValue` (the pack-as-value
|
||||||
|
> error at `src/ir/lower/expr.zig:1722`).
|
||||||
|
>
|
||||||
|
> A comptime pack index has no addressable storage of its own — `sources[0]`
|
||||||
|
> is the call-site arg node, which only acquires storage when lowered as a
|
||||||
|
> value. So the address-of path is the wrong path for a pack index.
|
||||||
|
>
|
||||||
|
> Suspected fix (pick the one that keeps the value/address paths from
|
||||||
|
> diverging — the recurring two-resolver defect class in this codebase):
|
||||||
|
> 1. **Preferred — teach `isLvalueExpr` that a comptime pack index is NOT
|
||||||
|
> an lvalue.** Add a check: an `index_expr` whose object is a pack name
|
||||||
|
> (`isPackName(ie.object...)` / a `packArgNodeAt(ie) != null` hit) is an
|
||||||
|
> rvalue. Then `buildProtocolErasure` falls into its existing
|
||||||
|
> `else { heap_copy = true; alloca + store(operand) }` branch and erases
|
||||||
|
> the already-materialized `IntCell` value correctly. Smallest, and
|
||||||
|
> matches the semantic truth (a pack element is a comptime rvalue).
|
||||||
|
> 2. Alternatively, make `lowerExprAsPtr`'s `.index_expr` arm resolve a
|
||||||
|
> pack index the way `lowerIndexExpr` does (`packArgNodeAt` → lower the
|
||||||
|
> arg node as a value → `addr_of` an alloca holding it). More plumbing,
|
||||||
|
> and it manufactures storage the caller could already manufacture.
|
||||||
|
> Do NOT paper over with a silent default — per CLAUDE.md, resolve the real
|
||||||
|
> path or emit a diagnostic.
|
||||||
|
>
|
||||||
|
> Verification: the repro prints `7`, exit 0; then `zig build &&
|
||||||
|
> zig build test` green. Add positive coverage (a new
|
||||||
|
> `examples/05xx-packs-xx-pack-index-to-protocol.sx`, packs category) and a
|
||||||
|
> sibling that erases two distinct pack elements. When resolved, this also
|
||||||
|
> UNBLOCKS issue 0133 (see below) — re-apply the 0133 unified-resolver fix
|
||||||
|
> and confirm `examples/0540-packs-pack-type-arg-spread.sx` stays green.
|
||||||
|
|
||||||
|
## Relationship to issue 0133
|
||||||
|
|
||||||
|
Surfaced while fixing **issue 0133** (assigning a struct literal to a union
|
||||||
|
member panics — the RHS never gets its target type). The clean 0133 fix
|
||||||
|
(per its own investigation prompt) unifies the lvalue field resolver so the
|
||||||
|
**target-type path** and the **lvalue-pointer path** share one matcher
|
||||||
|
(`fieldLvalueResolve`), which then resolves *tuple element* LHS types too
|
||||||
|
(not just structs). That makes `c.sources.0 = xx sources[0]` in
|
||||||
|
`examples/0540-packs-pack-type-arg-spread.sx` set `target_type = VL(i64)`
|
||||||
|
for the RHS — which routes `xx sources[0]` through `buildProtocolErasure`
|
||||||
|
(it previously erased later, at the store, via `coerceToType` on the
|
||||||
|
already-materialized value). That is exactly this bug.
|
||||||
|
|
||||||
|
So **issue 0133's correct (unified-resolver) fix is BLOCKED on this issue.**
|
||||||
|
The ready-to-apply 0133 patch is recorded in
|
||||||
|
`issues/0133-union-member-struct-literal-assign-unresolved-panic.md`; after
|
||||||
|
0135 lands, re-apply it and confirm both the 0133 union repro (`code=9`) and
|
||||||
|
`examples/0540` stay green.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Diagnostic site (symptom): `src/ir/lower/expr.zig:1723` (`isPackName` at
|
||||||
|
1722 → `diagPackAsValue`, `.generic`) via `src/ir/lower/expr.zig:1386`
|
||||||
|
(`lowerExpr(ie.object)` in `lowerIndexExpr`) — reached here through
|
||||||
|
`lowerExprAsPtr`'s `.index_expr` arm, NOT `lowerIndexExpr`.
|
||||||
|
- Root area (cause): `buildProtocolErasure` (`src/ir/lower/coerce.zig:389-390`)
|
||||||
|
+ `lowerExprAsPtr` `.index_expr` arm (`src/ir/lower/stmt.zig:1005`) +
|
||||||
|
`isLvalueExpr`.
|
||||||
|
- Prior art (RESOLVED, not duplicates): 0052 (slice-of-protocol variadic
|
||||||
|
erasure), 0053 (`xx <whole-pack>` → slice bridge), 0054 (generic-struct →
|
||||||
|
param protocol erasure). None handle single-element `xx pack[i]` → protocol
|
||||||
|
scalar.
|
||||||
@@ -333,9 +333,26 @@ pub fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Nod
|
|||||||
/// `xx <struct-typed expr>` to decide between borrow (lvalue → take the
|
/// `xx <struct-typed expr>` to decide between borrow (lvalue → take the
|
||||||
/// address) and heap-copy (rvalue → allocate a fresh copy).
|
/// address) and heap-copy (rvalue → allocate a fresh copy).
|
||||||
pub fn isLvalueExpr(self: *Lowering, node: *const Node) bool {
|
pub fn isLvalueExpr(self: *Lowering, node: *const Node) bool {
|
||||||
_ = self;
|
|
||||||
return switch (node.data) {
|
return switch (node.data) {
|
||||||
.identifier, .field_access, .index_expr, .deref_expr => true,
|
.identifier, .field_access, .deref_expr => true,
|
||||||
|
// A comptime pack index (`pack[i]`) is NOT an lvalue: a pack is
|
||||||
|
// comptime-only with no runtime storage — `pack[i]` resolves to the
|
||||||
|
// call-site arg node, which only acquires storage when lowered as a
|
||||||
|
// value. Taking its address via `lowerExprAsPtr` would lower the bare
|
||||||
|
// pack as a value and trip the pack-as-value error (issue 0135).
|
||||||
|
// Reporting it as an rvalue routes `buildProtocolErasure` into its
|
||||||
|
// heap-copy branch, which copies the already-materialized element.
|
||||||
|
// A non-pack index (array/slice element) is a genuine lvalue.
|
||||||
|
//
|
||||||
|
// Decide pack-ness with the SAME predicate the value path uses —
|
||||||
|
// `packArgNodeAt` (the `pack_arg_nodes` substitution map) — NOT
|
||||||
|
// `isPackName` (the `pack_param_count` map). The two maps are set
|
||||||
|
// together in the pack-fn path but the comptime-call path
|
||||||
|
// (comptime.zig) installs only `pack_arg_nodes`; using `isPackName`
|
||||||
|
// there would disagree with the value substitution and mis-route the
|
||||||
|
// erasure. Sharing one predicate keeps the value/lvalue paths from
|
||||||
|
// diverging on what counts as a pack element.
|
||||||
|
.index_expr => self.packArgNodeAt(&node.data.index_expr) == null,
|
||||||
else => false,
|
else => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user