initial
This commit is contained in:
+140
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A parser grammar.
|
||||
*
|
||||
* This class represents a parser grammar that can be consumed by WP_Parser.
|
||||
* It loads a compressed grammar from a PHP array, inflates it to an internal
|
||||
* representation, and precomputes a lookup table for quick branch selection.
|
||||
*
|
||||
* @TODO: Add more details about the grammar implementation.
|
||||
*/
|
||||
class WP_Parser_Grammar {
|
||||
/**
|
||||
* ID for a special grammar rule that represents an empty "ε" (epsilon) rule.
|
||||
*
|
||||
* An "ε" rule in a grammar is a rule that matches an empty input of 0 bytes.
|
||||
* It can be used to represent optional grammar productions, and it is helpful
|
||||
* for expanding 0-or-1, 0-or-more, and 1-or-more quantifiers into simple rules.
|
||||
*
|
||||
* @TODO Investigate whether we can prevent possible conflict with a token ID.
|
||||
* The MySQL grammar doesn't define a token with ID "0", but generally
|
||||
* token IDs are not guaranteed to always satisfy this condition.
|
||||
*/
|
||||
const EMPTY_RULE_ID = 0;
|
||||
|
||||
/**
|
||||
* @TODO: Review and document these properties and their visibility.
|
||||
*/
|
||||
public $rules;
|
||||
public $rule_names;
|
||||
public $fragment_ids;
|
||||
public $lookahead_is_match_possible = array();
|
||||
public $lowest_non_terminal_id;
|
||||
public $highest_terminal_id;
|
||||
public $native_grammar;
|
||||
|
||||
public function __construct( array $rules ) {
|
||||
$this->inflate( $rules );
|
||||
}
|
||||
|
||||
public function get_rule_name( $rule_id ) {
|
||||
return $this->rule_names[ $rule_id ];
|
||||
}
|
||||
|
||||
public function get_rule_id( $rule_name ) {
|
||||
return array_search( $rule_name, $this->rule_names, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Inflate the grammar to an internal representation optimized for parsing.
|
||||
*
|
||||
* The input grammar is a compressed PHP array to minimize the file size.
|
||||
* Every rule and token in the compressed grammar is encoded as an integer.
|
||||
*/
|
||||
private function inflate( $grammar ) {
|
||||
$this->lowest_non_terminal_id = $grammar['rules_offset'];
|
||||
$this->highest_terminal_id = $this->lowest_non_terminal_id - 1;
|
||||
|
||||
foreach ( $grammar['rules_names'] as $rule_index => $rule_name ) {
|
||||
$this->rule_names[ $rule_index + $grammar['rules_offset'] ] = $rule_name;
|
||||
$this->rules[ $rule_index + $grammar['rules_offset'] ] = array();
|
||||
|
||||
/**
|
||||
* Treat all intermediate rules as fragments to inline before returning
|
||||
* the final parse tree to the API consumer.
|
||||
*
|
||||
* The original grammar was too difficult to parse with rules like:
|
||||
*
|
||||
* query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF))
|
||||
*
|
||||
* We've factored rule fragments, such as `EOF?`, into separate rules, such as `%EOF_zero_or_one`.
|
||||
* This is super useful for parsing, but it limits the API consumer's ability to
|
||||
* reason about the parse tree.
|
||||
*
|
||||
* Fragments are intermediate rules that are not part of the original grammar.
|
||||
* They are prefixed with a "%" to be distinguished from the original rules.
|
||||
*/
|
||||
if ( '%' === $rule_name[0] ) {
|
||||
$this->fragment_ids[ $rule_index + $grammar['rules_offset'] ] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->rules = array();
|
||||
foreach ( $grammar['grammar'] as $rule_index => $branches ) {
|
||||
$rule_id = $rule_index + $grammar['rules_offset'];
|
||||
$this->rules[ $rule_id ] = $branches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a rule => [token => true] lookup table for each rule
|
||||
* that starts with a terminal OR with another rule that already
|
||||
* has a lookahead mapping.
|
||||
*
|
||||
* This is similar to left-factoring the grammar, even if not quite
|
||||
* the same.
|
||||
*
|
||||
* This enables us to quickly bail out from checking branches that
|
||||
* cannot possibly match the current token. This increased the parser
|
||||
* speed by a whopping 80%!
|
||||
*
|
||||
* @TODO: Explore these possible next steps:
|
||||
*
|
||||
* * Compute a rule => [token => branch[]] list lookup table and only
|
||||
* process the branches that have a chance of matching the current token.
|
||||
* * Actually left-factor the grammar as much as possible. This, however,
|
||||
* could inflate the serialized grammar size.
|
||||
*/
|
||||
// 5 iterations seem to give us all the speed gains we can get from this.
|
||||
for ( $i = 0; $i < 5; $i++ ) {
|
||||
foreach ( $grammar['grammar'] as $rule_index => $branches ) {
|
||||
$rule_id = $rule_index + $grammar['rules_offset'];
|
||||
if ( isset( $this->lookahead_is_match_possible[ $rule_id ] ) ) {
|
||||
continue;
|
||||
}
|
||||
$rule_lookup = array();
|
||||
$first_symbol_can_be_expanded_to_all_terminals = true;
|
||||
foreach ( $branches as $branch ) {
|
||||
$terminals = false;
|
||||
$branch_starts_with_terminal = $branch[0] < $this->lowest_non_terminal_id;
|
||||
if ( $branch_starts_with_terminal ) {
|
||||
$terminals = array( $branch[0] );
|
||||
} elseif ( isset( $this->lookahead_is_match_possible[ $branch[0] ] ) ) {
|
||||
$terminals = array_keys( $this->lookahead_is_match_possible[ $branch[0] ] );
|
||||
}
|
||||
|
||||
if ( false === $terminals ) {
|
||||
$first_symbol_can_be_expanded_to_all_terminals = false;
|
||||
break;
|
||||
}
|
||||
foreach ( $terminals as $terminal ) {
|
||||
$rule_lookup[ $terminal ] = true;
|
||||
}
|
||||
}
|
||||
if ( $first_symbol_can_be_expanded_to_all_terminals ) {
|
||||
$this->lookahead_is_match_possible[ $rule_id ] = $rule_lookup;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+384
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A node in parse tree.
|
||||
*
|
||||
* This class represents a node in the parse tree that is produced by WP_Parser.
|
||||
* A node corresponds to the related grammar rule that was matched by the parser.
|
||||
* Each node can contain children, consisting of other nodes and grammar tokens.
|
||||
* In this way, a parser node constitutes a recursive structure that represents
|
||||
* a parse (sub)tree at each level of the full grammar tree.
|
||||
*/
|
||||
class WP_Parser_Node {
|
||||
/**
|
||||
* @TODO: Review and document these properties and their visibility.
|
||||
*/
|
||||
public $rule_id;
|
||||
public $rule_name;
|
||||
protected $children = array();
|
||||
|
||||
public function __construct( $rule_id, $rule_name ) {
|
||||
$this->rule_id = $rule_id;
|
||||
$this->rule_name = $rule_name;
|
||||
}
|
||||
|
||||
public function append_child( $node ) {
|
||||
$this->children[] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the matched rule fragments as if their children were direct
|
||||
* descendants of the current rule.
|
||||
*
|
||||
* What are rule fragments?
|
||||
*
|
||||
* When we initially parse the grammar file, it has compound rules such
|
||||
* as this one:
|
||||
*
|
||||
* query ::= EOF | ((simpleStatement | beginWork) ((SEMICOLON_SYMBOL EOF?) | EOF))
|
||||
*
|
||||
* Building a parser that can understand such rules is way more complex than building
|
||||
* a parser that only follows simple rules, so we flatten those compound rules into
|
||||
* simpler ones. The above rule would be flattened to:
|
||||
*
|
||||
* query ::= EOF | %query0
|
||||
* %query0 ::= %%query01 %%query02
|
||||
* %%query01 ::= simpleStatement | beginWork
|
||||
* %%query02 ::= SEMICOLON_SYMBOL EOF_zero_or_one | EOF
|
||||
* EOF_zero_or_one ::= EOF | ε
|
||||
*
|
||||
* This factorization happens in "convert-grammar.php".
|
||||
*
|
||||
* "Fragments" are intermediate artifacts whose names are not in the original grammar.
|
||||
* They are extremely useful for the parser, but the API consumer should never have to
|
||||
* worry about them. Fragment names start with a percent sign ("%").
|
||||
*
|
||||
* The code below inlines every fragment back in its parent rule.
|
||||
*
|
||||
* We could optimize this. The current $match may be discarded later on so any inlining
|
||||
* effort here would be wasted. However, inlining seems cheap and doing it bottom-up here
|
||||
* is **much** easier than reprocessing the parse tree top-down later on.
|
||||
*
|
||||
* The following parse tree:
|
||||
*
|
||||
* [
|
||||
* 'query' => [
|
||||
* [
|
||||
* '%query01' => [
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ],
|
||||
* '%query02' => [
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ]
|
||||
* ],
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*
|
||||
* Would be inlined as:
|
||||
*
|
||||
* [
|
||||
* 'query' => [
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ]
|
||||
* ],
|
||||
* [
|
||||
* 'simpleStatement' => [
|
||||
* MySQLToken(MySQLLexer::WITH_SYMBOL, 'WITH')
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
public function merge_fragment( $node ) {
|
||||
$this->children = array_merge( $this->children, $node->children );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this node has any child nodes or tokens.
|
||||
*
|
||||
* @return bool True if this node has any child nodes or tokens, false otherwise.
|
||||
*/
|
||||
public function has_child(): bool {
|
||||
return count( $this->children ) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this node has any child nodes.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return bool True if any child nodes are found, false otherwise.
|
||||
*/
|
||||
public function has_child_node( ?string $rule_name = null ): bool {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Node
|
||||
&& ( null === $rule_name || $child->rule_name === $rule_name )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this node has any child tokens.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return bool True if any child tokens are found, false otherwise.
|
||||
*/
|
||||
public function has_child_token( ?int $token_id = null ): bool {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Token
|
||||
&& ( null === $token_id || $child->id === $token_id )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child node or token of this node.
|
||||
*
|
||||
* @return WP_Parser_Node|WP_Parser_Token|null The first child node or token;
|
||||
* null when no children are found.
|
||||
*/
|
||||
public function get_first_child() {
|
||||
return $this->children[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child node of this node.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node|null The first matching child node; null when no children are found.
|
||||
*/
|
||||
public function get_first_child_node( ?string $rule_name = null ): ?WP_Parser_Node {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Node
|
||||
&& ( null === $rule_name || $child->rule_name === $rule_name )
|
||||
) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first child token of this node.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token|null The first matching child token; null when no children are found.
|
||||
*/
|
||||
public function get_first_child_token( ?int $token_id = null ): ?WP_Parser_Token {
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Token
|
||||
&& ( null === $token_id || $child->id === $token_id )
|
||||
) {
|
||||
return $child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first descendant node of this node.
|
||||
*
|
||||
* The node children are traversed recursively in a depth-first order until
|
||||
* a matching descendant node is found, or the entire subtree is searched.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node|null The first matching descendant node; null when no descendants are found.
|
||||
*/
|
||||
public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Parser_Node {
|
||||
for ( $i = 0; $i < count( $this->children ); $i++ ) {
|
||||
$child = $this->children[ $i ];
|
||||
if ( ! $child instanceof WP_Parser_Node ) {
|
||||
continue;
|
||||
}
|
||||
if ( null === $rule_name || $child->rule_name === $rule_name ) {
|
||||
return $child;
|
||||
}
|
||||
$node = $child->get_first_descendant_node( $rule_name );
|
||||
if ( $node ) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first descendant token of this node.
|
||||
*
|
||||
* The node children are traversed recursively in a depth-first order until
|
||||
* a matching descendant token is found, or the entire subtree is searched.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token|null The first matching descendant token; null when no descendants are found.
|
||||
*/
|
||||
public function get_first_descendant_token( ?int $token_id = null ): ?WP_Parser_Token {
|
||||
for ( $i = 0; $i < count( $this->children ); $i++ ) {
|
||||
$child = $this->children[ $i ];
|
||||
if ( $child instanceof WP_Parser_Token ) {
|
||||
if ( null === $token_id || $child->id === $token_id ) {
|
||||
return $child;
|
||||
}
|
||||
} else {
|
||||
$token = $child->get_first_descendant_token( $token_id );
|
||||
if ( $token ) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all children of this node.
|
||||
*
|
||||
* @return array<WP_Parser_Node|WP_Parser_Token> An array of all child nodes and tokens of this node.
|
||||
*/
|
||||
public function get_children(): array {
|
||||
return $this->children;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child nodes of this node.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node[] An array of all matching child nodes.
|
||||
*/
|
||||
public function get_child_nodes( ?string $rule_name = null ): array {
|
||||
$nodes = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Node
|
||||
&& ( null === $rule_name || $child->rule_name === $rule_name )
|
||||
) {
|
||||
$nodes[] = $child;
|
||||
}
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all child tokens of this node.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token[] An array of all matching child tokens.
|
||||
*/
|
||||
public function get_child_tokens( ?int $token_id = null ): array {
|
||||
$tokens = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if (
|
||||
$child instanceof WP_Parser_Token
|
||||
&& ( null === $token_id || $child->id === $token_id )
|
||||
) {
|
||||
$tokens[] = $child;
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendants of this node.
|
||||
*
|
||||
* The descendants are collected using a depth-first pre-order NLR traversal.
|
||||
* This produces a natural ordering that corresponds to the original input.
|
||||
*
|
||||
* @return array<WP_Parser_Node|WP_Parser_Token> An array of all descendant nodes and tokens of this node.
|
||||
*/
|
||||
public function get_descendants(): array {
|
||||
$descendants = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if ( $child instanceof WP_Parser_Node ) {
|
||||
$descendants[] = $child;
|
||||
$descendants = array_merge( $descendants, $child->get_descendants() );
|
||||
} else {
|
||||
$descendants[] = $child;
|
||||
}
|
||||
}
|
||||
return $descendants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant nodes of this node.
|
||||
*
|
||||
* The descendants are collected using a depth-first pre-order NLR traversal.
|
||||
* This produces a natural ordering that corresponds to the original input.
|
||||
* All matching nodes are collected during the traversal.
|
||||
*
|
||||
* @param string|null $rule_name Optional. A node rule name to check for.
|
||||
* @return WP_Parser_Node[] An array of all matching descendant nodes.
|
||||
*/
|
||||
public function get_descendant_nodes( ?string $rule_name = null ): array {
|
||||
$nodes = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if ( ! $child instanceof WP_Parser_Node ) {
|
||||
continue;
|
||||
}
|
||||
if ( null === $rule_name || $child->rule_name === $rule_name ) {
|
||||
$nodes[] = $child;
|
||||
}
|
||||
$nodes = array_merge( $nodes, $child->get_descendant_nodes( $rule_name ) );
|
||||
}
|
||||
return $nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descendant tokens of this node.
|
||||
*
|
||||
* The descendants are collected using a depth-first pre-order NLR traversal.
|
||||
* This produces a natural ordering that corresponds to the original input.
|
||||
* All matching tokens are collected during the traversal.
|
||||
*
|
||||
* @param int|null $token_id Optional. A token ID to check for.
|
||||
* @return WP_Parser_Token[] An array of all matching descendant tokens.
|
||||
*/
|
||||
public function get_descendant_tokens( ?int $token_id = null ): array {
|
||||
$tokens = array();
|
||||
foreach ( $this->children as $child ) {
|
||||
if ( $child instanceof WP_Parser_Token ) {
|
||||
if ( null === $token_id || $child->id === $token_id ) {
|
||||
$tokens[] = $child;
|
||||
}
|
||||
} else {
|
||||
$tokens = array_merge( $tokens, $child->get_descendant_tokens( $token_id ) );
|
||||
}
|
||||
}
|
||||
return $tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the byte offset in the input string where this node begins.
|
||||
*
|
||||
* @return int The byte offset in the input string where this node begins.
|
||||
*/
|
||||
public function get_start(): int {
|
||||
return $this->get_first_descendant_token()->start;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the byte length of this node in the input string.
|
||||
*
|
||||
* @return int The byte length of this node in the input string.
|
||||
*/
|
||||
public function get_length(): int {
|
||||
$tokens = $this->get_descendant_tokens();
|
||||
$first_token = $tokens[0];
|
||||
$last_token = $tokens[ count( $tokens ) - 1 ];
|
||||
return $last_token->start + $last_token->length - $first_token->start;
|
||||
}
|
||||
}
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A token, representing a leaf in the parse tree.
|
||||
*
|
||||
* This class represents a token that is consumed and recognized by WP_Parser.
|
||||
* In a parse tree, a token represent a leaf, that is, a node without children.
|
||||
* It is a simple generic container for a token ID and value, that can be used
|
||||
* as a base class and extended for specific use cases.
|
||||
*/
|
||||
class WP_Parser_Token {
|
||||
/**
|
||||
* Token ID represented as an integer constant.
|
||||
*
|
||||
* @var int $id
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* Byte offset in the input where the token begins.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $start;
|
||||
|
||||
/**
|
||||
* Byte length of the token in the input.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $length;
|
||||
|
||||
/**
|
||||
* Input bytes from which the token was parsed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $input;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $id Token type.
|
||||
* @param int $start Byte offset in the input where the token begins.
|
||||
* @param int $length Byte length of the token in the input.
|
||||
* @param string $input Input bytes from which the token was parsed.
|
||||
*/
|
||||
public function __construct(
|
||||
int $id,
|
||||
int $start,
|
||||
int $length,
|
||||
string $input
|
||||
) {
|
||||
$this->id = $id;
|
||||
$this->start = $start;
|
||||
$this->length = $length;
|
||||
$this->input = $input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the raw bytes of the token from the input.
|
||||
*
|
||||
* @return string The token bytes.
|
||||
*/
|
||||
public function get_bytes(): string {
|
||||
return substr( $this->input, $this->start, $this->length );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the real unquoted value of the token.
|
||||
*
|
||||
* @return string The token value.
|
||||
*/
|
||||
public function get_value(): string {
|
||||
return $this->get_bytes();
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* A recursive descent parser.
|
||||
*
|
||||
* This is a dynamic recursive descent parser that can parse LL grammars.
|
||||
*
|
||||
* @TODO: Add a detailed description and list the properties that a grammar must
|
||||
* satisfy in order to be supported by this parser (e.g., no left recursion).
|
||||
*/
|
||||
class WP_Parser {
|
||||
protected $grammar;
|
||||
protected $tokens;
|
||||
protected $position;
|
||||
|
||||
public function __construct( WP_Parser_Grammar $grammar, array $tokens ) {
|
||||
$this->grammar = $grammar;
|
||||
$this->tokens = $tokens;
|
||||
$this->position = 0;
|
||||
}
|
||||
|
||||
public function parse() {
|
||||
// @TODO: Make the starting rule lookup non-grammar-specific.
|
||||
$query_rule_id = $this->grammar->get_rule_id( 'query' );
|
||||
$ast = $this->parse_recursive( $query_rule_id );
|
||||
return false === $ast ? null : $ast;
|
||||
}
|
||||
|
||||
private function parse_recursive( $rule_id ) {
|
||||
$is_terminal = $rule_id <= $this->grammar->highest_terminal_id;
|
||||
if ( $is_terminal ) {
|
||||
if ( $this->position >= count( $this->tokens ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( WP_Parser_Grammar::EMPTY_RULE_ID === $rule_id ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ( $this->tokens[ $this->position ]->id === $rule_id ) {
|
||||
++$this->position;
|
||||
return $this->tokens[ $this->position - 1 ];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$branches = $this->grammar->rules[ $rule_id ];
|
||||
if ( ! count( $branches ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Bale out from processing the current branch if none of its rules can
|
||||
// possibly match the current token.
|
||||
if ( isset( $this->grammar->lookahead_is_match_possible[ $rule_id ] ) ) {
|
||||
$token_id = $this->tokens[ $this->position ]->id;
|
||||
if (
|
||||
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) &&
|
||||
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$rule_name = $this->grammar->rule_names[ $rule_id ];
|
||||
$starting_position = $this->position;
|
||||
foreach ( $branches as $branch ) {
|
||||
$this->position = $starting_position;
|
||||
$node = new WP_Parser_Node( $rule_id, $rule_name );
|
||||
$branch_matches = true;
|
||||
foreach ( $branch as $subrule_id ) {
|
||||
$subnode = $this->parse_recursive( $subrule_id );
|
||||
if ( false === $subnode ) {
|
||||
$branch_matches = false;
|
||||
break;
|
||||
} elseif ( true === $subnode ) {
|
||||
/*
|
||||
* The subrule was matched without actually matching a token.
|
||||
* This means a special empty "ε" (epsilon) rule was matched.
|
||||
* An "ε" rule in a grammar matches an empty input of 0 bytes.
|
||||
* It is used to represent optional grammar productions.
|
||||
*/
|
||||
continue;
|
||||
} elseif ( is_array( $subnode ) && 0 === count( $subnode ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( is_array( $subnode ) && ! count( $subnode ) ) {
|
||||
continue;
|
||||
}
|
||||
if ( isset( $this->grammar->fragment_ids[ $subrule_id ] ) ) {
|
||||
$node->merge_fragment( $subnode );
|
||||
} else {
|
||||
$node->append_child( $subnode );
|
||||
}
|
||||
}
|
||||
|
||||
// Negative lookahead for INTO after a valid SELECT statement.
|
||||
// If we match a SELECT statement, but there is an INTO keyword after it,
|
||||
// we're in the wrong branch and need to leave matching to a later rule.
|
||||
// @TODO: Extract this to the "WP_MySQL_Parser" class, or add support
|
||||
// for right-associative rules, which could solve this.
|
||||
// See: https://github.com/mysql/mysql-workbench/blob/8.0.38/library/parsers/grammars/MySQLParser.g4#L994
|
||||
// See: https://github.com/antlr/antlr4/issues/488
|
||||
$la = $this->tokens[ $this->position ] ?? null;
|
||||
if ( $la && 'selectStatement' === $rule_name && WP_MySQL_Lexer::INTO_SYMBOL === $la->id ) {
|
||||
$branch_matches = false;
|
||||
}
|
||||
|
||||
if ( true === $branch_matches ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $branch_matches ) {
|
||||
$this->position = $starting_position;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $node->has_child() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user