diff --git a/src/backend/dev/LirCodeGen.zig b/src/backend/dev/LirCodeGen.zig index f4fe36201a5..25ac66e0eeb 100644 --- a/src/backend/dev/LirCodeGen.zig +++ b/src/backend/dev/LirCodeGen.zig @@ -1243,10 +1243,15 @@ pub fn LirCodeGen(comptime target: RocTarget) type { const final_result = try self.generateExpr(expr_id); const actual_ret_layout = result_layout; - // Store result to the saved result pointer - const ret_size = self.getLayoutSize(actual_ret_layout); - if (ret_size > 0) { - try self.storeResultToSavedPtr(final_result, actual_ret_layout, result_ptr_save_reg, tuple_len); + // If the body never returns (e.g., a top-level expression that is + // entirely a runtime_error / crash), the trap has already been + // emitted and there is no value to store. Skip the result store. + if (final_result != .noreturn) { + // Store result to the saved result pointer + const ret_size = self.getLayoutSize(actual_ret_layout); + if (ret_size > 0) { + try self.storeResultToSavedPtr(final_result, actual_ret_layout, result_ptr_save_reg, tuple_len); + } } // Emit epilogue using DeferredFrameBuilder with actual stack usage diff --git a/src/eval/test/comptime_eval_test.zig b/src/eval/test/comptime_eval_test.zig index 00932d0c3d9..8d15b720e73 100644 --- a/src/eval/test/comptime_eval_test.zig +++ b/src/eval/test/comptime_eval_test.zig @@ -3447,3 +3447,40 @@ test "issue 9281: dev evaluator stack overflow with nested recursive opaque type try testing.expect(code_result.code.len > 0); try testing.expect(code_result.entry_offset < code_result.code.len); } + +test "issue #9349: top-level expect with type-erroneous condition does not panic in dev codegen" { + const src = + \\foo : U64 -> U64 + \\foo = |x| x + \\ + \\expect foo(Dynamite) == 5 + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + // Locate the top-level `expect` statement so we can ask the dev + // backend to generate code for it, the same way `roc test` does + // (see rocTest in src/cli/main.zig). + const statements = result.module_env.store.sliceStatements(result.module_env.all_statements); + var expect_body: ?can.CIR.Expr.Idx = null; + for (statements) |stmt_idx| { + const stmt = result.module_env.store.getStatement(stmt_idx); + if (stmt == .s_expect) { + expect_body = stmt.s_expect.body; + break; + } + } + try testing.expect(expect_body != null); + + var dev_eval = try DevEvaluator.init(test_allocator, null); + defer dev_eval.deinit(); + + const all_module_envs = [_]*ModuleEnv{ result.builtin_module.env, result.module_env }; + + // Returning an error is acceptable; panicking is the bug. + if (dev_eval.generateCode(result.module_env, expect_body.?, &all_module_envs, null)) |code_result_ok| { + var code_result = code_result_ok; + code_result.deinit(); + } else |_| {} +} diff --git a/src/lir/MirToLir.zig b/src/lir/MirToLir.zig index abadae49d8b..3ef71c7d02a 100644 --- a/src/lir/MirToLir.zig +++ b/src/lir/MirToLir.zig @@ -5111,6 +5111,23 @@ fn lowerLowLevel(self: *Self, ll: anytype, mir_expr_id: MIR.ExprId, region: Regi try self.scratch_lir_expr_ids.append(self.allocator, ensured_arg); } + // If any lowered arg is itself a `runtime_error` (an atomic, noreturn LIR + // expression — see `isAtomicExpr`), the entire low-level call is + // unreachable: evaluating that arg traps and the op never executes. Replace + // the call with a single `runtime_error` carrying the call's ret layout so + // backends never see a noreturn expression in a value-position arg slot. + // Any accumulated let-bindings for prior args are preserved by `acc.finish` + // so their side effects still happen in source order before the trap. + for (self.scratch_lir_expr_ids.items[save_expr_len..]) |arg_lir_id| { + if (self.lir_store.getExpr(arg_lir_id) == .runtime_error) { + const re_result = try self.lir_store.addExpr( + .{ .runtime_error = .{ .ret_layout = ret_layout } }, + region, + ); + return acc.finish(re_result, ret_layout, region); + } + } + const lir_args = try self.lir_store.addExprSpan(self.scratch_lir_expr_ids.items[save_expr_len..]); const callable_proc = switch (ll.op) { .list_sort_with => blk: {