diff --git a/src/autoload/mod.rs b/src/autoload/mod.rs index 151f3928e7..21b81df38a 100644 --- a/src/autoload/mod.rs +++ b/src/autoload/mod.rs @@ -45,6 +45,7 @@ const BUILTIN_CLASS_LIKE_NAMES: &[&str] = &[ "EmptyIterator", "Error", "ArithmeticError", + "UnhandledMatchError", "Exception", "Fiber", "FiberError", diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index ab09a37f66..1438483759 100644 --- a/src/types/checker/builtin_types/declarations.rs +++ b/src/types/checker/builtin_types/declarations.rs @@ -72,6 +72,7 @@ pub(crate) fn inject_builtin_throwables( "TypeError", "ValueError", "ArithmeticError", + "UnhandledMatchError", "Exception", "RuntimeException", "JsonException", @@ -246,6 +247,25 @@ pub(crate) fn inject_builtin_throwables( used_traits: Vec::new(), }, ); + // Thrown by a `match` expression when no arm matches and no `default` arm is present. Codegen + // materializes it at the implicit no-match throw, so it must be a declared builtin subclass of + // Error for the checker to resolve the type reference. + class_map.insert( + "UnhandledMatchError".to_string(), + FlattenedClass { + name: "UnhandledMatchError".to_string(), + extends: Some("Error".to_string()), + implements: Vec::new(), + is_abstract: false, + is_final: false, + is_readonly_class: false, + properties: Vec::new(), + methods: Vec::new(), + attributes: Vec::new(), + constants: Vec::new(), + used_traits: Vec::new(), + }, + ); // Fiber: cooperative coroutine class. Methods are placeholders here — the // codegen intercepts every Fiber operation (`new Fiber(...)`, instance diff --git a/tests/codegen/control_flow/branches_and_loops.rs b/tests/codegen/control_flow/branches_and_loops.rs index 4fb7681c7e..ff421f7a7f 100644 --- a/tests/codegen/control_flow/branches_and_loops.rs +++ b/tests/codegen/control_flow/branches_and_loops.rs @@ -250,3 +250,26 @@ fn test_while_null_no_loop() { } // --- Ternary operator --- + +// --- match without default (UnhandledMatchError) --- + +/// Regression: a `match` with no `default` arm references the builtin `UnhandledMatchError` class +/// (thrown at the implicit no-match point), so that class must be a declared builtin subclass of +/// `Error`. Before it was declared, any default-less `match` failed with +/// "Undefined class: UnhandledMatchError". The happy path (a matching arm) compiles and runs. +#[test] +fn test_match_without_default_compiles() { + let out = compile_and_run( + r#" "one", + 2 => "two", + }; +} +echo classify(1), classify(2); +"#, + ); + assert_eq!(out, "onetwo"); +}