issues: file 0143 (pack-as-[]Type stride mismatch) + record out is end-state-only
0143: a ..$args pack forwarded as a []Type argument across a call is backed by a [N x Any] (16B) array but viewed as []type_value (8B) -> half-stride reads. A lowering bug the legacy Value model masks; the byte-accurate VM exposes it. Blocks examples/0114 on the VM. Filed per CLAUDE.md (not worked around; the type_name .unresolved guard only makes the VM decline rather than emit garbage). Checkpoint also records the sequencing insight: comptime `out` (print) can only land once the fallback is removed (a print-then-bail double-prints under the legacy re-run), so side-effecting ops + fallback-removal are the FINAL step; pure ops + migrations land first.
This commit is contained in:
@@ -373,6 +373,25 @@ when reached (sentinels or accessor fns; see the design doc Risks).
|
|||||||
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
|
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
|
||||||
|
|
||||||
## Log
|
## Log
|
||||||
|
- **Phase 4 burndown — switch_br + type_name ported; issue 0143 filed; KEY sequencing insight: `out` is end-state-only (2026-06-18).**
|
||||||
|
Ported two PURE comptime ops (`379ed05`): `switch_br` (i64-discriminant multi-way branch — enum/error
|
||||||
|
tag or `.type_value` index) and `type_name` (Type value / Any box → `table.typeName`, with an
|
||||||
|
`.unresolved`-bail guard). Correct in isolation; 0520–0524 run GREEN under strict. **Two blockers found:**
|
||||||
|
1. **issue 0143 (FILED, OPEN) — pack-as-`[]Type` stride mismatch.** A `..$args` pack forwarded as a
|
||||||
|
`[]Type` ARGUMENT across a call is backed by a `[N x Any]` (16B) array but viewed as `[]type_value`
|
||||||
|
(8B) → half-stride reads (`[i64 <unresolved> string]` vs legacy `[i64 string bool]`). A LOWERING bug
|
||||||
|
the legacy's Value model masks; the byte-accurate VM exposes it. Blocks `examples/0114` from running
|
||||||
|
HANDLED. **Per CLAUDE.md: filed, NOT worked around** (the `type_name` `.unresolved` guard just makes
|
||||||
|
the VM decline rather than emit garbage). Repro + fix-prompt in `issues/0143-…md`.
|
||||||
|
2. **`out` (comptime print) is an END-STATE op — it cannot land while the fallback exists.** Under the
|
||||||
|
legacy fallback, an eval that prints via `out` then BAILS double-prints (the VM wrote to fd 1, then
|
||||||
|
legacy re-runs the whole eval — no rewind). 0114 demonstrated it. So a direct-write `out` is only
|
||||||
|
safe once the fallback is GONE (strict-by-default). **Revised ordering:** land the PURE ops
|
||||||
|
(switch_br/type_name/type_is_unsigned/error_tag_name_get/global_addr/interp_print_frames) + the
|
||||||
|
BuildOptions migration + #insert + bundler FIRST; then in the FINAL step flip strict-to-default
|
||||||
|
(removing the fallback) AND add `out` together — at which point every `out`-using example flips
|
||||||
|
atomically with deletion. (Most of the gap-list examples print, so they stay on fallback until that
|
||||||
|
final flip — that's expected, not a regression.) 699/0 both default gates.
|
||||||
- **Phase 4 — STRICT no-fallback mode (the interp-retirement enumeration gate) + full gap list (2026-06-18).**
|
- **Phase 4 — STRICT no-fallback mode (the interp-retirement enumeration gate) + full gap list (2026-06-18).**
|
||||||
Added `-Dcomptime-flat-strict` / env `SX_COMPTIME_FLAT_STRICT` (implies `comptime_flat`): at all
|
Added `-Dcomptime-flat-strict` / env `SX_COMPTIME_FLAT_STRICT` (implies `comptime_flat`): at all
|
||||||
THREE comptime sites (type-fn in `lower/comptime.zig`, const-init + `#run` in `emit_llvm.zig`) a VM
|
THREE comptime sites (type-fn in `lower/comptime.zig`, const-init + `#run` in `emit_llvm.zig`) a VM
|
||||||
|
|||||||
86
issues/0143-pack-as-type-slice-stride-mismatch.md
Normal file
86
issues/0143-pack-as-type-slice-stride-mismatch.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# 0143 — A variadic pack passed as `[]Type` across a call is mis-strided (Any-sized backing, Type-sized view)
|
||||||
|
|
||||||
|
**Symptom** — When a variadic `..$args` pack is forwarded as a `[]Type` *argument*
|
||||||
|
to another function, reading `args[i]` yields the **wrong** element: the backing
|
||||||
|
array is laid out as `[N x Any]` (16-byte `{i64,i64}` slots) but the slice's
|
||||||
|
element type is `Type`/`type_value` (8 bytes), so an 8-byte-strided index read
|
||||||
|
lands on `elem0`, then the *padding* of `elem0` (reads as `.unresolved`), then
|
||||||
|
`elem1`, … — i.e. half-stride reads.
|
||||||
|
|
||||||
|
Observed (on the flat-memory comptime VM, which is byte-accurate) vs expected
|
||||||
|
(legacy interp, whose tagged-`Value` model is stride-agnostic and reads the right
|
||||||
|
logical element):
|
||||||
|
|
||||||
|
```
|
||||||
|
VM: [i64 <unresolved> string ]
|
||||||
|
legacy: [i64 string bool ]
|
||||||
|
```
|
||||||
|
|
||||||
|
The legacy interpreter masks the bug (it indexes a logical sequence of `Value`s,
|
||||||
|
not bytes). The VM exposes it. This is what makes `examples/0114-types-build-block-convert`
|
||||||
|
read `arg1: <unresolved>` instead of `arg1: string` once the VM handles `type_name`
|
||||||
|
(it currently bails on the `.unresolved` read — see the guard added in `comptime_vm.zig`
|
||||||
|
`type_name`, commit 379ed05 — so under the fallback it falls back; under
|
||||||
|
`SX_COMPTIME_FLAT_STRICT` it is a hard error).
|
||||||
|
|
||||||
|
## Reproduction
|
||||||
|
|
||||||
|
```sx
|
||||||
|
#import "modules/std.sx";
|
||||||
|
|
||||||
|
inner :: (args: []Type) -> string {
|
||||||
|
s := ""; i : i64 = 0;
|
||||||
|
while i < args.len { s = concat(s, type_name(args[i])); s = concat(s, " "); i = i + 1; }
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
outer :: (..$args) -> string { return inner($args); } // <-- pack passed as []Type across a call
|
||||||
|
|
||||||
|
R :: #run outer(42, "hi", true);
|
||||||
|
main :: () { print("[{}]\n", R); }
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `[i64 string bool ]`. Actual (VM): `[i64 <unresolved> string ]`.
|
||||||
|
|
||||||
|
Note the *direct* use (no cross-call) is fine — `walk :: (..$args) { list := $args; … type_name(list[i]) … }`
|
||||||
|
reads correctly. Only forwarding the pack as a `[]Type` **argument** mis-strides.
|
||||||
|
|
||||||
|
## Root cause (hypothesis)
|
||||||
|
|
||||||
|
The pack `$args` is materialised as a `[N x Any]` array (each arg boxed as a
|
||||||
|
16-byte `Any` — confirmed in the LLVM IR: `array_to_string__AR_3_Any` /
|
||||||
|
`[3 x { i64, i64 }]`), but when forwarded to a `[]Type` parameter the slice is
|
||||||
|
typed with element `type_value` (8 bytes). The slice `{ptr,len}` points at the
|
||||||
|
`[N x Any]` data, so `index_get` with an 8-byte element stride reads garbage.
|
||||||
|
|
||||||
|
The fix belongs in **lowering**, not the VM: a pack consumed as `[]Type` must
|
||||||
|
either (a) materialise a real `[N x Type]` (8-byte) array by extracting each
|
||||||
|
arg's type tag from its Any box, or (b) keep the slice element type as `Any` and
|
||||||
|
have `type_name`/reflection read the Any's tag (the VM's `type_name` already
|
||||||
|
handles an `.any` arg). Option (b) is likely smaller. Whichever: the slice's
|
||||||
|
element type and its backing array's element size MUST agree.
|
||||||
|
|
||||||
|
Suspected area: the pack-expansion / pack-as-slice lowering (search `src/ir/lower/`
|
||||||
|
for the `..$args` → slice materialisation and the `[]Type` coercion of a value
|
||||||
|
pack; `pack.zig`).
|
||||||
|
|
||||||
|
## Investigation prompt (paste into a fresh session)
|
||||||
|
|
||||||
|
> A variadic `..$args` pack forwarded as a `[]Type` argument across a call is
|
||||||
|
> mis-strided: the backing array is `[N x Any]` (16B slots) but the slice element
|
||||||
|
> type is `type_value` (8B), so `args[i]` reads at the wrong offset. The legacy
|
||||||
|
> interp masks it (Value-indexed); the byte-accurate comptime VM exposes it
|
||||||
|
> (`examples/0114`, and the minimal repro in `issues/0143-…md`). Fix it in
|
||||||
|
> lowering: make the pack→`[]Type` materialisation agree on element size — either
|
||||||
|
> build a real `[N x Type]` array (extract each Any's type tag) or type the slice
|
||||||
|
> `[]Any` and let reflection read the Any tag (`type_name` already handles `.any`).
|
||||||
|
> Verify: run the repro on the VM (`SX_COMPTIME_FLAT=1 sx run`) and expect
|
||||||
|
> `[i64 string bool ]`; then `examples/0114` must produce `arg1: string` (not
|
||||||
|
> `<unresolved>`) and run HANDLED under `SX_COMPTIME_FLAT_STRICT=1` matching legacy.
|
||||||
|
> Drop the `tid == .unresolved` guard in `comptime_vm.zig` `type_name` once the
|
||||||
|
> root cause is fixed (it was a stopgap so the VM declines rather than emits
|
||||||
|
> garbage).
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
OPEN. Blocks `examples/0114` from running HANDLED on the comptime VM (Phase 4
|
||||||
|
legacy-interp retirement). Not a blocker for the other comptime-op ports.
|
||||||
Reference in New Issue
Block a user