fix(ir): comptime print of an Any-held Type no longer silently stops [F0.12]

`any_to_string` runs `type := type_of(val)`; for an `.any` operand
`type_of` lowers to `struct_get(val, 0)` to read the Any's tag. At
runtime a first-class Type value is the aggregate `{ tag=.any, value=tid }`
so the read succeeds, but the comptime interpreter stores a Type as a bare
`.type_tag(tid)` and the comptime `struct_get` arm had no case for it — it
raised `CannotEvalComptime`, which `runComptimeSideEffects` swallowed into
`void_val`, truncating the `#run` while still building with exit 0.

- interp.zig: comptime `struct_get` handles a `.type_tag(tid)` base by
  mirroring the runtime Any-Type layout (field 0 -> `.any` tag, field 1 ->
  the type id), so `type_of` of an Any-held Type evaluates as it does at
  runtime and execution continues.
- emit_llvm.zig: `runComptimeSideEffects` no longer swallows a side-effect
  bail; it prints a loud diagnostic and sets `comptime_failed`
  (-> error.ComptimeError, non-zero exit), matching the const-init path.
  A truncated `#run` can no longer ship a successful build.

Regression: examples/0613-comptime-print-any-type.sx (all five lines print,
exit 0). Resolves issue 0096.
This commit is contained in:
agra
2026-06-05 20:48:49 +03:00
parent 794f2ef94a
commit b0d85a858c
7 changed files with 147 additions and 1 deletions

View File

@@ -0,0 +1,79 @@
# issue 0096 — `#run`/comptime print of an `Any` holding a `Type` silently stops
> **RESOLVED** (F0.12).
> **Root cause:** `any_to_string` runs `type := type_of(val)`; for an `.any`
> operand `type_of` lowers to `struct_get(val, 0)` (read the Any's tag field).
> At runtime a first-class `Type` value is the aggregate `{ tag=.any, value=tid }`,
> so the read succeeds. The comptime interpreter stores a `Type` as a bare
> `.type_tag(tid)` Value, and the comptime `struct_get` arm had no case for
> `.type_tag` — it fell through to `typeErrorDetail("…base has no fields…")` and
> raised `CannotEvalComptime`. That error was then swallowed silently:
> `runComptimeSideEffects` ran `interp.call(...) catch Value.void_val`, so the
> `#run` truncated mid-execution yet the build still exited 0.
> **Fix:** (1) `src/ir/interp.zig` — the comptime `struct_get` arm now handles a
> `.type_tag(tid)` base by mirroring the runtime Any-Type layout: field 0 →
> `.int(TypeId.any.index())` (the `.any` tag), field 1 → `.type_tag(tid)`. So
> `type_of` of an Any-held Type evaluates the same as runtime and execution
> continues. (2) `src/ir/emit_llvm.zig` — `runComptimeSideEffects` no longer
> swallows a side-effect bail into `void_val`; it prints a loud diagnostic and
> sets `comptime_failed` (→ `error.ComptimeError`, non-zero exit), matching the
> const-init path. A truncated `#run` can no longer ship a successful build.
> **Regression test:** `examples/0613-comptime-print-any-type.sx` (all five
> lines print, exit 0). Verified fail-before / pass-after.
## Symptom
During `#run`/comptime execution, `print("{}", at)` where `at : Any` holds a
`Type` value **silently halts** the comptime interpreter: the formatted value
and every following statement are omitted, yet **the build still succeeds
(exit 0)**. At runtime the same `Any`-held `Type` prints fine (`u64`). A
successful build with truncated `#run` execution is the dangerous part — a
silent stop, the exact class of failure the project's REJECTED-PATTERNS rule
forbids.
## Reproduction (only imports `modules/std.sx`)
```sx
#import "modules/std.sx";
ct_probe :: () {
print("before\n");
x : u64 = 1;
t : Type = type_of(x);
at : Any = t;
print("name={}\n", type_name(at));
print("unsigned={}\n", type_is_unsigned(at));
print("value={}\n", at);
print("after\n");
}
#run ct_probe();
main :: () {}
```
**Observed pre-fix** (comptime stops after `unsigned=true`, build still exit 0):
```text
before
name=u64
unsigned=true
--- build done ---
```
**Expected / post-fix** (same as runtime, execution continues):
```text
before
name=u64
unsigned=true
value=u64
after
--- build done ---
```
## Bisect (ground-truth)
Pre-existing: the minimal repro stops on `dist-foundation` too (and pre-F0.8),
so it is NOT introduced by F0.8's Any-tag fix and is orthogonal to issue 0090.
A standing comptime-interpreter limitation, scheduled and fixed as F0.12.