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:
@@ -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;
|
||||
};
|
||||
|
||||
@@ -640,6 +640,13 @@ pub const Function = struct {
|
||||
/// drops the leftover declaration. See current/PLAN-COMPILER-VM.md (S3).
|
||||
is_compiler_domain: bool = false,
|
||||
|
||||
/// For a body-local `#run` wrapper (`L :: #run f()` → an `is_comptime`
|
||||
/// `__ct_N` function): the user-facing const NAME the `#run` initializes, so
|
||||
/// a comptime-init failure can report `comptime init of 'L' failed` (issue
|
||||
/// 0182) rather than the internal `__ct_N` wrapper name. Null when the `#run`
|
||||
/// is not bound to a named const (a bare inline `#run`).
|
||||
comptime_display_name: ?StringId = null,
|
||||
|
||||
/// True for an `abi(.naked)` function — no calling-convention
|
||||
/// prologue/epilogue/frame, no implicit `__sx_ctx`. Its body is a single
|
||||
/// inline-asm block that reads args from ABI registers and emits its own
|
||||
|
||||
@@ -222,6 +222,12 @@ pub const Lowering = struct {
|
||||
suppress_int_fit_check: bool = false, // inside an explicit `xx` cast operand: truncation is requested, skip the literal fits-check
|
||||
block_counter: u32 = 0,
|
||||
comptime_counter: u32 = 0,
|
||||
/// Transient: the user-facing const name of the body-local `#run` currently
|
||||
/// being lowered (`L :: #run f()`), so `lowerInlineComptime` can stamp the
|
||||
/// `__ct` wrapper's `comptime_display_name` for a friendly comptime-init
|
||||
/// failure diagnostic (issue 0182). Set/cleared around the const's value
|
||||
/// lowering; null for a bare inline `#run`.
|
||||
comptime_const_name: ?[]const u8 = null,
|
||||
main_file: ?[]const u8 = null, // path of the main file; imported functions are declared extern
|
||||
resolved_root: ?*const Node = null, // full AST root (for building comptime modules)
|
||||
comptime_param_nodes: ?std.StringHashMap(*const Node) = null, // active comptime substitutions
|
||||
|
||||
@@ -337,6 +337,11 @@ pub fn lowerComptimeSideEffect(self: *Lowering, expr: *const Node) void {
|
||||
pub fn lowerInlineComptime(self: *Lowering, expr: *const Node) Ref {
|
||||
const ret_ty: TypeId = self.target_type orelse self.inferExprType(expr);
|
||||
const func_id = self.createComptimeFunction("__ct", expr, ret_ty);
|
||||
// Carry the binding const's name (when this `#run` initializes one) onto the
|
||||
// wrapper so a comptime-init failure names the user const, not `__ct_N`.
|
||||
if (self.comptime_const_name) |cname| {
|
||||
self.module.getFunctionMut(func_id).comptime_display_name = self.module.types.internString(cname);
|
||||
}
|
||||
// Emit a call to the comptime function. At interpretation time,
|
||||
// this will be evaluated and the result inlined as a constant.
|
||||
const func = &self.module.functions.items[@intFromEnum(func_id)];
|
||||
|
||||
@@ -478,6 +478,13 @@ pub fn lowerConstDecl(self: *Lowering, cd: *const ast.ConstDecl) void {
|
||||
return;
|
||||
}
|
||||
|
||||
// For a body-local `#run` const (`L :: #run f()`), record the const NAME so
|
||||
// the `__ct` wrapper carries it as a display name — a comptime-init failure
|
||||
// then reports `comptime init of 'L' failed` instead of `__ct_N` (issue 0182).
|
||||
const saved_ct_name = self.comptime_const_name;
|
||||
if (cd.value.data == .comptime_expr) self.comptime_const_name = cd.name;
|
||||
defer self.comptime_const_name = saved_ct_name;
|
||||
|
||||
const ref = self.lowerExpr(cd.value);
|
||||
// If there's an explicit type annotation, use it. Otherwise, infer from the expression.
|
||||
const ty = if (cd.type_annotation) |ta|
|
||||
|
||||
Reference in New Issue
Block a user