Skip to content

Commit 0356257

Browse files
committed
feat: add optimizer and CommonExpressionLanguage public API
Signed-off-by: azjezz <azjezz@protonmail.com>
1 parent a8f3cd1 commit 0356257

File tree

17 files changed

+403
-43
lines changed

17 files changed

+403
-43
lines changed

README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,23 @@ const EXPRESSION = <<<CEL
3939
&& account.overdraftLimit >= transaction.withdrawal - account.balance)
4040
CEL;
4141

42+
43+
$configuration = new Cel\Runtime\Configuration(
44+
// You can customize the runtime configuration here
45+
);
46+
47+
$runtime = new Cel\Runtime\Runtime(configuration: $configuration);
48+
$cel = new Cel\CommonExpressionLanguage(runtime: $runtime);
49+
4250
try {
43-
$result = Cel\run(EXPRESSION, [
51+
// Parse the expression
52+
$expression = $cel->parseString(EXPRESSION);
53+
54+
// Perform optimizations on the expression, e.g. short-circuiting
55+
$expression = $cel->optimize($expression);
56+
57+
// Evaluate the expression with a given environment
58+
$result = $cel->run($expression, Cel\Runtime\Environment\Environment::fromArray([
4459
'account' => [
4560
'balance' => 500,
4661
'overdraftProtection' => true,
@@ -49,8 +64,8 @@ try {
4964
'transaction' => [
5065
'withdrawal' => 700,
5166
],
52-
]);
53-
67+
]));
68+
5469
IO\write_line('Result: %s(%s)', $result->getType(), var_export($result->getNativeValue(), true));
5570
} catch (Cel\Parser\Exception\ExceptionInterface $exception) {
5671
// Parsing failed...

config/mago.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ integrations = ["phpunit", "php-standard-library"]
1010

1111
[linter.rules]
1212
# Complexity 😖
13-
too-many-methods = { threshold = 20 }
13+
too-many-methods = { threshold = 25 }
1414
excessive-parameter-list = { threshold = 10 }
1515
halstead = { effort-threshold = 20_000, difficulty-threshold = 20, volume-threshold = 1_500 }
1616
kan-defect = { threshold = 5.0 }

examples/simple.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
CEL;
1414

1515
try {
16-
$result = Cel\run(EXPRESSION, [
16+
$result = Cel\evaluate(EXPRESSION, [
1717
'account' => [
1818
'balance' => 500,
1919
'overdraftProtection' => true,

src/CommonExpressionLanguage.php

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cel;
6+
7+
use Cel\Input\InputInterface;
8+
use Cel\Lexer\LexerInterface;
9+
use Cel\Optimizer\Optimization;
10+
use Cel\Optimizer\Optimizer;
11+
use Cel\Optimizer\OptimizerInterface;
12+
use Cel\Parser\Parser;
13+
use Cel\Parser\ParserInterface;
14+
use Cel\Runtime\Environment\EnvironmentInterface;
15+
use Cel\Runtime\Extension\ExtensionInterface;
16+
use Cel\Runtime\Runtime;
17+
use Cel\Runtime\RuntimeInterface;
18+
use Cel\Runtime\RuntimeReceipt;
19+
use Cel\Syntax\Expression;
20+
use Override;
21+
22+
final readonly class CommonExpressionLanguage implements ParserInterface, OptimizerInterface, RuntimeInterface
23+
{
24+
public function __construct(
25+
private ParserInterface $parser = new Parser(),
26+
private OptimizerInterface $optimizer = new Optimizer(),
27+
private RuntimeInterface $runtime = new Runtime(),
28+
) {}
29+
30+
/**
31+
* @inheritDoc
32+
*/
33+
#[Override]
34+
public static function default(): static
35+
{
36+
return new self(
37+
parser: Parser::default(),
38+
optimizer: Optimizer::default(),
39+
runtime: Runtime::default(),
40+
);
41+
}
42+
43+
/**
44+
* @inheritDoc
45+
*/
46+
#[Override]
47+
public function parseString(string $string): Expression
48+
{
49+
return $this->parser->parseString($string);
50+
}
51+
52+
/**
53+
* @inheritDoc
54+
*/
55+
#[Override]
56+
public function parse(InputInterface $input): Expression
57+
{
58+
return $this->parser->parse($input);
59+
}
60+
61+
/**
62+
* @inheritDoc
63+
*/
64+
#[Override]
65+
public function construct(LexerInterface $lexer): Expression
66+
{
67+
return $this->parser->construct($lexer);
68+
}
69+
70+
/**
71+
* @inheritDoc
72+
*/
73+
#[Override]
74+
public function addOptimization(Optimization\OptimizationInterface $optimization): void
75+
{
76+
$this->optimizer->addOptimization($optimization);
77+
}
78+
79+
/**
80+
* @inheritDoc
81+
*/
82+
#[Override]
83+
public function optimize(Expression $expression): Expression
84+
{
85+
return $this->optimizer->optimize($expression);
86+
}
87+
88+
/**
89+
* @inheritDoc
90+
*/
91+
#[Override]
92+
public function register(ExtensionInterface $extension): void
93+
{
94+
$this->runtime->register($extension);
95+
}
96+
97+
/**
98+
* @inheritDoc
99+
*/
100+
#[Override]
101+
public function run(Expression $expression, EnvironmentInterface $environment): RuntimeReceipt
102+
{
103+
return $this->runtime->run($expression, $environment);
104+
}
105+
}

src/Optimizer/Optimizer.php

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cel\Optimizer;
6+
7+
use Cel\Syntax\Aggregate\FieldInitializerNode;
8+
use Cel\Syntax\Aggregate\ListExpression;
9+
use Cel\Syntax\Aggregate\MapEntryNode;
10+
use Cel\Syntax\Aggregate\MapExpression;
11+
use Cel\Syntax\Aggregate\MessageExpression;
12+
use Cel\Syntax\Binary\BinaryExpression;
13+
use Cel\Syntax\ConditionalExpression;
14+
use Cel\Syntax\Expression;
15+
use Cel\Syntax\Member\CallExpression;
16+
use Cel\Syntax\Member\IndexExpression;
17+
use Cel\Syntax\Member\MemberAccessExpression;
18+
use Cel\Syntax\PunctuatedSequence;
19+
use Cel\Syntax\Unary\UnaryExpression;
20+
use Override;
21+
22+
final class Optimizer implements OptimizerInterface
23+
{
24+
/**
25+
* @var list<Optimization\OptimizationInterface>
26+
*/
27+
private array $optimizations;
28+
29+
/**
30+
* @param list<Optimization\OptimizationInterface> $optimizations Initial list of optimizations to apply.
31+
*/
32+
public function __construct(array $optimizations = [])
33+
{
34+
$this->optimizations = $optimizations;
35+
}
36+
37+
#[Override]
38+
public static function default(): static
39+
{
40+
return new self([
41+
new Optimization\ShortCircuitBooleanOptimization(),
42+
new Optimization\UnwrapParenthesesOptimization(),
43+
]);
44+
}
45+
46+
/**
47+
* @inheritDoc
48+
*/
49+
#[Override]
50+
public function addOptimization(Optimization\OptimizationInterface $optimization): void
51+
{
52+
$this->optimizations[] = $optimization;
53+
}
54+
55+
/**
56+
* @inheritDoc
57+
*/
58+
#[Override]
59+
public function optimize(Expression $expression): Expression
60+
{
61+
if ($expression instanceof ListExpression) {
62+
$optimizedElements = [];
63+
foreach ($expression->elements->elements as $element) {
64+
$optimizedElements[] = $this->optimize($element);
65+
}
66+
67+
$expression = new ListExpression(
68+
$expression->openingBracket,
69+
new PunctuatedSequence($optimizedElements, $expression->elements->commas),
70+
$expression->closingBracket,
71+
);
72+
}
73+
74+
if ($expression instanceof MapExpression) {
75+
$optimizedEntries = [];
76+
foreach ($expression->entries->elements as $entry) {
77+
$optimizedKey = $this->optimize($entry->key);
78+
$optimizedValue = $this->optimize($entry->value);
79+
80+
$optimizedEntries[] = new MapEntryNode($optimizedKey, $entry->colon, $optimizedValue);
81+
}
82+
83+
$expression = new MapExpression(
84+
$expression->openingBrace,
85+
new PunctuatedSequence($optimizedEntries, $expression->entries->commas),
86+
$expression->closingBrace,
87+
);
88+
}
89+
90+
if ($expression instanceof UnaryExpression) {
91+
$optimizedOperand = $this->optimize($expression->operand);
92+
93+
$expression = new UnaryExpression($expression->operator, $optimizedOperand);
94+
}
95+
96+
if ($expression instanceof BinaryExpression) {
97+
$optimizedLeft = $this->optimize($expression->left);
98+
$optimizedRight = $this->optimize($expression->right);
99+
100+
$expression = new BinaryExpression($optimizedLeft, $expression->operator, $optimizedRight);
101+
}
102+
103+
if ($expression instanceof ConditionalExpression) {
104+
$optimizedCondition = $this->optimize($expression->condition);
105+
$optimizedThen = $this->optimize($expression->then);
106+
$optimizedElse = $this->optimize($expression->else);
107+
108+
$expression = new ConditionalExpression(
109+
$optimizedCondition,
110+
$expression->question,
111+
$optimizedThen,
112+
$expression->colon,
113+
$optimizedElse,
114+
);
115+
}
116+
117+
if ($expression instanceof MemberAccessExpression) {
118+
$optimizedOperand = $this->optimize($expression->operand);
119+
120+
$expression = new MemberAccessExpression($optimizedOperand, $expression->dot, $expression->field);
121+
}
122+
123+
if ($expression instanceof IndexExpression) {
124+
$optimizedOperand = $this->optimize($expression->operand);
125+
$optimizedIndex = $this->optimize($expression->index);
126+
127+
$expression = new IndexExpression(
128+
$optimizedOperand,
129+
$expression->openingBracket,
130+
$optimizedIndex,
131+
$expression->closingBracket,
132+
);
133+
}
134+
135+
if ($expression instanceof CallExpression) {
136+
$optimizedTarget = null;
137+
if (null !== $expression->target) {
138+
$optimizedTarget = $this->optimize($expression->target);
139+
}
140+
141+
$optimizedArguments = [];
142+
foreach ($expression->arguments->elements as $argument) {
143+
$optimizedArguments[] = $this->optimize($argument);
144+
}
145+
146+
$expression = new CallExpression(
147+
$optimizedTarget,
148+
$expression->targetSeparator,
149+
$expression->function,
150+
$expression->openingParenthesis,
151+
new PunctuatedSequence($optimizedArguments, $expression->arguments->commas),
152+
$expression->closingParenthesis,
153+
);
154+
}
155+
156+
if ($expression instanceof MessageExpression) {
157+
$initializers = [];
158+
foreach ($expression->initializers as $initializer) {
159+
$optimizedInitializerValue = $this->optimize($initializer->value);
160+
161+
$initializers[] = new FieldInitializerNode(
162+
$initializer->field,
163+
$initializer->colon,
164+
$optimizedInitializerValue,
165+
);
166+
}
167+
168+
$expression = new MessageExpression(
169+
$expression->dot,
170+
$expression->selector,
171+
$expression->followingSelectors,
172+
$expression->openingBrace,
173+
new PunctuatedSequence($initializers, $expression->initializers->commas),
174+
$expression->closingBrace,
175+
);
176+
}
177+
178+
foreach ($this->optimizations as $optimization) {
179+
$optimized = $optimization->apply($expression);
180+
if ($optimized !== null && $optimized !== $expression) { // Guard against no-op optimizations that return the same instance
181+
return $this->optimize($optimized);
182+
}
183+
}
184+
185+
return $expression;
186+
}
187+
}

src/Optimizer/OptimizerInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
namespace Cel\Optimizer;
66

77
use Cel\Syntax\Expression;
8+
use Psl\Default\DefaultInterface;
89

910
/**
1011
* Interface for expression optimizers.
1112
*/
12-
interface OptimizerInterface
13+
interface OptimizerInterface extends DefaultInterface
1314
{
1415
/**
1516
* Adds an optimization pass to the optimizer.

src/Parser/Parser.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ final class Parser implements ParserInterface
4949

5050
private TokenStream $stream;
5151

52+
/**
53+
* @inheritDoc
54+
*/
55+
#[Override]
56+
public static function default(): static
57+
{
58+
return new self();
59+
}
60+
5261
/**
5362
* @param LexerInterface $lexer
5463
*

src/Parser/ParserInterface.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
use Cel\Parser\Exception\UnexpectedEndOfFileException;
88
use Cel\Parser\Exception\UnexpectedTokenException;
99
use Cel\Syntax\Expression;
10+
use Psl\Default\DefaultInterface;
1011

1112
/**
1213
* Defines the contract for a parser, which consumes a stream of tokens from a lexer
1314
* and produces a syntax tree.
1415
*/
15-
interface ParserInterface
16+
interface ParserInterface extends DefaultInterface
1617
{
1718
/**
1819
* Parses the string and produces a syntax tree.

0 commit comments

Comments
 (0)