Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query: parse as AST #6788

Draft
wants to merge 35 commits into
base: v5/develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
5e1c65c
Add new query parsers / compilers / runners
rasteiner Nov 12, 2024
061cdc3
Merge branch 'getkirby:main' into v5/compiled-queries
rasteiner Nov 12, 2024
f23a29a
Fix missing imports of Exception
rasteiner Nov 12, 2024
ea6025c
Fix missing imports of Exception
rasteiner Nov 12, 2024
3ceab61
Merge branch 'v5/compiled-queries' of https://github.com/rasteiner/ki…
rasteiner Nov 12, 2024
54f66b4
query parser fixes
rasteiner Nov 12, 2024
fa2ed19
Implement `intercept`, rename AST classes with a "Node" suffix
rasteiner Nov 12, 2024
d1dba29
fix type annotations for arrays in CodeGen and Interpreter classes
rasteiner Nov 12, 2024
dd6eb99
rename parameter in run method from bindings to context to be fully c…
rasteiner Nov 12, 2024
d56e4d6
don't use option helper
rasteiner Nov 13, 2024
67aa780
Update token type constants to include "T_" prefix for safety
rasteiner Nov 13, 2024
f46ab0a
allow identifiers to start with a number and contain (escaped) dots
rasteiner Nov 13, 2024
c68b85f
coding style
rasteiner Nov 13, 2024
52d94fd
Remove unused imports in Query.php
rasteiner Nov 13, 2024
a512a8e
First set of CS fixes
distantnative Nov 13, 2024
2e9186a
Merge pull request #106 from rasteiner/queries/first-cs-cleanup
rasteiner Nov 13, 2024
d82b385
set "interpreted" as default query runner
rasteiner Nov 13, 2024
a25e37d
Add new query parsers / compilers / runners
rasteiner Nov 12, 2024
85dd0e0
Fix missing imports of Exception
rasteiner Nov 12, 2024
46b6755
query parser fixes
rasteiner Nov 12, 2024
9392a43
Implement `intercept`, rename AST classes with a "Node" suffix
rasteiner Nov 12, 2024
b599e66
fix type annotations for arrays in CodeGen and Interpreter classes
rasteiner Nov 12, 2024
90b1715
rename parameter in run method from bindings to context to be fully c…
rasteiner Nov 12, 2024
42fbd4f
don't use option helper
rasteiner Nov 13, 2024
96b8a41
Update token type constants to include "T_" prefix for safety
rasteiner Nov 13, 2024
0c8b8cd
allow identifiers to start with a number and contain (escaped) dots
rasteiner Nov 13, 2024
045867c
coding style
rasteiner Nov 13, 2024
f62695d
Remove unused imports in Query.php
rasteiner Nov 13, 2024
2bf1ffc
First set of CS fixes
distantnative Nov 13, 2024
738674e
set "interpreted" as default query runner
rasteiner Nov 13, 2024
c7da7b7
Merge branch 'v5/compiled-queries' of https://github.com/rasteiner/ki…
rasteiner Dec 3, 2024
a22d4ca
fix query: sequential member access was not resetting method arguments
rasteiner Dec 3, 2024
ae94934
code style
rasteiner Dec 3, 2024
d9d6e8d
only intercept accessed objects
rasteiner Dec 4, 2024
0026642
query fixes, more tests, inject cache as dependency
rasteiner Dec 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 35 additions & 11 deletions src/Query/Query.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Kirby\Query;

use Closure;
use Exception;
use Kirby\Cms\App;
use Kirby\Cms\Collection;
use Kirby\Cms\File;
Expand All @@ -11,6 +12,8 @@
use Kirby\Cms\User;
use Kirby\Image\QrCode;
use Kirby\Toolkit\I18n;
use Kirby\Toolkit\Query\Runners\Interpreted;
use Kirby\Toolkit\Query\Runners\Transpiled;

/**
* The Query class can be used to query arrays and objects,
Expand Down Expand Up @@ -38,6 +41,8 @@ class Query
*/
public static array $entries = [];

private static array $resolverCache = [];

/**
* Creates a new Query object
*/
Expand Down Expand Up @@ -66,18 +71,8 @@ public function intercept(mixed $result): mixed
return $result;
}

