Skip to content

Commit 5c0d0e7

Browse files
committed
Fix native parser lifetime caches
1 parent e6b5619 commit 5c0d0e7

4 files changed

Lines changed: 140 additions & 63 deletions

File tree

packages/mysql-on-sqlite/ext/wp-mysql-parser/src/lib.rs

Lines changed: 43 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use std::collections::{HashMap, HashSet};
44
use std::os::raw::c_char;
55
use std::ptr;
6-
use std::sync::{Arc, Mutex, OnceLock};
6+
use std::sync::Arc;
77

88
use ext_php_rs::convert::{FromZval, IntoZval, IntoZvalDyn};
99
use ext_php_rs::exception::{PhpException, PhpResult};
@@ -62,33 +62,15 @@ struct PhpClasses {
6262
parser_node: &'static ClassEntry,
6363
}
6464

65-
// Class entries are process-lifetime Zend metadata. We only read the pointers
66-
// after lookup, and PHP owns their lifetime.
67-
unsafe impl Send for PhpClasses {}
68-
unsafe impl Sync for PhpClasses {}
69-
70-
static PHP_CLASSES: OnceLock<PhpClasses> = OnceLock::new();
71-
72-
fn php_classes() -> PhpResult<&'static PhpClasses> {
73-
if let Some(classes) = PHP_CLASSES.get() {
74-
return Ok(classes);
75-
}
76-
77-
let classes = PhpClasses {
65+
fn php_classes() -> PhpResult<PhpClasses> {
66+
Ok(PhpClasses {
7867
parser_token: ClassEntry::try_find("WP_Parser_Token")
7968
.ok_or_else(|| php_error("Missing WP_Parser_Token class"))?,
8069
mysql_token: ClassEntry::try_find("WP_MySQL_Token")
8170
.ok_or_else(|| php_error("Missing WP_MySQL_Token class"))?,
8271
parser_node: ClassEntry::try_find("WP_Parser_Node")
8372
.ok_or_else(|| php_error("Missing WP_Parser_Node class"))?,
84-
};
85-
86-
PHP_CLASSES
87-
.set(classes)
88-
.map_err(|_| php_error("PHP class cache was initialized concurrently"))?;
89-
PHP_CLASSES
90-
.get()
91-
.ok_or_else(|| php_error("PHP class cache initialization failed"))
73+
})
9274
}
9375

9476
fn update_object_property(
@@ -942,7 +924,11 @@ impl Grammar {
942924
}
943925
}
944926

945-
static GRAMMAR_CACHE: OnceLock<Mutex<HashMap<u32, Arc<Grammar>>>> = OnceLock::new();
927+
#[php_class]
928+
#[php(name = "WP_MySQL_Native_Grammar")]
929+
pub struct WpMySqlNativeGrammar {
930+
grammar: Arc<Grammar>,
931+
}
946932

947933
enum ParserTokenSource {
948934
Php(Vec<Zval>),
@@ -1683,21 +1669,13 @@ impl WpMySqlNativeParser {
16831669
}
16841670
}
16851671

