This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -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;
}
}