From 433e78890203c73f6fc341184765961af738a6c6 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sun, 18 Oct 2026 11:10:59 +0000 Subject: [PATCH 1/3] Skip handlers of unreachable or-patterns A guarded or-pattern after a case that matches everything crashed the compiler with "Fatal error: Matching.comp_exit". The or-pattern is compiled to a handler that its rows jump to. Its rows were dropped as unreachable, but the handler was still compiled; when its guard fails it falls through to the following cases, and in a match without a default there is nothing to fall through to. A handler that the compiled match never jumps to is now skipped. Example (rescript): let g = () => true let f = x => switch x { | _ => 2 | 1 | 2 if g() => 3 } Before (text): >> Fatal error: Matching.comp_exit Fatal error: exception Misc.Fatal_error After (js): function f(x) { return 2; } Source: compiled with the parent commit and with this one. Review: Can skipping a handler drop reachable code? The handler is skipped only when no `Lstaticraise` to its exit occurs in the compiled match, so no path can reach it. Area: lambda tests Output: unchanged, all committed generated JS is byte-identical; programs that crashed now compile Tests: +tests/tests/src/unreachable_or_guard_test.res Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo Signed-off-by: Christoph Knittel --- changes/unreachable-guarded-or-pattern.fix.md | 1 + compiler/ml/matching.ml | 9 ++++ tests/tests/src/unreachable_or_guard_test.mjs | 53 +++++++++++++++++++ tests/tests/src/unreachable_or_guard_test.res | 43 +++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 changes/unreachable-guarded-or-pattern.fix.md create mode 100644 tests/tests/src/unreachable_or_guard_test.mjs create mode 100644 tests/tests/src/unreachable_or_guard_test.res diff --git a/changes/unreachable-guarded-or-pattern.fix.md b/changes/unreachable-guarded-or-pattern.fix.md new file mode 100644 index 0000000000..0cba29da91 --- /dev/null +++ b/changes/unreachable-guarded-or-pattern.fix.md @@ -0,0 +1 @@ +- Fix a compiler crash ("Fatal error: Matching.comp_exit") on a guarded or-pattern that an earlier case makes unreachable. diff --git a/compiler/ml/matching.ml b/compiler/ml/matching.ml index e96eecb02a..fb4e14c9e8 100644 --- a/compiler/ml/matching.ml +++ b/compiler/ml/matching.ml @@ -2484,9 +2484,18 @@ let as_exit_call lam = in go Ident.empty lam +let rec raises_exit i lam = + match lam with + | Lstaticraise (j, _) when j = i -> true + | _ -> Lambda_traverse.shallow_exists (raises_exit i) lam + let compile_orhandlers compile_fun lambda1 total1 ctx to_catch = let rec do_rec r total_r = function | [] -> (r, total_r) + | (_, i, _, _) :: rem when not (raises_exit i r) -> + (* The or-pattern is unreachable, e.g. after a wildcard. Its handler is + not compiled: with a guard and no default, its failure has no exit. *) + do_rec r total_r rem | (mat, i, vars, pm) :: rem -> ( try let ctx = select_columns mat ctx in diff --git a/tests/tests/src/unreachable_or_guard_test.mjs b/tests/tests/src/unreachable_or_guard_test.mjs new file mode 100644 index 0000000000..b877b54a12 --- /dev/null +++ b/tests/tests/src/unreachable_or_guard_test.mjs @@ -0,0 +1,53 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + +import * as Mocha from "mocha"; +import * as Test_utils from "./test_utils.mjs"; + +function g() { + return true; +} + +function int(x) { + return 2; +} + +function char(c) { + return "any"; +} + +function string(s) { + return "any"; +} + +function pair(p) { + if (p[0] !== 0) { + return "other"; + } else { + return "zero"; + } +} + +Mocha.describe("Unreachable_or_guard_test", () => { + Mocha.test("unreachable guarded or-patterns", () => { + Test_utils.eq("File \"unreachable_or_guard_test.res\", line 37, characters 7-14", 2, 2); + Test_utils.eq("File \"unreachable_or_guard_test.res\", line 38, characters 7-14", "any", "any"); + Test_utils.eq("File \"unreachable_or_guard_test.res\", line 39, characters 7-14", "any", "any"); + Test_utils.eq("File \"unreachable_or_guard_test.res\", line 40, characters 7-14", pair([ + 0, + 1 + ]), "zero"); + Test_utils.eq("File \"unreachable_or_guard_test.res\", line 41, characters 7-14", pair([ + 1, + 1 + ]), "other"); + }); +}); + +export { + g, + int, + char, + string, + pair, +} +/* Not a pure module */ diff --git a/tests/tests/src/unreachable_or_guard_test.res b/tests/tests/src/unreachable_or_guard_test.res new file mode 100644 index 0000000000..a754995f9b --- /dev/null +++ b/tests/tests/src/unreachable_or_guard_test.res @@ -0,0 +1,43 @@ +// The guarded or-patterns below are unreachable. Compiling them used to fail +// with "Fatal error: Matching.comp_exit". +@@warning("-11") + +open Mocha +open Test_utils + +let g = () => true + +let int = x => + switch x { + | _ => 2 + | 1 | 2 if g() => 3 + } + +let char = c => + switch c { + | _ => "any" + | 'a' | 'b' if g() => "ab" + } + +let string = s => + switch s { + | _ => "any" + | "a" | "b" if g() => "ab" + } + +let pair = p => + switch p { + | (0, _) => "zero" + | (_, _) => "other" + | (1, 1) | (2, 2) if g() => "same" + } + +describe(__MODULE__, () => { + test("unreachable guarded or-patterns", () => { + eq(__LOC__, int(1), 2) + eq(__LOC__, char('a'), "any") + eq(__LOC__, string("a"), "any") + eq(__LOC__, pair((0, 1)), "zero") + eq(__LOC__, pair((1, 1)), "other") + }) +}) From 2ad87f483989512d7ec3797895f562af1ef5d95b Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sun, 18 Oct 2026 11:57:07 +0000 Subject: [PATCH 2/3] Test unreachable guarded or-patterns as a warning fixture The regression test disabled warning 11 for its module. It is now a super_errors fixture, which compiles with all warnings and records the four unused-case warnings; without the fix its snapshot shows the fatal error. Area: tests Output: unchanged Tests: +tests/build_tests/super_errors/fixtures/warning_11_unreachable_guarded_or_pattern.res -tests/tests/src/unreachable_or_guard_test.res Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo Signed-off-by: Christoph Knittel --- ...nreachable_guarded_or_pattern.res.expected | 47 ++++++++++++++++ ...ning_11_unreachable_guarded_or_pattern.res | 29 ++++++++++ tests/tests/src/unreachable_or_guard_test.mjs | 53 ------------------- tests/tests/src/unreachable_or_guard_test.res | 43 --------------- 4 files changed, 76 insertions(+), 96 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/warning_11_unreachable_guarded_or_pattern.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/warning_11_unreachable_guarded_or_pattern.res delete mode 100644 tests/tests/src/unreachable_or_guard_test.mjs delete mode 100644 tests/tests/src/unreachable_or_guard_test.res diff --git a/tests/build_tests/super_errors/expected/warning_11_unreachable_guarded_or_pattern.res.expected b/tests/build_tests/super_errors/expected/warning_11_unreachable_guarded_or_pattern.res.expected new file mode 100644 index 0000000000..a86ac90122 --- /dev/null +++ b/tests/build_tests/super_errors/expected/warning_11_unreachable_guarded_or_pattern.res.expected @@ -0,0 +1,47 @@ + + Warning number 11 + /.../fixtures/warning_11_unreachable_guarded_or_pattern.res:9:5-9 + + 7 ┆ switch x { + 8 ┆ | _ => 2 + 9 ┆ | 1 | 2 if g() => 3 + 10 ┆ } + 11 ┆ + + this match case is unused. + + + Warning number 11 + /.../fixtures/warning_11_unreachable_guarded_or_pattern.res:15:5-13 + + 13 ┆ switch c { + 14 ┆ | _ => "any" + 15 ┆ | 'a' | 'b' if g() => "ab" + 16 ┆ } + 17 ┆ + + this match case is unused. + + + Warning number 11 + /.../fixtures/warning_11_unreachable_guarded_or_pattern.res:21:5-13 + + 19 ┆ switch s { + 20 ┆ | _ => "any" + 21 ┆ | "a" | "b" if g() => "ab" + 22 ┆ } + 23 ┆ + + this match case is unused. + + + Warning number 11 + /.../fixtures/warning_11_unreachable_guarded_or_pattern.res:28:5-19 + + 26 ┆ | (0, _) => "zero" + 27 ┆ | (_, _) => "other" + 28 ┆ | (1, 1) | (2, 2) if g() => "same" + 29 ┆ } + 30 ┆ + + this match case is unused. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/warning_11_unreachable_guarded_or_pattern.res b/tests/build_tests/super_errors/fixtures/warning_11_unreachable_guarded_or_pattern.res new file mode 100644 index 0000000000..e013b1362e --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/warning_11_unreachable_guarded_or_pattern.res @@ -0,0 +1,29 @@ +// Each guarded or-pattern is unreachable after a case that matches +// everything. Compiling them used to fail with +// "Fatal error: Matching.comp_exit" instead of reporting them as unused. +let g = () => true + +let int = x => + switch x { + | _ => 2 + | 1 | 2 if g() => 3 + } + +let char = c => + switch c { + | _ => "any" + | 'a' | 'b' if g() => "ab" + } + +let string = s => + switch s { + | _ => "any" + | "a" | "b" if g() => "ab" + } + +let pair = p => + switch p { + | (0, _) => "zero" + | (_, _) => "other" + | (1, 1) | (2, 2) if g() => "same" + } diff --git a/tests/tests/src/unreachable_or_guard_test.mjs b/tests/tests/src/unreachable_or_guard_test.mjs deleted file mode 100644 index b877b54a12..0000000000 --- a/tests/tests/src/unreachable_or_guard_test.mjs +++ /dev/null @@ -1,53 +0,0 @@ -// Generated by ReScript, PLEASE EDIT WITH CARE - -import * as Mocha from "mocha"; -import * as Test_utils from "./test_utils.mjs"; - -function g() { - return true; -} - -function int(x) { - return 2; -} - -function char(c) { - return "any"; -} - -function string(s) { - return "any"; -} - -function pair(p) { - if (p[0] !== 0) { - return "other"; - } else { - return "zero"; - } -} - -Mocha.describe("Unreachable_or_guard_test", () => { - Mocha.test("unreachable guarded or-patterns", () => { - Test_utils.eq("File \"unreachable_or_guard_test.res\", line 37, characters 7-14", 2, 2); - Test_utils.eq("File \"unreachable_or_guard_test.res\", line 38, characters 7-14", "any", "any"); - Test_utils.eq("File \"unreachable_or_guard_test.res\", line 39, characters 7-14", "any", "any"); - Test_utils.eq("File \"unreachable_or_guard_test.res\", line 40, characters 7-14", pair([ - 0, - 1 - ]), "zero"); - Test_utils.eq("File \"unreachable_or_guard_test.res\", line 41, characters 7-14", pair([ - 1, - 1 - ]), "other"); - }); -}); - -export { - g, - int, - char, - string, - pair, -} -/* Not a pure module */ diff --git a/tests/tests/src/unreachable_or_guard_test.res b/tests/tests/src/unreachable_or_guard_test.res deleted file mode 100644 index a754995f9b..0000000000 --- a/tests/tests/src/unreachable_or_guard_test.res +++ /dev/null @@ -1,43 +0,0 @@ -// The guarded or-patterns below are unreachable. Compiling them used to fail -// with "Fatal error: Matching.comp_exit". -@@warning("-11") - -open Mocha -open Test_utils - -let g = () => true - -let int = x => - switch x { - | _ => 2 - | 1 | 2 if g() => 3 - } - -let char = c => - switch c { - | _ => "any" - | 'a' | 'b' if g() => "ab" - } - -let string = s => - switch s { - | _ => "any" - | "a" | "b" if g() => "ab" - } - -let pair = p => - switch p { - | (0, _) => "zero" - | (_, _) => "other" - | (1, 1) | (2, 2) if g() => "same" - } - -describe(__MODULE__, () => { - test("unreachable guarded or-patterns", () => { - eq(__LOC__, int(1), 2) - eq(__LOC__, char('a'), "any") - eq(__LOC__, string("a"), "any") - eq(__LOC__, pair((0, 1)), "zero") - eq(__LOC__, pair((1, 1)), "other") - }) -}) From 3f32388ae6020ed2821fac3a87920df45c7cb677 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Sun, 18 Oct 2026 12:10:17 +0000 Subject: [PATCH 3/3] Collect raised exits once when compiling or-pattern handlers Whether a handler is reachable was checked by scanning the whole compiled match, including function bodies, once per handler. The exits are now collected into a table once, extended with each compiled handler, and function bodies are skipped, since static exits cannot leave a function. Area: lambda Output: unchanged Tests: none, because the output is unchanged; the reachability check is covered by warning_11_unreachable_guarded_or_pattern.res Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JALXUyHhkDqzcB3WuW9Svo Signed-off-by: Christoph Knittel --- compiler/ml/matching.ml | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/compiler/ml/matching.ml b/compiler/ml/matching.ml index fb4e14c9e8..1b6e0e7fe5 100644 --- a/compiler/ml/matching.ml +++ b/compiler/ml/matching.ml @@ -2484,15 +2484,23 @@ let as_exit_call lam = in go Ident.empty lam -let rec raises_exit i lam = +(* Adds the static exits [lam] jumps to to [raised]. Exits cannot leave a + function, so function bodies are skipped. *) +let rec add_raised_exits raised lam = match lam with - | Lstaticraise (j, _) when j = i -> true - | _ -> Lambda_traverse.shallow_exists (raises_exit i) lam + | Lfunction _ -> () + | Lstaticraise (i, _) -> + Hashtbl.replace raised i (); + Lambda_traverse.iter (add_raised_exits raised) lam + | _ -> Lambda_traverse.iter (add_raised_exits raised) lam let compile_orhandlers compile_fun lambda1 total1 ctx to_catch = + (* The exits of the match and of the handlers compiled so far *) + let raised = Hashtbl.create 16 in + add_raised_exits raised lambda1; let rec do_rec r total_r = function | [] -> (r, total_r) - | (_, i, _, _) :: rem when not (raises_exit i r) -> + | (_, i, _, _) :: rem when not (Hashtbl.mem raised i) -> (* The or-pattern is unreachable, e.g. after a wildcard. Its handler is not compiled: with a guard and no default, its failure has no exit. *) do_rec r total_r rem @@ -2500,6 +2508,7 @@ let compile_orhandlers compile_fun lambda1 total1 ctx to_catch = try let ctx = select_columns mat ctx in let handler_i, total_i = compile_fun ctx pm in + add_raised_exits raised handler_i; match as_exit_call r with | Some (j, args) -> if i = j then