1686-
fn export_grammar(grammar: &mut Zval) -> PhpResult<Arc<Grammar>> {
1687-
let grammar_id = grammar.object().map(|object| object.get_id());
1688-
if let Some(grammar_id) = grammar_id {
1689-
let cache = GRAMMAR_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
1690-
if let Some(cached) = cache
1691-
.lock()
1692-
.map_err(|_| php_error("Grammar cache lock poisoned"))?
1693-
.get(&grammar_id)
1694-
{
1695-
return Ok(Arc::clone(cached));
1696-
}
1672+
fn export_grammar(grammar_zval: &mut Zval) -> PhpResult<Arc<Grammar>> {
1673+
if let Some(cached) = cached_native_grammar(grammar_zval)? {
1674+
return Ok(cached);
16971675
}
16981676

16991677
let exported = php_function("wp_sqlite_mysql_native_export_grammar")?
1700-
.try_call(vec![&*grammar as &dyn IntoZvalDyn])
1678+
.try_call(vec![&*grammar_zval as &dyn IntoZvalDyn])
17011679
.map_err(php_error)?;
17021680
let array = exported
17031681
.array()
@@ -1752,17 +1730,39 @@ fn export_grammar(grammar: &mut Zval) -> PhpResult<Arc<Grammar>> {
17521730
select_statement_rule_id,
17531731
});
17541732

1755-
if let Some(grammar_id) = grammar_id {
1756-
GRAMMAR_CACHE
1757-
.get_or_init(|| Mutex::new(HashMap::new()))
1758-
.lock()
1759-
.map_err(|_| php_error("Grammar cache lock poisoned"))?
1760-
.insert(grammar_id, Arc::clone(&grammar));
1761-
}
1733+
cache_native_grammar(grammar_zval, Arc::clone(&grammar))?;
17621734

17631735
Ok(grammar)
17641736
}
17651737

1738+
fn cached_native_grammar(grammar: &Zval) -> PhpResult<Option<Arc<Grammar>>> {
1739+
let object = grammar
1740+
.object()
1741+
.ok_or_else(|| php_error("Parser grammar must be an object"))?;
1742+
let properties = object.get_properties().map_err(php_error)?;
1743+
let Some(native_grammar) = properties.get("native_grammar") else {
1744+
return Ok(None);
1745+
};
1746+
let Some(native_grammar) = <&WpMySqlNativeGrammar as FromZval>::from_zval(native_grammar)
1747+
else {
1748+
return Ok(None);
1749+
};
1750+
1751+
Ok(Some(Arc::clone(&native_grammar.grammar)))
1752+
}
1753+
1754+
fn cache_native_grammar(grammar_zval: &mut Zval, grammar: Arc<Grammar>) -> PhpResult<()> {
1755+
let object = grammar_zval
1756+
.object_mut()
1757+
.ok_or_else(|| php_error("Parser grammar must be an object"))?;
1758+
let native_grammar = WpMySqlNativeGrammar { grammar }
1759+
.into_zval(false)
1760+
.map_err(php_error)?;
1761+
object
1762+
.set_property("native_grammar", native_grammar)
1763+
.map_err(php_error)
1764+
}
1765+
17661766
fn export_tokens(tokens: &mut Zval) -> PhpResult<(ParserTokenSource, Vec<i64>)> {
17671767
if let Some(stream) = <&WpMySqlNativeTokenStream as FromZval>::from_zval(tokens) {
17681768
let token_ids = stream.tokens.iter().map(|token| token.id).collect();
@@ -1914,6 +1914,7 @@ extern "C" fn php_module_info(_module: *mut ModuleEntry) {
19141914
#[php_module]
19151915
pub fn get_module(module: ModuleBuilder) -> ModuleBuilder {
19161916
module
1917+
.class::<WpMySqlNativeGrammar>()
19171918
.class::<WpMySqlNativeAst>()
19181919
.class::<WpMySqlNativeTokenStream>()
19191920
.class::<WpMySqlNativeLexer>()

packages/mysql-on-sqlite/src/mysql/mysql-rust-bridge.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
*/
1414
function wp_sqlite_mysql_native_export_grammar( WP_Parser_Grammar $grammar ): array {
1515
return array(
16-
'highest_terminal_id' => $grammar->highest_terminal_id,
17-
'rules' => $grammar->rules,
18-
'lookahead_is_match_possible' => $grammar->lookahead_is_match_possible,
19-
'rule_names' => $grammar->rule_names,
20-
'fragment_ids' => $grammar->fragment_ids,
16+
'highest_terminal_id' => $grammar->get_highest_terminal_id(),
17+
'rules' => $grammar->get_rules(),
18+
'lookahead_is_match_possible' => $grammar->get_lookahead_is_match_possible(),
19+
'rule_names' => $grammar->get_rule_names(),
20+
'fragment_ids' => $grammar->get_fragment_ids(),
2121
);
2222
}

packages/mysql-on-sqlite/src/parser/class-wp-parser-grammar.php

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,48 @@ class WP_Parser_Grammar {
2626
/**
2727
* @TODO: Review and document these properties and their visibility.
2828
*/
29-
public $rules;
30-
public $rule_names;
31-
public $fragment_ids;
32-
public $lookahead_is_match_possible = array();
33-
public $lowest_non_terminal_id;
34-
public $highest_terminal_id;
29+
private $rules = array();
30+
private $rule_names = array();
31+
private $fragment_ids = array();
32+
private $lookahead_is_match_possible = array();
33+
private $lowest_non_terminal_id;
34+
private $highest_terminal_id;
35+
public $native_grammar;
3536

3637
public function __construct( array $rules ) {
3738
$this->inflate( $rules );
3839
}
3940

41+
public function __get( $name ) {
42+
if ( $this->is_grammar_property( $name ) ) {
43+
return $this->$name;
44+
}
45+
46+
trigger_error( 'Undefined property: ' . __CLASS__ . '::$' . $name, E_USER_NOTICE );
47+
return null;
48+
}
49+
50+
public function __isset( $name ) {
51+
return $this->is_grammar_property( $name ) && isset( $this->$name );
52+
}
53+
54+
public function __set( $name, $value ) {
55+
if ( $this->is_grammar_property( $name ) ) {
56+
$this->$name = $value;
57+
$this->native_grammar = null;
58+
return;
59+
}
60+
61+
trigger_error( 'Undefined property: ' . __CLASS__ . '::$' . $name, E_USER_NOTICE );
62+
}
63+
64+
public function __unset( $name ) {
65+
if ( $this->is_grammar_property( $name ) ) {
66+
unset( $this->$name );
67+
$this->native_grammar = null;
68+
}
69+
}
70+
4071
public function get_rule_name( $rule_id ) {
4172
return $this->rule_names[ $rule_id ];
4273
}
@@ -45,6 +76,41 @@ public function get_rule_id( $rule_name ) {
4576
return array_search( $rule_name, $this->rule_names, true );
4677
}
4778

79+
public function get_rules() {
80+
return $this->rules;
81+
}
82+
83+
public function get_rule_names() {
84+
return $this->rule_names;
85+
}
86+
87+
public function get_fragment_ids() {
88+
return $this->fragment_ids;
89+
}
90+
91+
public function get_lookahead_is_match_possible() {
92+
return $this->lookahead_is_match_possible;
93+
}
94+
95+
public function get_highest_terminal_id() {
96+
return $this->highest_terminal_id;
97+
}
98+
99+
private function is_grammar_property( $name ) {
100+
return in_array(
101+
$name,
102+
array(
103+
'rules',
104+
'rule_names',
105+
'fragment_ids',
106+
'lookahead_is_match_possible',
107+
'lowest_non_terminal_id',
108+
'highest_terminal_id',
109+
),
110+
true
111+
);
112+
}
113+
48114
/**
49115
* Inflate the grammar to an internal representation optimized for parsing.
50116
*

packages/mysql-on-sqlite/src/parser/class-wp-parser.php

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,23 @@
1010
*/
1111
class WP_Parser {
1212
protected $grammar;
13+
protected $rules;
14+
protected $rule_names;
15+
protected $fragment_ids;
16+
protected $lookahead_is_match_possible;
17+
protected $highest_terminal_id;
1318
protected $tokens;
1419
protected $position;
1520

1621
public function __construct( WP_Parser_Grammar $grammar, array $tokens ) {
17-
$this->grammar = $grammar;
18-
$this->tokens = $tokens;
19-
$this->position = 0;
22+
$this->grammar = $grammar;
23+
$this->rules = $grammar->get_rules();
24+
$this->rule_names = $grammar->get_rule_names();
25+
$this->fragment_ids = $grammar->get_fragment_ids();
26+
$this->lookahead_is_match_possible = $grammar->get_lookahead_is_match_possible();
27+
$this->highest_terminal_id = $grammar->get_highest_terminal_id();
28+
$this->tokens = $tokens;
29+
$this->position = 0;
2030
}
2131

2232
public function parse() {
@@ -27,7 +37,7 @@ public function parse() {
2737
}
2838

2939
private function parse_recursive( $rule_id ) {
30-
$is_terminal = $rule_id <= $this->grammar->highest_terminal_id;
40+
$is_terminal = $rule_id <= $this->highest_terminal_id;
3141
if ( $is_terminal ) {
3242
if ( $this->position >= count( $this->tokens ) ) {
3343
return false;
@@ -44,24 +54,24 @@ private function parse_recursive( $rule_id ) {
4454
return false;
4555
}
4656

47-
$branches = $this->grammar->rules[ $rule_id ];
57+
$branches = $this->rules[ $rule_id ];
4858
if ( ! count( $branches ) ) {
4959
return false;
5060
}
5161

5262
// Bale out from processing the current branch if none of its rules can
5363
// possibly match the current token.
54-
if ( isset( $this->grammar->lookahead_is_match_possible[ $rule_id ] ) ) {
64+
if ( isset( $this->lookahead_is_match_possible[ $rule_id ] ) ) {
5565
$token_id = $this->tokens[ $this->position ]->id;
5666
if (
57-
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) &&
58-
! isset( $this->grammar->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] )
67+
! isset( $this->lookahead_is_match_possible[ $rule_id ][ $token_id ] ) &&
68+
! isset( $this->lookahead_is_match_possible[ $rule_id ][ WP_Parser_Grammar::EMPTY_RULE_ID ] )
5969
) {
6070
return false;
6171
}
6272
}
6373

64-
$rule_name = $this->grammar->rule_names[ $rule_id ];
74+
$rule_name = $this->rule_names[ $rule_id ];
6575
$starting_position = $this->position;
6676
foreach ( $branches as $branch ) {
6777
$this->position = $starting_position;
@@ -86,7 +96,7 @@ private function parse_recursive( $rule_id ) {
8696
if ( is_array( $subnode ) && ! count( $subnode ) ) {
8797
continue;
8898
}
89-
if ( isset( $this->grammar->fragment_ids[ $subrule_id ] ) ) {
99+
if ( isset( $this->fragment_ids[ $subrule_id ] ) ) {
90100
$node->merge_fragment( $subnode );
91101
} else {
92102
$node->append_child( $subnode );

0 commit comments

Comments
 (0)