From 4a82b7aad6dc919c29b7b1f98bf986013b2a11b0 Mon Sep 17 00:00:00 2001 From: Chad Peppers Date: Thu, 9 Jul 2026 13:36:08 -0500 Subject: [PATCH] feat(errors): register UnhandledMatchError as a builtin Error subclass `UnhandledMatchError` (PHP 8's error for a `match` with no matching arm and no default) is now a registered builtin class extending `Error`: newable with a message, throwable from a `match` default arm, catchable by name, and its getMessage() round-trips. Co-Authored-By: Claude Opus 4.8 --- .../checker/builtin_types/declarations.rs | 17 +++++++++++++ src/types/checker/builtin_types/exception.rs | 1 + tests/codegen/exceptions.rs | 25 +++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/src/types/checker/builtin_types/declarations.rs b/src/types/checker/builtin_types/declarations.rs index ab09a37f66..ec99f5a974 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,22 @@ pub(crate) fn inject_builtin_throwables( used_traits: Vec::new(), }, ); + 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/src/types/checker/builtin_types/exception.rs b/src/types/checker/builtin_types/exception.rs index 33523e1c0b..e763e5de48 100644 --- a/src/types/checker/builtin_types/exception.rs +++ b/src/types/checker/builtin_types/exception.rs @@ -339,6 +339,7 @@ pub(crate) fn patch_builtin_exception_signatures(checker: &mut Checker) { "TypeError", "ValueError", "ArithmeticError", + "UnhandledMatchError", "Exception", "RuntimeException", "JsonException", diff --git a/tests/codegen/exceptions.rs b/tests/codegen/exceptions.rs index 58e17e9f99..608e87ac6b 100644 --- a/tests/codegen/exceptions.rs +++ b/tests/codegen/exceptions.rs @@ -624,3 +624,28 @@ try { echo $c->foo(); echo 'no'; } catch (Error $e) { echo 'err'; } ); assert_eq!(out, "err"); } + +/// UnhandledMatchError is a registered builtin Error subclass: newable with a message, +/// throwable from a `match` default arm, catchable by its fully-qualified name, and its +/// getMessage() round-trips — matching PHP 8's unhandled-match error type. +#[test] +fn test_unhandled_match_error_throw_and_catch() { + let out = compile_and_run( + r#" "neg", + $n === 0 => "zero", + default => throw new UnhandledMatchError("no arm for " . $n), + }; +} +echo classify(-1), "|", classify(0), "|"; +try { + classify(5); +} catch (\UnhandledMatchError $e) { + echo "caught:", $e->getMessage(); +} +"#, + ); + assert_eq!(out, "neg|zero|caught:no arm for 5"); +}