From 43a288c138f36c6647eae3ac41e0bccefad63e44 Mon Sep 17 00:00:00 2001 From: Rudolph Gottesheim Date: Thu, 6 Feb 2025 18:39:11 +0100 Subject: [PATCH] Introduce value declaration statements --- src/Parser/Declarations.php | 44 +++++++++++++++--- src/Parser/ExpressionParser.php | 52 +++++++++++++++++++++- src/Parser/Token.php | 1 + src/Parser/Tokenizer.php | 3 ++ src/ValueDeclaration.php | 12 +++++ tests/unit/ExpressionTest.php | 19 ++++++++ tests/unit/Parser/ExpressionParserTest.php | 8 +++- 7 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 src/ValueDeclaration.php diff --git a/src/Parser/Declarations.php b/src/Parser/Declarations.php index ba9f415..e054256 100644 --- a/src/Parser/Declarations.php +++ b/src/Parser/Declarations.php @@ -5,11 +5,15 @@ namespace Eventjet\Ausdruck\Parser; use Eventjet\Ausdruck\Type; +use Eventjet\Ausdruck\ValueDeclaration; use InvalidArgumentException; +use function array_filter; use function array_key_exists; use function sprintf; +use const ARRAY_FILTER_USE_KEY; + final class Declarations { /** @var array */ @@ -24,7 +28,22 @@ public function __construct( public readonly array $variables = [], array $functions = [], ) { - $fns = [ + $fns = self::builtInFunctions(); + foreach ($functions as $name => $type) { + if (array_key_exists($name, $fns)) { + throw new InvalidArgumentException(sprintf('Can\'t override built-in function %s', $name)); + } + $fns[$name] = $type; + } + $this->functions = $fns; + } + + /** + * @return array + */ + private static function builtInFunctions(): array + { + return [ 'contains' => Type::func(Type::bool(), [Type::listOf(Type::any()), Type::any()]), 'count' => Type::func(Type::int(), [Type::listOf(Type::any())]), // Can't declare filter until we have generics @@ -39,12 +58,23 @@ public function __construct( // Can't declare unwrap until we have generics // 'unwrap' => Type::func(Type::some(Type::any()), [Type::option(Type::any())]), ]; - foreach ($functions as $name => $type) { - if (array_key_exists($name, $fns)) { - throw new InvalidArgumentException(sprintf('Can\'t override built-in function %s', $name)); - } - $fns[$name] = $type; + } + + /** + * @param list $declarations + */ + public function withAddedDeclarations(array $declarations): self + { + $newValues = $this->variables; + foreach ($declarations as $declaration) { + $newValues[$declaration->name] = $declaration->type; } - $this->functions = $fns; + $builtInFunctions = self::builtInFunctions(); + $functions = array_filter( + $this->functions, + static fn($name) => !array_key_exists($name, $builtInFunctions), + ARRAY_FILTER_USE_KEY, + ); + return new self($this->types, $newValues, $functions); } } diff --git a/src/Parser/ExpressionParser.php b/src/Parser/ExpressionParser.php index 82b2c67..b3b945d 100644 --- a/src/Parser/ExpressionParser.php +++ b/src/Parser/ExpressionParser.php @@ -12,6 +12,7 @@ use Eventjet\Ausdruck\ListLiteral; use Eventjet\Ausdruck\StructLiteral; use Eventjet\Ausdruck\Type; +use Eventjet\Ausdruck\ValueDeclaration; use function array_shift; use function assert; @@ -49,7 +50,7 @@ public static function parse(string $expression, Declarations|Types|null $types * string literals and identifiers are just put back together. If you encounter a case where it does matter, * just change it to mb_str_split and add an appropriate test case. */ - return (new self(new Peekable(Tokenizer::tokenize($chars)), $declarations))->parseExpression(); + return (new self(new Peekable(Tokenizer::tokenize($chars)), $declarations))->parseStatements(); } public static function parseTyped(string $expression, Type $type, Declarations|Types|null $types = null): Expression @@ -87,6 +88,52 @@ private static function fieldAccess(Expression $target, string $name, Span $loca return Expr::fieldAccess($target, $name, $location); } + private function parseStatements(): Expression + { + $declarations = []; + while (true) { + $declaration = $this->parseDeclaration(); + if ($declaration === null) { + break; + } + $declarations[] = $declaration; + } + $this->declarations = $this->declarations->withAddedDeclarations($declarations); + return $this->parseExpression(); + } + + private function parseDeclaration(): ValueDeclaration|null + { + $token = $this->tokens->peek()?->token; + if ($token === 'val') { + return $this->parseValueDeclaration(); + } + return null; + } + + private function parseValueDeclaration(): ValueDeclaration + { + $valToken = $this->tokens->next(); + assert($valToken !== null); + [$name] = $this->expectIdentifier($valToken, 'value name'); + $this->expect(Token::Equals); + $typeNode = TypeParser::parse($this->tokens); + if ($typeNode === null) { + throw SyntaxError::create('Expected type, got end of input', $this->nextSpan()); + } + if ($typeNode instanceof ParsedToken) { + throw SyntaxError::create( + sprintf('Expected type, got %s', Token::print($typeNode->token)), + $typeNode->location(), + ); + } + $type = $this->declarations->types->resolve($typeNode); + if ($type instanceof TypeError) { + throw $type; + } + return new ValueDeclaration($name, $type); + } + private function parseExpression(): Expression { /** @var Expression | null $expr */ @@ -117,6 +164,9 @@ private function parseLazy(Expression|null $left): Expression|null return null; } $token = $parsedToken->token; + if ($token === Token::Equals) { + self::unexpectedToken($parsedToken); + } if ($token === Token::Dot) { if ($left === null) { self::unexpectedToken($parsedToken); diff --git a/src/Parser/Token.php b/src/Parser/Token.php index 406a645..4ff6950 100644 --- a/src/Parser/Token.php +++ b/src/Parser/Token.php @@ -28,6 +28,7 @@ enum Token: string case Colon = ':'; case Minus = '-'; case Arrow = '->'; + case Equals = '='; /** * @param Token | string | Literal $token diff --git a/src/Parser/Tokenizer.php b/src/Parser/Tokenizer.php index 8eb96bc..b45c7a4 100644 --- a/src/Parser/Tokenizer.php +++ b/src/Parser/Tokenizer.php @@ -164,6 +164,9 @@ private static function equals(Peekable $chars, int $line, int &$column): Token { $chars->next(); $column++; + if ($chars->peek() !== '=') { + return Token::Equals; + } self::expect($chars, '==', $line, $column); return Token::TripleEquals; } diff --git a/src/ValueDeclaration.php b/src/ValueDeclaration.php new file mode 100644 index 0000000..7850c93 --- /dev/null +++ b/src/ValueDeclaration.php @@ -0,0 +1,12 @@ + ['foo', null, 'bar']]), ['foo', 'bar'], ], + [ + << 42]), + 42, + ], + [ + << 'a', 'bar' => 'b']), + false, + ], ]; foreach ($cases as $tuple) { [$expr, $scope, $expected] = $tuple; diff --git a/tests/unit/Parser/ExpressionParserTest.php b/tests/unit/Parser/ExpressionParserTest.php index 2865c80..3d2ee86 100644 --- a/tests/unit/Parser/ExpressionParserTest.php +++ b/tests/unit/Parser/ExpressionParserTest.php @@ -102,7 +102,7 @@ public static function invalidSyntaxExpressions(): iterable yield 'double equals' => ['foo:string == bar:string']; yield 'double length fat arrow' => ['foo:string ==> bar:string']; yield 'end after single pipe' => ['foo:bool |']; - yield 'end after single equals' => ['foo:bool =', 'Expected ==, got end of input']; + yield 'end after single equals' => ['foo:bool =', 'Unexpected =']; yield 'end after double equals' => ['foo:bool ==']; yield 'close brace after triple equals' => ['foo:bool === )']; yield 'lambda: missing closing brace' => ['(foo, bar => foo:string']; @@ -149,6 +149,11 @@ public static function invalidSyntaxExpressions(): iterable yield 'non-token, non-identifier symbol' => ['foo:bool € bar:bool']; yield 'identifier starting with a number' => ['42foo:bool', 'Unexpected identifier foo']; yield 'identifier starting with an underscore' => ['_foo:bool', 'Unexpected character _']; + yield 'End of input after val keyword' => ['val', 'Expected value name, got end of input']; + yield 'End of input after val name' => ['val foo', 'Expected =, got end of input']; + yield 'End of input after val equals' => ['val foo =', 'Expected type, got end of input']; + yield 'No Expression after val statement' => ['val foo = int', 'Expected expression, got end of input']; + yield 'Not a type after val equals' => ["val foo = :\nfoo", 'Expected type, got :']; } /** @@ -246,6 +251,7 @@ public static function typeErrorExpressions(): iterable 'Unknown field "age" on type { name: string }', ]; yield 'field access on string' => ['foo:string.age', 'Can\'t access field "age" on non-struct type string']; + yield 'val statement with unknown type' => ["val foo = huh\nfoo", 'Unknown type huh']; } /**