diff --git a/design.md b/design.md index 7beaa46001d..56dc438f08c 100644 --- a/design.md +++ b/design.md @@ -2700,6 +2700,43 @@ checking selected. In particular, `builtin_direct` stores neither the synthetic conversion callable nor a runtime dispatch plan, and `specialization_dispatch` is not a standalone compile-time root. +#### Erroneous Call Operand Retirement + +After checking an unannotated operand expression, the checker records its exact +identity in the dense `call_operand_type_error_exprs` table when its checked +value type contains an error. A repeated check replaces the expression's slot, +so the table always describes the latest check. This record is narrower than +`erroneous_value_exprs`: a relation may +replace an expression with a runtime error while preserving a usable checked +type for independent consumers, and that expression does not poison a parent +call relation. A call-like parent whose operand is in the narrower set is +retired before it introduces a static-dispatch constraint or stamps a dispatch +plan. `retireCallLikeExprWithErroneousOperands` marks the parent's effective +checker variable erroneous and inserts the parent expression into both sets. +This is a diagnostic-recovery mechanism, not a typing rule; an error-free operand +never takes this path. The decision consumes the checker's explicit +expression-identity record. It must not be reconstructed from the shape of a +constraint callable or from a later union-find representative. +An ordinary call judges its callee from the callable variable produced by +scheme instantiation; imported static methods may use an erroneous source +placeholder whose instantiated callable is valid. Its arguments consume +`call_operand_type_error_exprs` like the other call-like forms. +Statement-owned iterator loops have no parent expression to mark; they consume +the same operand record and leave the erroneous iterable expression in +`erroneous_value_exprs`. Checked for-nodes require a topology plan even on +recovery, so the checker mints the plan's callable shapes without attaching +constraints, marks both callable classes rejected, and records the required +plan. CheckedModule construction consumes those rejection markers to seal +both calls with explicit `checked_error` resolutions. + +Retirement is atomic with dispatch introduction. A retired expression emits no +constraint and no live plan; a required iterator recovery plan carries rejected +callables but still emits no constraints. Independently valid sibling +expressions introduce and solve their own constraints normally. This keeps an +erroneous callable out of a receiver's constraint scheme instead of asking +unification to coalesce it with a valid callable and asking checked-module +construction to choose a call site from the coalesced class. + Source dispatch, type dispatch, method equality, and iterator `for` plans all use checked dispatch plans. Iterator `for` uses its own iterator-dispatch operand shape because the `.next` call receives the compiler-created iterator @@ -5123,6 +5160,13 @@ Other solved-graph mutations: an already-reported error. It marks the checker node's solved class directly, preserving the class-wide cascade suppression previously provided by unifying that node with a fresh error variable. +- `retireCallLikeExprWithErroneousOperands` / statement-owned iterator plan + recovery (`markErroneous`, `markStaticDispatchFnRejected`, and the explicit + `call_operand_type_error_exprs` table)—mechanism: Erroneous Call Operand Retirement + (above). An operand already owns a reported error, so its call-like parent is + inserted into both expression sets before any dispatch constraint is + introduced; a required iterator plan is sealed with rejected callable metadata + instead. - `checkMatchExpr`'s branch-pattern target—mechanism: diagnostic recovery after an already-reported error. An erroneous scrutinee cannot relate the branch patterns to each other, so they unify against a shared fresh variable instead diff --git a/src/check/Check.zig b/src/check/Check.zig index f5b2e6688a0..a62aa0cc20a 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -524,6 +524,9 @@ value_lookup_tracking: std.ArrayListUnmanaged(ValueLookupEntry), /// Tracks expressions whose checked type contains an error, even if annotation /// preservation later gives their raw expr var a non-error type. erroneous_value_exprs: std.AutoHashMapUnmanaged(CIR.Expr.Idx, void), +/// Tracks unannotated expression identities whose checked value type contains +/// an error and therefore cannot be used to introduce a parent call relation. +call_operand_type_error_exprs: std.ArrayListUnmanaged(bool), /// Tracks bindings whose defining expression is known erroneous and whose /// subsequent local lookups must therefore become explicit runtime errors. erroneous_value_patterns: std.AutoHashMapUnmanaged(CIR.Pattern.Idx, void), @@ -2388,6 +2391,7 @@ fn initAssumePrepared( .binding_scheme_nodes = binding_scheme_nodes, .value_lookup_tracking = .empty, .erroneous_value_exprs = .empty, + .call_operand_type_error_exprs = try initNodeSlots(bool, gpa, node_count, false), .erroneous_value_patterns = .empty, .accepted_nominal_constructor_backings = .empty, .hoist_frames = .empty, @@ -2498,6 +2502,7 @@ pub fn deinit(self: *Self) void { self.predeclared_local_scheme_vars.deinit(self.gpa); self.value_lookup_tracking.deinit(self.gpa); self.erroneous_value_exprs.deinit(self.gpa); + self.call_operand_type_error_exprs.deinit(self.gpa); self.erroneous_value_patterns.deinit(self.gpa); self.accepted_nominal_constructor_backings.deinit(self.gpa); self.hoist_frames.deinit(self.gpa); @@ -15886,6 +15891,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) const expr = self.cir.store.getExpr(expr_idx); const expr_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(expr_idx)); const expr_var_raw = ModuleEnv.varFrom(expr_idx); + // Expression nodes can be checked again after more constraints settle. The + // dense slot always describes this check, so a prior error cannot retire a + // parent after the expression has checked successfully. + self.call_operand_type_error_exprs.items[nodeSlot(expr_idx)] = false; // Consume the checking_call_arg flag: it applies only to this immediate // checkExpr call and must not propagate to recursive calls (e.g. nested call @@ -17071,9 +17080,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) break :blk_instantiate call_func_expr_var; } }; - // Resolve the func var - const resolved_func = self.types.resolveVar(func_var).desc.content; - var did_err = resolved_func == .err; + // The instantiated callable is the authoritative callee result. + // Imported static methods can use an erroneous source placeholder + // whose binding scheme instantiates to a valid callable here. + var did_err = self.types.resolveVar(func_var).desc.content == .err; // Second, check the arguments being called // It could be effectful, e.g. `fn(mk_arg!())` @@ -17082,18 +17092,12 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) self.checking_call_arg = true; self.checking_immediate_callee = false; does_fx = try self.checkExpr(call_arg_idx, env, child_expected) or does_fx; - - // Check if this arg errored - did_err = did_err or (self.types.resolveVar(ModuleEnv.varFrom(call_arg_idx)).desc.content == .err); } + const args_did_err = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, call_arg_expr_idxs); + did_err = args_did_err or did_err; if (did_err) { - // If the fn or any args had error, propagate the error - // without doing any additional work. The call itself is - // the executable boundary that reaches the invalid - // child, so publish it as an explicit runtime error. - try self.markErroneous(expr_var); - try self.erroneous_value_exprs.put(self.gpa, expr_idx, {}); + try self.retireCallLikeExpr(expr_idx, expr_var); } else { // From the base function type, extract its shape. Effect // classification happens only after arguments unify below: @@ -17371,13 +17375,13 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) ) or does_fx; }, .e_binop => |binop| { - does_fx = try self.checkBinopExpr(expr_idx, expr_region, env, binop, nested_expected) or does_fx; + does_fx = try self.checkBinopExpr(expr_idx, expr_var, expr_region, env, binop, nested_expected) or does_fx; }, .e_unary_minus => |unary| { - does_fx = try self.checkUnaryMinusExpr(expr_idx, expr_region, env, unary, nested_expected) or does_fx; + does_fx = try self.checkUnaryMinusExpr(expr_idx, expr_var, expr_region, env, unary, nested_expected) or does_fx; }, .e_unary_not => |unary| { - does_fx = try self.checkUnaryNotExpr(expr_idx, expr_region, env, unary, nested_expected) or does_fx; + does_fx = try self.checkUnaryNotExpr(expr_idx, expr_var, expr_region, env, unary, nested_expected) or does_fx; }, .e_field_access => |field_access| { std.debug.assert(field_access.segments.len > 0); @@ -17494,7 +17498,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) const first_var = ModuleEnv.varFrom(interpolation.first); const str_var = try self.freshStr(env, expr_region); _ = try self.unify(first_var, str_var, env); - var did_err = self.types.resolveVar(first_var).desc.content == .err; + var did_err = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{interpolation.first}); const parts = self.cir.store.sliceExpr(interpolation.parts); std.debug.assert(parts.len % 2 == 0); @@ -17503,15 +17507,13 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) while (part_i < parts.len) : (part_i += 2) { self.checking_call_arg = true; does_fx = try self.checkExpr(parts[part_i], env, child_expected) or does_fx; - const interpolated_var = ModuleEnv.varFrom(parts[part_i]); - did_err = did_err or (self.types.resolveVar(interpolated_var).desc.content == .err); self.checking_call_arg = true; does_fx = try self.checkExpr(parts[part_i + 1], env, child_expected) or does_fx; const following_segment_var = ModuleEnv.varFrom(parts[part_i + 1]); _ = try self.unify(str_var, following_segment_var, env); - did_err = did_err or (self.types.resolveVar(following_segment_var).desc.content == .err); } + did_err = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, parts) or did_err; const pair_elems = try self.types.appendVars(&.{ item_var, str_var }); const pair_var = try self.freshFromContent(.{ .structure = .{ @@ -17528,9 +17530,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) .ret = step_ret_var, } } }, env, expr_region); - if (did_err) { - try self.markErroneous(expr_var); - } else { + if (!did_err) { const dispatcher_var = (try self.explicitTypeSuffixVar(expr_idx, expr_region, env)) orelse expr_var; const arg_vars = [_]Var{ first_var, rest_var }; const constraint_fn_var = try self.mkInterpolationConstraint( @@ -17557,7 +17557,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) .e_method_call => |method_call| { does_fx = try self.checkExpr(method_call.receiver, env, child_expected) or does_fx; const receiver_var = ModuleEnv.varFrom(method_call.receiver); - var did_err = self.types.resolveVar(receiver_var).desc.content == .err; + var did_err = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{method_call.receiver}); const arg_expr_idxs = self.cir.store.sliceExpr(method_call.args); var arg_vars_sfa = std.heap.stackFallback(16 * @sizeOf(Var), self.gpa); @@ -17570,12 +17570,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) does_fx = try self.checkExpr(arg_expr_idx, env, child_expected) or does_fx; const arg_var = ModuleEnv.varFrom(arg_expr_idx); arg_vars[i] = arg_var; - did_err = did_err or (self.types.resolveVar(arg_var).desc.content == .err); } + did_err = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, arg_expr_idxs) or did_err; - if (did_err) { - try self.markErroneous(expr_var); - } else { + if (!did_err) { const constraint_fn_var = try self.mkMethodCallConstraint( receiver_var, arg_vars, @@ -17602,17 +17600,15 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) }, .e_dispatch_call => |method_call| { does_fx = try self.checkExpr(method_call.receiver, env, child_expected) or does_fx; - var did_err = self.types.resolveVar(ModuleEnv.varFrom(method_call.receiver)).desc.content == .err; + _ = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{method_call.receiver}); - for (self.cir.store.sliceExpr(method_call.args)) |arg_expr_idx| { + const arg_expr_idxs = self.cir.store.sliceExpr(method_call.args); + for (arg_expr_idxs) |arg_expr_idx| { self.checking_call_arg = true; does_fx = try self.checkExpr(arg_expr_idx, env, child_expected) or does_fx; - did_err = did_err or (self.types.resolveVar(ModuleEnv.varFrom(arg_expr_idx)).desc.content == .err); } + _ = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, arg_expr_idxs); - if (did_err) { - try self.markErroneous(expr_var); - } if (try self.varIsEffectfulFunction(method_call.constraint_fn_var)) { self.markCurrentHoistObservableEffect(); does_fx = true; @@ -17649,11 +17645,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) const lhs_var = ModuleEnv.varFrom(eq.lhs); arg_vars[0] = ModuleEnv.varFrom(eq.rhs); - if (self.types.resolveVar(lhs_var).desc.content == .err or - self.types.resolveVar(arg_vars[0]).desc.content == .err) - { - try self.markErroneous(expr_var); - } else { + if (!try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{ eq.lhs, eq.rhs })) { const constraint_fn_var = try self.mkMethodCallConstraint( lhs_var, arg_vars, @@ -17679,18 +17671,15 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) const arg_vars = try arg_vars_alloc.alloc(Var, arg_expr_idxs.len); defer arg_vars_alloc.free(arg_vars); - var did_err = false; for (arg_expr_idxs, 0..) |arg_expr_idx, i| { self.checking_call_arg = true; does_fx = try self.checkExpr(arg_expr_idx, env, child_expected) or does_fx; const arg_var = ModuleEnv.varFrom(arg_expr_idx); arg_vars[i] = arg_var; - did_err = did_err or (self.types.resolveVar(arg_var).desc.content == .err); } + const did_err = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, arg_expr_idxs); - if (did_err) { - try self.markErroneous(expr_var); - } else { + if (!did_err) { const dispatcher_var = self.typeDispatchOwnerVar(method_call.type_dispatch_stmt); const constraint_fn_var = try self.mkTypeMethodCallConstraint( dispatcher_var, @@ -17716,16 +17705,13 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) } }, .e_type_dispatch_call => |method_call| { - var did_err = false; - for (self.cir.store.sliceExpr(method_call.args)) |arg_expr_idx| { + const arg_expr_idxs = self.cir.store.sliceExpr(method_call.args); + for (arg_expr_idxs) |arg_expr_idx| { self.checking_call_arg = true; does_fx = try self.checkExpr(arg_expr_idx, env, child_expected) or does_fx; - did_err = did_err or (self.types.resolveVar(ModuleEnv.varFrom(arg_expr_idx)).desc.content == .err); } + _ = try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, arg_expr_idxs); - if (did_err) { - try self.markErroneous(expr_var); - } if (try self.varIsEffectfulFunction(method_call.constraint_fn_var)) { self.markCurrentHoistObservableEffect(); does_fx = true; @@ -17763,6 +17749,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) self.markCurrentHoistObservableEffect(); does_fx = try self.checkIteratorForLoop( ModuleEnv.nodeIdxFrom(expr_idx), + .{ .expr_idx = expr_idx, .expr_var = expr_var }, for_expr.patt, for_expr.expr, for_expr.body, @@ -17928,6 +17915,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) if (mb_anno_vars == null) { if (try self.varContainsError(expr_var, &self.var_set)) { try self.erroneous_value_exprs.put(self.gpa, expr_idx, {}); + self.call_operand_type_error_exprs.items[nodeSlot(expr_idx)] = true; } } @@ -17994,6 +17982,37 @@ fn checkExprInCallPosition( return self.checkExpr(expr_idx, env, expected); } +/// Retire a call-like expression before it introduces dispatch or call +/// relations when one of its already-checked operands is an erroneous value. +/// The child expression set is explicit checker output; operand identity comes +/// only from that set, never from the operand's solved type shape. +fn retireCallLikeExprWithErroneousOperands( + self: *Self, + expr_idx: CIR.Expr.Idx, + expr_var: Var, + operand_exprs: []const CIR.Expr.Idx, +) Allocator.Error!bool { + if (!self.callLikeOperandsContainErroneousValue(operand_exprs)) return false; + + try self.retireCallLikeExpr(expr_idx, expr_var); + return true; +} + +fn retireCallLikeExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_var: Var) Allocator.Error!void { + try self.markErroneous(expr_var); + if (!self.erroneous_value_exprs.contains(expr_idx)) { + try self.erroneous_value_exprs.put(self.gpa, expr_idx, {}); + } + self.call_operand_type_error_exprs.items[nodeSlot(expr_idx)] = true; +} + +fn callLikeOperandsContainErroneousValue(self: *const Self, operand_exprs: []const CIR.Expr.Idx) bool { + for (operand_exprs) |operand_expr| { + if (self.call_operand_type_error_exprs.items[nodeSlot(operand_expr)]) return true; + } + return false; +} + fn getExprPatternIdent(self: *const Self, expr_idx: CIR.Expr.Idx) ?Ident.Idx { const trace = tracy.trace(@src()); defer trace.end(); @@ -19216,6 +19235,7 @@ fn checkBlockStatements(self: *Self, statements: CIR.Statement.Span, env: *Env, const for_expected = if (blocks_later_hoists) base_statement_expected else statement_expected; does_fx = try self.checkIteratorForLoop( ModuleEnv.nodeIdxFrom(stmt_idx), + null, for_stmt.patt, for_stmt.expr, for_stmt.body, @@ -20272,15 +20292,15 @@ fn checkMatchExpr( /// Check the unary expr. /// Desugars `-a` to `a.negate() : a -> a`, -fn checkUnaryMinusExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, env: *Env, unary: CIR.Expr.UnaryMinus, expected: Expected) Allocator.Error!bool { +fn checkUnaryMinusExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_var: Var, expr_region: Region, env: *Env, unary: CIR.Expr.UnaryMinus, expected: Expected) Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); - const expr_var = @as(Var, ModuleEnv.varFrom(expr_idx)); const child_expected = expected.forStatement(); // Check the operand expression const does_fx = try self.checkExpr(unary.expr, env, child_expected); + if (try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{unary.expr})) return does_fx; // Get the not method + ret var // Here, we assert that the arg and ret of `not` are same type @@ -20302,15 +20322,15 @@ fn checkUnaryMinusExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, /// Check the unary expr. /// Desugars `!a` to `a.not() : a -> a`, -fn checkUnaryNotExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, env: *Env, unary: CIR.Expr.UnaryNot, expected: Expected) Allocator.Error!bool { +fn checkUnaryNotExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_var: Var, expr_region: Region, env: *Env, unary: CIR.Expr.UnaryNot, expected: Expected) Allocator.Error!bool { const trace = tracy.trace(@src()); defer trace.end(); - const expr_var = @as(Var, ModuleEnv.varFrom(expr_idx)); const child_expected = expected.forStatement(); // Check the operand expression const does_fx = try self.checkExpr(unary.expr, env, child_expected); + if (try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{unary.expr})) return does_fx; // Get the not method + ret var // Here, we assert that the arg and ret of `not` are same type @@ -20334,6 +20354,7 @@ fn checkUnaryNotExpr(self: *Self, expr_idx: CIR.Expr.Idx, expr_region: Region, e fn checkBinopExpr( self: *Self, expr_idx: CIR.Expr.Idx, + expr_var: Var, expr_region: Region, env: *Env, binop: CIR.Expr.Binop, @@ -20342,7 +20363,6 @@ fn checkBinopExpr( const trace = tracy.trace(@src()); defer trace.end(); - const expr_var = ModuleEnv.varFrom(expr_idx); const lhs_var = @as(Var, ModuleEnv.varFrom(binop.lhs)); const rhs_var = @as(Var, ModuleEnv.varFrom(binop.rhs)); const child_expected = expected.forStatement(); @@ -20352,6 +20372,12 @@ fn checkBinopExpr( does_fx = try self.checkExpr(binop.lhs, env, child_expected) or does_fx; does_fx = try self.checkExpr(binop.rhs, env, child_expected) or does_fx; + if (binop.op != .@"and" and binop.op != .@"or" and + try self.retireCallLikeExprWithErroneousOperands(expr_idx, expr_var, &.{ binop.lhs, binop.rhs })) + { + return does_fx; + } + switch (binop.op) { .add, .sub, .mul, .div, .rem, .div_trunc => { const method_name = @@ -20994,9 +21020,15 @@ fn publishUnaryDispatchExpr( ); } +const IteratorLoopExpr = struct { + expr_idx: CIR.Expr.Idx, + expr_var: Var, +}; + fn checkIteratorForLoop( self: *Self, loop_node: CIR.Node.Idx, + loop_expr: ?IteratorLoopExpr, pattern: CIR.Pattern.Idx, iterable: CIR.Expr.Idx, body: CIR.Expr.Idx, @@ -21015,28 +21047,38 @@ fn checkIteratorForLoop( const iterable_region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(iterable)); const iterable_var: Var = ModuleEnv.varFrom(iterable); + const iterable_is_erroneous = if (loop_expr) |expr| + try self.retireCallLikeExprWithErroneousOperands(expr.expr_idx, expr.expr_var, &.{iterable}) + else + self.callLikeOperandsContainErroneousValue(&.{iterable}); const iterator_var = try self.mkIterVar(item_var, env, iterable_region); const iter_method = try @constCast(self.cir).insertIdent(base.Ident.for_text("iter")); - const iter_fn_var = try self.mkSyntheticReceiverDispatchConstraint( - iterable_var, - &.{}, - iterator_var, - iter_method, - env, - iterable_region, - ); + const iter_fn_var = if (iterable_is_erroneous) + try self.mkRejectedSyntheticReceiverDispatchFn(iterable_var, &.{}, iterator_var, env, iterable_region) + else + try self.mkSyntheticReceiverDispatchConstraint( + iterable_var, + &.{}, + iterator_var, + iter_method, + env, + iterable_region, + ); const step = try self.mkIteratorStepContent(item_var, iterator_var, env); const step_var = try self.freshFromContent(step.content, env, loop_region); const next_method = try @constCast(self.cir).insertIdent(base.Ident.for_text("next")); - const next_fn_var = try self.mkSyntheticReceiverDispatchConstraint( - iterator_var, - &.{}, - step_var, - next_method, - env, - loop_region, - ); + const next_fn_var = if (iterable_is_erroneous) + try self.mkRejectedSyntheticReceiverDispatchFn(iterator_var, &.{}, step_var, env, loop_region) + else + try self.mkSyntheticReceiverDispatchConstraint( + iterator_var, + &.{}, + step_var, + next_method, + env, + loop_region, + ); try self.cir.recordForLoopDispatchPlan( loop_node, @@ -21107,15 +21149,26 @@ fn mkSyntheticReceiverDispatchConstraint( ); } -fn mkReceiverDispatchConstraint( +fn mkRejectedSyntheticReceiverDispatchFn( + self: *Self, + receiver_var: Var, + arg_vars: []const Var, + ret_var: Var, + env: *Env, + region: Region, +) Allocator.Error!Var { + const fn_var = try self.mkReceiverDispatchFnVar(receiver_var, arg_vars, ret_var, env, region); + try self.markStaticDispatchFnRejected(fn_var); + return fn_var; +} + +fn mkReceiverDispatchFnVar( self: *Self, receiver_var: Var, arg_vars: []const Var, ret_var: Var, - method_name: Ident.Idx, env: *Env, region: Region, - method_expr_idx: ?CIR.Expr.Idx, ) Allocator.Error!Var { var all_args_sfa = std.heap.stackFallback(16 * @sizeOf(Var), self.gpa); const all_args_alloc = all_args_sfa.get(); @@ -21125,10 +21178,23 @@ fn mkReceiverDispatchConstraint( @memcpy(all_args[1..], arg_vars); const args_range = try self.types.appendVars(all_args); - const constraint_fn_var = try self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ + return self.freshFromContent(.{ .structure = .{ .fn_unbound = Func{ .args = args_range, .ret = ret_var, } } }, env, region); +} + +fn mkReceiverDispatchConstraint( + self: *Self, + receiver_var: Var, + arg_vars: []const Var, + ret_var: Var, + method_name: Ident.Idx, + env: *Env, + region: Region, + method_expr_idx: ?CIR.Expr.Idx, +) Allocator.Error!Var { + const constraint_fn_var = try self.mkReceiverDispatchFnVar(receiver_var, arg_vars, ret_var, env, region); const constraint = StaticDispatchConstraint{ .fn_name = method_name, diff --git a/src/compile/mod.zig b/src/compile/mod.zig index f390cab8c24..45450ed428d 100644 --- a/src/compile/mod.zig +++ b/src/compile/mod.zig @@ -138,6 +138,7 @@ test "compile tests" { std.testing.refAllDecls(@import("test/issue_10712_test.zig")); std.testing.refAllDecls(@import("test/issue_10723_test.zig")); std.testing.refAllDecls(@import("test/issue_10724_test.zig")); + std.testing.refAllDecls(@import("test/issue_10765_test.zig")); std.testing.refAllDecls(@import("test/tce_capture_test.zig")); std.testing.refAllDecls(@import("test/list_map_target_independent_lir_test.zig")); std.testing.refAllDecls(@import("test/platform_box_update_lir_test.zig")); diff --git a/src/compile/test/issue_10765_test.zig b/src/compile/test/issue_10765_test.zig new file mode 100644 index 00000000000..3145dc42c7c --- /dev/null +++ b/src/compile/test/issue_10765_test.zig @@ -0,0 +1,101 @@ +//! Regression test for issue #10765. + +const std = @import("std"); +const roc_target = @import("roc_target"); + +const compile_build = @import("../compile_build.zig"); +const BuildEnv = compile_build.BuildEnv; + +const Issue10765TestError = compile_build.InitError || + compile_build.BuildRootError || + std.Io.Dir.RealPathFileAllocError || + std.Io.Dir.WriteFileError || + error{TestUnexpectedResult}; + +/// Check `source` as `main.roc` and assert that the out-of-scope name in it is +/// reported to the user. +fn expectChecksWithNameNotInScope(source: []const u8) Issue10765TestError!void { + const gpa = std.testing.allocator; + const io = std.testing.io; + + var tmp_dir = std.testing.tmpDir(.{}); + defer tmp_dir.cleanup(); + + try tmp_dir.dir.writeFile(io, .{ .sub_path = "main.roc", .data = source }); + + const cwd = try tmp_dir.dir.realPathFileAlloc(io, ".", gpa); + defer gpa.free(cwd); + const main_path = try tmp_dir.dir.realPathFileAlloc(io, "main.roc", gpa); + defer gpa.free(main_path); + + var build_env = try BuildEnv.init(gpa, .single_threaded, 1, roc_target.RocTarget.detectNative(), cwd, io); + defer build_env.deinit(); + + try build_env.build(main_path); + + const drained = try build_env.drainReports(); + defer build_env.freeDrainedReports(drained); + + var found_name_not_in_scope = false; + var found_missing_method = false; + for (drained) |mod| { + for (mod.reports) |report| { + if (std.mem.eql(u8, report.title, "Name Not In Scope")) found_name_not_in_scope = true; + if (std.mem.eql(u8, report.title, "Missing Method")) found_missing_method = true; + } + } + try std.testing.expect(found_name_not_in_scope); + try std.testing.expect(!found_missing_method); +} + +// repro for https://github.com/roc-lang/roc/issues/10765 +// +// Two calls to the same function, where one argument's numeric binop operand is +// an out-of-scope name and the other's is a literal, must publish as a checked +// artifact carrying the name-not-in-scope error. +test "issue 10765: numeric dispatch on an out-of-scope operand reports the name" { + try expectChecksWithNameNotInScope( + \\f = |n| f(n - d) + f(n - 2) + \\ + \\main! = |_| Ok({}) + \\ + ); +} + +test "issue 10765: a later erroneous dispatch does not retire an earlier valid sibling" { + try expectChecksWithNameNotInScope( + \\f = |n| f(n - 2) + f(n - d) + \\ + \\main! = |_| Ok({}) + \\ + ); +} + +test "issue 10765: nested erroneous dispatch operand reports the name" { + try expectChecksWithNameNotInScope( + \\f = |n| n.foo({ bad: d }) + n.foo({ bad: 2 }) + \\ + \\main! = |_| Ok({}) + \\ + ); +} + +test "issue 10765: erroneous for iterable does not introduce iterator constraints" { + try expectChecksWithNameNotInScope( + \\f = || { + \\ for _ in { bad: d } 0 + \\} + \\ + \\main! = |_| Ok({}) + \\ + ); +} + +test "issue 10765: recursive fib with an out-of-scope operand reports the name" { + try expectChecksWithNameNotInScope( + \\fib = |n| if n <= !1 n else fib(n - d) + fib(n - 2) + \\ + \\main! = |_| Ok({}) + \\ + ); +} diff --git a/test/snapshots/expr/unary_negation.md b/test/snapshots/expr/unary_negation.md index 268901b813b..4e1b371388a 100644 --- a/test/snapshots/expr/unary_negation.md +++ b/test/snapshots/expr/unary_negation.md @@ -27,10 +27,8 @@ NO CHANGE ~~~ # CANONICALIZE ~~~clojure -(e-dispatch-call (method "negate") (constraint-fn-var 203) - (receiver - (e-runtime-error (tag "ident_not_in_scope"))) - (args)) +(e-unary-minus + (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES ~~~clojure diff --git a/test/snapshots/expr/unary_not.md b/test/snapshots/expr/unary_not.md index 7e9480755fc..29452f0a9b6 100644 --- a/test/snapshots/expr/unary_not.md +++ b/test/snapshots/expr/unary_not.md @@ -27,10 +27,8 @@ NO CHANGE ~~~ # CANONICALIZE ~~~clojure -(e-dispatch-call (method "not") (constraint-fn-var 203) - (receiver - (e-runtime-error (tag "ident_not_in_scope"))) - (args)) +(e-unary-not + (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES ~~~clojure diff --git a/test/snapshots/expr/unary_op_not.md b/test/snapshots/expr/unary_op_not.md index 5b95d61b8f8..37a30c6d518 100644 --- a/test/snapshots/expr/unary_op_not.md +++ b/test/snapshots/expr/unary_op_not.md @@ -27,10 +27,8 @@ NO CHANGE ~~~ # CANONICALIZE ~~~clojure -(e-dispatch-call (method "not") (constraint-fn-var 203) - (receiver - (e-runtime-error (tag "ident_not_in_scope"))) - (args)) +(e-unary-not + (e-runtime-error (tag "ident_not_in_scope"))) ~~~ # TYPES ~~~clojure diff --git a/test/snapshots/fuzz_crash/fuzz_crash_023.md b/test/snapshots/fuzz_crash/fuzz_crash_023.md index 09dc6093929..c2584886a5c 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_023.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_023.md @@ -278,6 +278,8 @@ DECLARATION HAS NO VALUE - fuzz_crash_023.md:178:47:178:71 TOO FEW ARGS - fuzz_crash_023.md:155:2:157:3 TYPE MISMATCH - fuzz_crash_023.md:167:3:167:3 DECLARATION HAS NO VALUE - fuzz_crash_023.md:178:47:178:71 +TYPE MISMATCH - fuzz_crash_023.md:175:26:175:27 +TYPE MISMATCH - fuzz_crash_023.md:175:34:175:40 DECLARATION HAS NO VALUE - fuzz_crash_023.md:201:1:201:25 MISSING METHOD - fuzz_crash_023.md:189:26:189:40 MISSING METHOD - fuzz_crash_023.md:189:26:189:66 @@ -976,6 +978,36 @@ record = { foo: 123, bar: "Hello", ;az: tag, qux: Ok(world), punned } Add a value body here, or put hosted functions in a platform type mod so they are published through the host boundary. +── ✗ type mismatch ──────────────────────────────────── fuzz_crash_023.md:175:26 + +This expression is used in an unexpected way. + +Stdout.line!("Adding ${n} to ${number}") + ^ + +It has the type: + + Dec + +But you are trying to use it as: + + Str + +── ✗ type mismatch ──────────────────────────────────── fuzz_crash_023.md:175:34 + +This expression is used in an unexpected way. + +Stdout.line!("Adding ${n} to ${number}") + ^^^^^^ + +It has the type: + + Dec + +But you are trying to use it as: + + Str + ── ● declaration has no value ────────────────────────── fuzz_crash_023.md:201:1 This declaration has a type annotation but no implementation. diff --git a/test/snapshots/lambda_capture/lambda_invalid_references.md b/test/snapshots/lambda_capture/lambda_invalid_references.md index fdd9e4a2945..8485182d45b 100644 --- a/test/snapshots/lambda_capture/lambda_invalid_references.md +++ b/test/snapshots/lambda_capture/lambda_invalid_references.md @@ -43,14 +43,12 @@ NO CHANGE (e-lambda (args (p-assign (ident "y"))) - (e-dispatch-call (method "plus") (constraint-fn-var 211) - (receiver - (e-lookup-local - (p-assign (ident "x")))) - (args - (e-runtime-error (tag "ident_not_in_scope"))))))) + (e-binop (op "add") + (e-lookup-local + (p-assign (ident "x"))) + (e-runtime-error (tag "ident_not_in_scope")))))) ~~~ # TYPES ~~~clojure -(expr (type "a -> (_arg -> a) where [a.plus : a, Error -> a]")) +(expr (type "_arg -> (_arg2 -> Error)")) ~~~ diff --git a/test/snapshots/minus_no_space_in_call.md b/test/snapshots/minus_no_space_in_call.md index a6046cd925d..6b4c24a866e 100644 --- a/test/snapshots/minus_no_space_in_call.md +++ b/test/snapshots/minus_no_space_in_call.md @@ -32,11 +32,9 @@ foo(x - 1) ~~~clojure (e-call (e-runtime-error (tag "ident_not_in_scope")) - (e-dispatch-call (method "minus") (constraint-fn-var 214) - (receiver - (e-runtime-error (tag "ident_not_in_scope"))) - (args - (e-num (value "1"))))) + (e-binop (op "sub") + (e-runtime-error (tag "ident_not_in_scope")) + (e-num (value "1")))) ~~~ # TYPES ~~~clojure diff --git a/test/snapshots/nominal/associated_items_truly_comprehensive.md b/test/snapshots/nominal/associated_items_truly_comprehensive.md index ec522dd3702..8d6dfd9d592 100644 --- a/test/snapshots/nominal/associated_items_truly_comprehensive.md +++ b/test/snapshots/nominal/associated_items_truly_comprehensive.md @@ -3219,7 +3219,7 @@ anno2 = Annotated.L2.alsoTyped # 889 (e-runtime-error (tag "erroneous_value_use"))) (d-let (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.val1")) - (e-dispatch-call (method "plus") (constraint-fn-var 1703) + (e-dispatch-call (method "plus") (constraint-fn-var 1697) (receiver (e-lookup-local (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.L3.val3")))) @@ -3230,11 +3230,11 @@ anno2 = Annotated.L2.alsoTyped # 889 (e-num (value "5"))) (d-let (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.L3.L4.L5.val5")) - (e-dispatch-call (method "plus") (constraint-fn-var 1727) + (e-dispatch-call (method "plus") (constraint-fn-var 1721) (receiver - (e-dispatch-call (method "plus") (constraint-fn-var 1725) + (e-dispatch-call (method "plus") (constraint-fn-var 1719) (receiver - (e-dispatch-call (method "plus") (constraint-fn-var 1723) + (e-dispatch-call (method "plus") (constraint-fn-var 1717) (receiver (e-lookup-local (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.val1")))) @@ -3249,7 +3249,7 @@ anno2 = Annotated.L2.alsoTyped # 889 (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.L3.L4.val4")))))) (d-let (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.L3.L4.val4")) - (e-dispatch-call (method "times") (constraint-fn-var 1712) + (e-dispatch-call (method "times") (constraint-fn-var 1706) (receiver (e-lookup-local (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.L3.val3")))) @@ -3257,7 +3257,7 @@ anno2 = Annotated.L2.alsoTyped # 889 (e-num (value "2"))))) (d-let (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.val2")) - (e-dispatch-call (method "plus") (constraint-fn-var 1721) + (e-dispatch-call (method "plus") (constraint-fn-var 1715) (receiver (e-lookup-local (p-assign (ident "associated_items_truly_comprehensive.D5_Pattern3.L2.L3.L4.val4")))) @@ -3358,7 +3358,7 @@ anno2 = Annotated.L2.alsoTyped # 889 (ty-lookup (name "U64") (builtin)))) (d-let (p-assign (ident "associated_items_truly_comprehensive.Annotated.L2.alsoTyped")) - (e-dispatch-call (method "plus") (constraint-fn-var 1775) + (e-dispatch-call (method "plus") (constraint-fn-var 1769) (receiver (e-lookup-local (p-assign (ident "associated_items_truly_comprehensive.Annotated.typed")))) diff --git a/test/snapshots/records/record_different_fields_error.md b/test/snapshots/records/record_different_fields_error.md index 9b002a7da0a..05a7bd9ba4b 100644 --- a/test/snapshots/records/record_different_fields_error.md +++ b/test/snapshots/records/record_different_fields_error.md @@ -525,10 +525,8 @@ EndOfFile, (s-expr (e-runtime-error (tag "ident_not_in_scope"))) (s-expr - (e-dispatch-call (method "negate") (constraint-fn-var 318) - (receiver - (e-runtime-error (tag "ident_not_in_scope"))) - (args))) + (e-unary-minus + (e-runtime-error (tag "ident_not_in_scope")))) (s-expr (e-runtime-error (tag "expr_not_canonicalized"))) (s-expr diff --git a/test/snapshots/rigid_var_no_instantiation_error.md b/test/snapshots/rigid_var_no_instantiation_error.md index 9b0af9a2ca4..736c16e90fa 100644 --- a/test/snapshots/rigid_var_no_instantiation_error.md +++ b/test/snapshots/rigid_var_no_instantiation_error.md @@ -243,7 +243,7 @@ main! = |_| { (e-runtime-error (tag "erroneous_value_expr"))) (s-let (p-assign (ident "result3")) - (e-call (constraint-fn-var 338) + (e-call (constraint-fn-var 337) (e-lookup-local (p-assign (ident "swap"))) (e-tuple diff --git a/test/snapshots/static_dispatch/plus_operator_vs_method.md b/test/snapshots/static_dispatch/plus_operator_vs_method.md index 878b58502bd..9b03b5cca01 100644 --- a/test/snapshots/static_dispatch/plus_operator_vs_method.md +++ b/test/snapshots/static_dispatch/plus_operator_vs_method.md @@ -160,11 +160,7 @@ NO CHANGE (ty-lookup (name "MyType") (local)))) (d-let (p-assign (ident "result1")) - (e-binop (op "add") - (e-lookup-local - (p-assign (ident "a"))) - (e-lookup-local - (p-assign (ident "b")))) + (e-runtime-error (tag "erroneous_value_expr")) (annotation (ty-lookup (name "MyType") (local)))) (d-let @@ -200,7 +196,7 @@ NO CHANGE (defs (patt (type "MyType")) (patt (type "MyType")) - (patt (type "Error")) + (patt (type "MyType")) (patt (type "MyType")) (patt (type "MyType")) (patt (type "MyType"))) @@ -210,7 +206,7 @@ NO CHANGE (expressions (expr (type "MyType")) (expr (type "MyType")) - (expr (type "Error")) + (expr (type "MyType")) (expr (type "MyType")) (expr (type "MyType")) (expr (type "MyType")))) diff --git a/test/snapshots/syntax_grab_bag.md b/test/snapshots/syntax_grab_bag.md index a8600032cc4..a1caf4d0e28 100644 --- a/test/snapshots/syntax_grab_bag.md +++ b/test/snapshots/syntax_grab_bag.md @@ -270,6 +270,8 @@ MISSING METHOD - syntax_grab_bag.md:101:3:101:8 TYPE MISMATCH - syntax_grab_bag.md:84:2:84:2 TOO FEW ARGS - syntax_grab_bag.md:155:2:157:3 TYPE MISMATCH - syntax_grab_bag.md:167:3:167:3 +TYPE MISMATCH - syntax_grab_bag.md:175:26:175:27 +TYPE MISMATCH - syntax_grab_bag.md:175:34:175:40 DECLARATION HAS NO VALUE - syntax_grab_bag.md:201:1:201:25 MISSING METHOD - syntax_grab_bag.md:189:26:189:40 MISSING METHOD - syntax_grab_bag.md:189:26:189:66 @@ -873,6 +875,36 @@ But add_one needs the first argument to be: U64 +── ✗ type mismatch ─────────────────────────────────── syntax_grab_bag.md:175:26 + +This expression is used in an unexpected way. + +Stdout.line!("Adding ${n} to ${number}") + ^ + +It has the type: + + Dec + +But you are trying to use it as: + + Str + +── ✗ type mismatch ─────────────────────────────────── syntax_grab_bag.md:175:34 + +This expression is used in an unexpected way. + +Stdout.line!("Adding ${n} to ${number}") + ^^^^^^ + +It has the type: + + Dec + +But you are trying to use it as: + + Str + ── ● declaration has no value ───────────────────────── syntax_grab_bag.md:201:1 This declaration has a type annotation but no implementation.