Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions design.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
214 changes: 140 additions & 74 deletions src/check/Check.zig

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/compile/mod.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
101 changes: 101 additions & 0 deletions src/compile/test/issue_10765_test.zig
Original file line number Diff line number Diff line change
@@ -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({})
\\
);
}
6 changes: 2 additions & 4 deletions test/snapshots/expr/unary_negation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions test/snapshots/expr/unary_not.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions test/snapshots/expr/unary_op_not.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions test/snapshots/fuzz_crash/fuzz_crash_023.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
12 changes: 5 additions & 7 deletions test/snapshots/lambda_capture/lambda_invalid_references.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)"))
~~~
8 changes: 3 additions & 5 deletions test/snapshots/minus_no_space_in_call.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions test/snapshots/nominal/associated_items_truly_comprehensive.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"))))
Expand All @@ -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"))))
Expand All @@ -3249,15 +3249,15 @@ 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"))))
(args
(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"))))
Expand Down Expand Up @@ -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"))))
Expand Down
6 changes: 2 additions & 4 deletions test/snapshots/records/record_different_fields_error.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion test/snapshots/rigid_var_no_instantiation_error.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 3 additions & 7 deletions test/snapshots/static_dispatch/plus_operator_vs_method.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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")))
Expand All @@ -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"))))
Expand Down
Loading
Loading