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

@@ -1305,10 +1305,30 @@ pub const Ops = struct {
return;
}
if (callee_func.is_comptime and call_op.args.len == 0) {
// Inline comptime-call fold: evaluate the zero-arg comptime callee on
// the VM (the sole evaluator) and splat its scalar/string result as a
// constant. A bail (null) falls through to the normal call path.
if (callee_func.is_comptime and !enclosing.is_comptime) {
// Inline comptime-call fold: a body-local `#run expr` lowers to a
// `call` of its `is_comptime` `__ct` wrapper. That wrapper is
// comptime-ONLY — it must NEVER survive as a runtime call (its body
// may read `---` storage / depend on comptime-only state). Evaluate
// it on the VM (the sole evaluator).
//
// GATE on `!enclosing.is_comptime`: only a call reached from a REAL
// runtime body (e.g. `main`) is an actual `#run` fold site. An
// `is_comptime` callee that appears INSIDE another comptime wrapper's
// body (`make_enum` / `declare` / `define` called from a `__ctype`
// type-fn wrapper) is DEAD LLVM — never executed — and the VM
// evaluates the whole wrapper itself; standalone-folding such a nested
// call would mis-`tryEval` it (wrong arg count) and emit a spurious
// failure. Leave those to the normal (dead) call path.
//
// For the live `#run` fold:
// - scalar / string result → splat as a constant (the common case);
// - a BAIL (`tryEval` null — e.g. an unbridgeable `[2][]i64` return)
// is a comptime-init FAILURE. Mirror the GLOBAL `#run` path
// (`emitGlobals` → `error: comptime init of 'X' failed: <reason>`,
// `comptime_failed`): emit the located diagnostic and gate the
// build, NEVER fall through to a runtime call over `---` storage
// (issue 0182 — that produced exit-0 garbage with no diagnostic).
if (comptime_vm.tryEval(self.e.alloc, self.e.ir_mod, call_op.callee, &self.e.build_config, self.e.import_sources)) |result| {
if (result.asInt()) |v| {
self.e.mapRef(c.LLVMConstInt(self.e.toLLVMType(instruction.ty), @bitCast(v), 0));
@@ -1323,7 +1343,37 @@ pub const Ops = struct {
self.e.mapRef(self.e.emitStringConstant(result.string));
return;
}
// A non-scalar bridgeable result (struct / array / `?Arr`) the VM
// materialized successfully but this scalar fold can't splat.
// Its `__ct` body runs correctly at runtime, so fall through to
// the ordinary call path (the established, tested behavior — the
// result is well-defined data, not `---` garbage). Only a BAIL
// (handled below) signals an actual comptime failure.
} else if (comptime_vm.last_bail_was_bridge) {
// `tryEval` RAN the wrapper but could not BRIDGE its result shape
// to a host value (e.g. an unbridgeable `[2][]i64` — array of
// slices). Re-emitting a runtime `call` would re-run the SAME body
// over its (possibly `---`) storage and produce DIFFERENT garbage
// with no diagnostic — the exact silent miscompile of issue 0182.
// Mirror the GLOBAL `#run` path (`emitGlobals` → `error: comptime
// init of 'X' failed: <reason>`, `comptime_failed`): surface the
// bridge bail loudly and gate the build.
const fname = if (callee_func.comptime_display_name) |dn|
self.e.ir_mod.types.getString(dn)
else
self.e.ir_mod.types.getString(callee_func.name);
std.debug.print(
"error: comptime init of '{s}' failed: {s}\n",
.{ fname, comptime_vm.last_bail_reason orelse "<unknown>" },
);
self.e.comptime_failed = true;
self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty)));
return;
}
// An EXECUTION bail (the VM couldn't run the body — an unported op, a
// VM `DivisionByZero` that the runtime computes as NaN, …): the
// established runtime-call fallback computes the correct value. Fall
// through to the ordinary call path — NOT a build failure.
}
const callee = self.e.func_map.get(call_op.callee.index()) orelse {
self.e.mapRef(c.LLVMGetUndef(self.e.toLLVMType(instruction.ty)));