diff --git a/examples/0613-comptime-print-any-type.sx b/examples/0613-comptime-print-any-type.sx new file mode 100644 index 0000000..0eca7ac --- /dev/null +++ b/examples/0613-comptime-print-any-type.sx @@ -0,0 +1,32 @@ +// Comptime `#run` formatting of an `Any` that holds a `Type`. +// +// `print("{}", at)` where `at: Any` holds a `Type` value routes through +// `format` → `any_to_string`, whose `type := type_of(val)` lowers (for an +// `.any` operand) to `struct_get(val, 0)` — reading the Any-Type's tag. +// At runtime a `Type` value is the aggregate `{ tag=.any, value=tid }`, +// so the read works and the type's name prints. The comptime interpreter +// stores a first-class `Type` as a bare `.type_tag`, so the struct_get +// must mirror that same `{ .any, tid }` layout — otherwise it bails and +// the `#run` truncates. Reflection over the same `Any` (`type_name`, +// `type_is_unsigned`) already works; the value-print must match. +// +// Regression (issue 0096): a comptime `#run` print of an `Any`-held +// `Type` silently stopped (omitted `value=` + every later line) yet still +// built with exit 0. + +#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 :: () {} diff --git a/examples/expected/0613-comptime-print-any-type.exit b/examples/expected/0613-comptime-print-any-type.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/examples/expected/0613-comptime-print-any-type.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0613-comptime-print-any-type.stderr b/examples/expected/0613-comptime-print-any-type.stderr new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/examples/expected/0613-comptime-print-any-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0613-comptime-print-any-type.stdout b/examples/expected/0613-comptime-print-any-type.stdout new file mode 100644 index 0000000..985ca94 --- /dev/null +++ b/examples/expected/0613-comptime-print-any-type.stdout @@ -0,0 +1,6 @@ +before +name=u64 +unsigned=true +value=u64 +after +--- build done --- diff --git a/issues/0096-comptime-print-any-type-stops.md b/issues/0096-comptime-print-any-type-stops.md new file mode 100644 index 0000000..cc85ad1 --- /dev/null +++ b/issues/0096-comptime-print-any-type-stops.md @@ -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. diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index c67c8af..0c8d962 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -828,7 +828,23 @@ pub const LLVMEmitter = struct { interp_inst.build_config = &self.build_config; if (self.import_sources) |sm| interp_inst.setSourceMap(sm); sx_trace_clear(); - const result = interp_inst.call(func_id, &.{}) catch Value.void_val; + Interpreter.last_bail_op = null; + Interpreter.last_bail_builtin = null; + Interpreter.last_bail_detail = null; + const result = interp_inst.call(func_id, &.{}) catch |err| blk: { + // A comptime `#run` side-effect that bails must NOT silently + // truncate its output and still ship a successful build. + // Surface the bail loudly and fail the build, mirroring the + // const-init path in emitGlobals. Whatever output the run + // produced before the bail is flushed below so the user sees + // where execution stopped. + const op = Interpreter.last_bail_op orelse ""; + const detail = Interpreter.last_bail_detail orelse ""; + const sep: []const u8 = if (detail.len > 0) ": " else ""; + std.debug.print("error: comptime `#run` ({s}) failed: {s} (op={s}{s}{s})\n", .{ fname, @errorName(err), op, sep, detail }); + self.comptime_failed = true; + break :blk Value.void_val; + }; // Route #run `print` output to fd 1 so it joins the // JIT-executed runtime's stream. Same call site shape as // `core.flushInterpOutput` — see issue-0047. diff --git a/src/ir/interp.zig b/src/ir/interp.zig index be9d852..84c6119 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -916,6 +916,17 @@ pub const Interpreter = struct { if (fa.field_index == 0) return .{ .value = .{ .int = v } }; return error.OutOfBounds; }, + .type_tag => |tid| { + // A first-class Type value is the comptime form of the + // runtime Any-Type aggregate `{ tag=.any, value=tid }` + // (see `const_type` lowering in buildPackSliceValue). + // `type_of(any_holding_a_Type)` lowers to struct_get + // field 0, expecting that runtime layout — mirror it so + // field 0 reads the `.any` tag and field 1 the type id. + if (fa.field_index == 0) return .{ .value = .{ .int = @intCast(TypeId.any.index()) } }; + if (fa.field_index == 1) return .{ .value = .{ .type_tag = tid } }; + return error.OutOfBounds; + }, else => return typeErrorDetail("comptime struct_get: base has no fields (not an aggregate/string/int)"), } },