Files
sx/issues/0096-comptime-print-any-type-stops.md
agra b0d85a858c 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.
2026-06-05 20:48:49 +03:00

2.9 KiB

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

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

before
name=u64
unsigned=true
--- build done ---

Expected / post-fix (same as runtime, execution continues):

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.