/**
* Returns the query result if anything
* can be found, otherwise returns null
*
* @throws \Kirby\Exception\BadMethodCallException If an invalid method is accessed by the query
*/
public function resolve(array|object $data = []): mixed
private function resolve_legacy(array|object $data = []): mixed
{
if (empty($this->query) === true) {
return $data;
}

// merge data with default entries
if (is_array($data) === true) {
$data = [...static::$entries, ...$data];
Expand All @@ -99,6 +94,35 @@ public function resolve(array|object $data = []): mixed

// loop through all segments to resolve query
return Expression::factory($this->query, $this)->resolve($data);

}

/**
* Returns the query result if anything
* can be found, otherwise returns null
*
* @throws \Kirby\Exception\BadMethodCallException If an invalid method is accessed by the query
*/
public function resolve(array|object $data = []): mixed
{
if (empty($this->query) === true) {
return $data;
}

$mode = App::instance()->option('query.runner', 'interpreted');

if ($mode === 'legacy') {
return $this->resolve_legacy($data);
}

$runnerClass = match($mode) {
'transpiled' => Transpiled::class,
'interpreted' => Interpreted::class,
default => throw new Exception('Invalid query runner')
};

$runner = new $runnerClass(static::$entries, $this->intercept(...), static::$resolverCache);
return $runner->run($this->query, (array)$data);
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/Toolkit/Query/AST/ArgumentListNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class ArgumentListNode extends Node
{
public function __construct(
public array $arguments,
) {
}
}
11 changes: 11 additions & 0 deletions src/Toolkit/Query/AST/ArrayListNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class ArrayListNode extends Node
{
public function __construct(
public array $elements,
) {
}
}
15 changes: 15 additions & 0 deletions src/Toolkit/Query/AST/ClosureNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class ClosureNode extends Node
{
/**
* @param string[] $arguments The arguments names
*/
public function __construct(
public array $arguments,
public Node $body,
) {
}
}
12 changes: 12 additions & 0 deletions src/Toolkit/Query/AST/CoalesceNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class CoalesceNode extends Node
{
public function __construct(
public Node $left,
public Node $right,
) {
}
}
20 changes: 20 additions & 0 deletions src/Toolkit/Query/AST/GlobalFunctionNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class GlobalFunctionNode extends IdentifierNode
{
public function __construct(
public string $name,
public ArgumentListNode $arguments,
) {
}

/**
* Replace escaped dots with real dots
*/
public function name(): string
{
return str_replace('\.', '.', $this->name);
}
}
14 changes: 14 additions & 0 deletions src/Toolkit/Query/AST/IdentifierNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Kirby\Toolkit\Query\AST;

abstract class IdentifierNode extends Node
{
/**
* Replaces the escaped identifier with the actual identifier
*/
public static function unescape(string $name): string
{
return stripslashes($name);
}
}
11 changes: 11 additions & 0 deletions src/Toolkit/Query/AST/LiteralNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class LiteralNode extends Node
{
public function __construct(
public mixed $value,
) {
}
}
26 changes: 26 additions & 0 deletions src/Toolkit/Query/AST/MemberAccessNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class MemberAccessNode extends IdentifierNode
{
public function __construct(
public Node $object,
public string|int $member,
public ArgumentListNode|null $arguments = null,
public bool $nullSafe = false,
) {
}

/**
* Returns the member name and replaces escaped dots with real dots if it's a string
*/
public function member(): string|int
{
if (is_string($this->member)) {
return self::unescape($this->member);
}
return $this->member;

}
}
13 changes: 13 additions & 0 deletions src/Toolkit/Query/AST/Node.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Kirby\Toolkit\Query\AST;

use Kirby\Toolkit\Query\Visitor;

class Node
{
public function accept(Visitor $visitor)
{
return $visitor->visitNode($this);
}
}
14 changes: 14 additions & 0 deletions src/Toolkit/Query/AST/TernaryNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class TernaryNode extends Node
{
public function __construct(
public Node $condition,
public Node|null $trueBranch,
public Node $falseBranch,
public bool $trueBranchIsDefault = false,
) {
}
}
19 changes: 19 additions & 0 deletions src/Toolkit/Query/AST/VariableNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

namespace Kirby\Toolkit\Query\AST;

class VariableNode extends IdentifierNode
{
public function __construct(
public string $name,
) {
}

/**
* Replaces escaped dots with real dots
*/
public function name(): string
{
return self::unescape($this->name);
}
}
90 changes: 90 additions & 0 deletions src/Toolkit/Query/BaseParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php

namespace Kirby\Toolkit\Query;

use Exception;
use Iterator;

abstract class BaseParser
{
protected Token|null $previous;
protected Token $current;

/**
* @var Iterator<Token>
*/
protected Iterator $tokens;


public function __construct(
Tokenizer|Iterator $source,
) {
if ($source instanceof Tokenizer) {
$source = $source->tokenize();
}

$this->tokens = $source;
$first = $this->tokens->current();

if ($first === null) {
throw new Exception('No tokens found.');
}

$this->current = $first;
}

protected function consume(TokenType $type, string $message): Token
{
if ($this->check($type) === true) {
return $this->advance();
}

throw new Exception($message);
}

protected function check(TokenType $type): bool
{
if ($this->isAtEnd() === true) {
return false;
}

return $this->current->type === $type;
}

protected function advance(): Token|null
{
if ($this->isAtEnd() === false) {
$this->previous = $this->current;
$this->tokens->next();
$this->current = $this->tokens->current();
}

return $this->previous;
}

protected function isAtEnd(): bool
{
return $this->current->type === TokenType::T_EOF;
}


protected function match(TokenType $type): Token|false
{
if ($this->check($type) === true) {
return $this->advance();
}

return false;
}

protected function matchAny(array $types): Token|false
{
foreach ($types as $type) {
if ($this->check($type) === true) {
return $this->advance();
}
}

return false;
}
}
Loading
Loading