Skip to content

Instantly share code, notes, and snippets.

@thekid
Created December 27, 2025 16:27
Show Gist options
  • Select an option

  • Save thekid/6f6798bc05995caac6b7e825d6d5a76f to your computer and use it in GitHub Desktop.

Select an option

Save thekid/6f6798bc05995caac6b7e825d6d5a76f to your computer and use it in GitHub Desktop.
Catch with is operator
diff --git a/src/main/php/lang/ast/syntax/php/IsOperator.class.php b/src/main/php/lang/ast/syntax/php/IsOperator.class.php
index 7e34fc1..f100d10 100755
--- a/src/main/php/lang/ast/syntax/php/IsOperator.class.php
+++ b/src/main/php/lang/ast/syntax/php/IsOperator.class.php
@@ -6,6 +6,8 @@ use lang\ast\nodes\{
Assignment,
BinaryExpression,
Braced,
+ CatchStatement,
+ IfStatement,
InstanceExpression,
InstanceOfExpression,
InvokeExpression,
@@ -14,11 +16,13 @@ use lang\ast\nodes\{
MatchExpression,
OffsetExpression,
ScopeExpression,
+ ThrowExpression,
+ TryStatement,
Variable
};
use lang\ast\syntax\Extension;
use lang\ast\types\{IsArray, IsLiteral, IsFunction, IsMap, IsUnion, IsIntersection, IsNullable, IsValue};
-use lang\ast\{Node, Token};
+use lang\ast\{Node, Type, Token};
class IsOperator implements Extension {
@@ -170,6 +174,69 @@ class IsOperator implements Extension {
return new MatchExpression($patterns ? null : $condition, $cases, $default, $token->line);
});
+ $language->stmt('try', function($parse, $token) use($pattern) {
+ $parse->expecting('{', 'try');
+ $statements= $this->statements($parse);
+ $parse->expecting('}', 'try');
+
+ $catches= [];
+ while ('catch' === $parse->token->value) {
+ $parse->forward();
+
+ $parse->expecting('(', 'catch');
+ if ('name' === $parse->token->kind) {
+
+ // catch (Exception $e), catch (A|B $e)
+ // catch (Exception)
+ $types= new IsCompound([], '|');
+ do {
+ $types->patterns[]= new IsValue($parse->scope->resolve($parse->token->value));
+ $parse->forward();
+ } while ('|' === $parse->token->value && $parse->forward() | true);
+
+ if ('variable' === $parse->token->kind) {
+ $parse->forward();
+ $variable= $parse->token->value;
+ $parse->forward();
+ } else {
+ $variable= null;
+ }
+ } else if ('variable' === $parse->token->kind) {
+
+ // catch ($e)
+ // catch ($e is Exception)
+ // catch ($e is Exception|Error)
+ // catch ($e is Exception(message: $message))
+ $parse->forward();
+ $variable= $parse->token->value;
+ $parse->forward();
+
+ if ('is' === $parse->token->value) {
+ $parse->forward();
+ $types= $pattern($parse, $this);
+ } else {
+ $types= null;
+ }
+ }
+ $parse->expecting(')', 'catch');
+
+ $parse->expecting('{', 'catch');
+ $catches[]= new CatchStatement($types, $variable, $this->statements($parse), $parse->token->line);
+ $parse->expecting('}', 'catch');
+ }
+
+ if ('finally' === $parse->token->value) {
+ $parse->forward();
+ $parse->expecting('{', 'finally');
+ $finally= $this->statements($parse);
+ $parse->expecting('}', 'finally');
+ } else {
+ $finally= null;
+ }
+
+ return new TryStatement($statements, $catches, $finally, $token->line);
+ });
+
$match= function($codegen, $expression, $pattern) use(&$match) {
// Basic type matching, literal comparison and variable binding
@@ -269,5 +336,34 @@ class IsOperator implements Extension {
$emitter->transform('is', function($codegen, $node) use($match) {
return $match($codegen, $node->expression, $node->pattern);
});
+
+ $emitter->transform('try', function($codegen, $node) use($match) {
+ if (empty($node->catches)) return $node;
+
+ // Rewrite catches into if/else cascade
+ $bind= new Variable($codegen->symbol());
+ $catches= [new CatchStatement(null, $bind->pointer, [], $node->catches[0]->line)];
+ $cascade= &$catches[0]->body;
+
+ foreach ($node->catches as $catch) {
+ $handle= new IfStatement(
+ $catch->types ? $match($codegen, $bind, $catch->types) : new Literal('1'),
+ $catch->variable
+ ? [new Assignment(new Variable($catch->variable), '=', $bind), ...$catch->body]
+ : $catch->body
+ ,
+ []
+ );
+
+ if ($cascade) {
+ $cascade[0]->otherwise= [$handle];
+ $cascade= &$handle->otherwise;
+ } else {
+ $cascade= [$handle];
+ }
+ }
+
+ return [new TryStatement($node->body, $catches, $node->finally)];
+ });
}
}
\ No newline at end of file
<?php namespace lang\ast\syntax\php\unittest;
use lang\ast\unittest\emit\EmittingTest;
use test\{Assert, Test, Values};
class CatchTest extends EmittingTest {
#[Test, Values(['$e', 'IllegalAccessException $e', 'IllegalAccessException|Error $e'])]
public function default_try_with($catch) {
Assert::equals('Test', $this->run('use lang\\IllegalAccessException; class %T {
public function run() {
try {
throw new IllegalAccessException("Test");
} catch ('.$catch.') {
return $e->getMessage();
}
}
}'));
}
#[Test]
public function try_without_variable() {
Assert::equals('Caught', $this->run('use lang\\IllegalAccessException; class %T {
public function run() {
try {
throw new IllegalAccessException("Test");
} catch (IllegalAccessException) {
return "Caught";
}
}
}'));
}
#[Test, Values(['IllegalAccessException', 'IllegalAccessException|Error'])]
public function try_with_pattern_with($type) {
Assert::equals('Test', $this->run('use lang\\IllegalAccessException; class %T {
public function run() {
try {
throw new IllegalAccessException("Test");
} catch ($e is '.$type.') {
return $e->getMessage();
}
}
}'));
}
#[Test]
public function try_with_pattern_binding() {
Assert::equals('Test', $this->run('use lang\\IllegalAccessException; class %T {
public function run() {
try {
throw new IllegalAccessException("Test");
} catch ($e is IllegalAccessException(message: $message)) {
return $message;
}
}
}'));
}
#[Test, Values([[404, 'Client error'], [503, 'Server error'], [600, 'Unknown error 600']])]
public function try_with_pattern_matching($status, $expected) {
Assert::equals($expected, $this->run('use lang\\ast\\syntax\\php\\unittest\\Error; class %T {
public function run($status) {
try {
throw new Error($status);
} catch ($e is Error(status: >=400 & <500)) {
return "Client error";
} catch ($e is Error(status: >=500 & <600)) {
return "Server error";
} catch ($e is Error) {
return "Unknown error ".$e->status;
}
}
}', $status));
}
#[Test]
public function try_with_unmatched_pattern() {
Assert::equals('Uncaught', $this->run('use lang\\IllegalAccessException; class %T {
public function run() {
try {
throw new IllegalAccessException("Test");
} catch ($e is IllegalAccessException(message: "Database")|Error(status: 500)) {
return "Internal server error";
} catch ($e) {
return "Uncaught";
}
}
}'));
}
}
<?php namespace lang\ast\syntax\php\unittest;
use lang\Throwable;
class Error extends Throwable {
public $status;
public function __construct($status, $message= 'Error') {
parent::__construct('Status #'.$status.': '.$message);
$this->status= $status;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment