fix: body-local #run of an unbridged shape fails loudly instead of silent garbage (issue 0182)

The body-local #run fold in emitCall was effectively dead (gated on
args.len==0, but the __ct comptime wrapper always carries the implicit
*Context arg), so every body-local #run fell through to a RUNTIME call:
bridgeable shapes lucked into the right value; an unbridgeable shape
(e.g. [2][]i64) ran over --- storage -> garbage, exit 0, no diagnostic.

Fold any is_comptime callee (gated !enclosing.is_comptime so nested
metatype calls in a comptime wrapper's dead body aren't folded). On a
tryEval bail, distinguish a BRIDGE bail (result can't regToValue-
materialize -> error: comptime init of 'X' failed: <reason> +
comptime_failed, build fails, symmetric with the global #run path) from
an EXECUTION bail (VM can't run the body, e.g. NaN/extern -> runtime
fallthrough, preserving types/0150), via comptime_vm.last_bail_was_bridge
(reset at tryEval entry, set only at regToValue). The const name is
threaded onto the wrapper (comptime_display_name) so the diagnostic reads
the source name, not __ct_N.

Regressions: diagnostics/1204 (negative), comptime/0645 (positive).
Verified by 3 adversarial reviews, suite 801/0.
This commit is contained in:
agra
2026-06-23 19:29:11 +03:00
parent 95c9c0df4c
commit 6c89a0aa3e
15 changed files with 182 additions and 4 deletions

View File

@@ -182,6 +182,19 @@ pub const Frame = struct {
/// the top of every `tryEval`; meaningful only when `tryEval` returned `null`.
pub var last_bail_reason: ?[]const u8 = null;
/// True iff the most recent `tryEval` bail happened at the RESULT-BRIDGE step
/// (`regToValue` — the comptime function RAN to completion but its result shape
/// cannot be materialized into a host `Value`), as opposed to an EXECUTION bail
/// (`runEntry` couldn't evaluate the body — an unported op, a VM
/// `DivisionByZero`, etc.). The distinction matters for a body-local `#run`
/// fold (issue 0182): a BRIDGE bail means a runtime re-execution would run the
/// SAME body over (possibly `---`) storage and produce DIFFERENT, garbage data —
/// a silent miscompile that must fail the build. An EXECUTION bail means the VM
/// simply can't run it; the established runtime-call fallback computes the
/// correct value and must be preserved. Meaningful only when `tryEval` returned
/// `null`; cleared at the top of every `tryEval`.
pub var last_bail_was_bridge: bool = false;
/// Wiring entry point: try to evaluate comptime function `func_id` entirely on the
/// comptime VM and return its result as a legacy `Value`, or `null` if the VM
/// can't handle it (unsupported op, no body, or any bail) — the caller then falls
@@ -194,6 +207,7 @@ pub var last_bail_reason: ?[]const u8 = null;
/// rather than crashing the compiler. On a bail, `last_bail_reason` names the cause.
pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.FuncId, build_config: ?*compiler_hooks.BuildConfig, source_map: ?*const std.StringHashMap([:0]const u8)) ?Value {
last_bail_reason = null;
last_bail_was_bridge = false;
const func = module.getFunction(func_id);
if (func.is_extern or func.blocks.items.len == 0) {
last_bail_reason = "extern / no body";
@@ -219,6 +233,10 @@ pub fn tryEval(gpa: std.mem.Allocator, module: *const Module, func_id: inst_mod.
// `regToValue` would bail on the void type, so yield `.void_val` directly.
if (func.ret == .void or func.ret == .noreturn) return .void_val;
return vm.regToValue(gpa, &module.types, reg, func.ret) catch |err| {
// The body RAN; only the result bridge failed → mark this a BRIDGE bail
// so a body-local `#run` fold can tell a genuine "result can't be
// materialized" miscompile from a "VM can't run it" fallback (issue 0182).
last_bail_was_bridge = true;
last_bail_reason = vm.detail orelse @errorName(err);
return null;
};