88 * caller actually walks the tree. On the first mutation (append_child or
99 * merge_fragment), the node materializes its children into the inherited
1010 * `$children` array and behaves like a plain WP_Parser_Node from then on.
11+ *
12+ * Wrappers returned by accessors are interned through a per-AST identity
13+ * map (WP_MySQL_Native_AST_Cache) so two reads of the same logical node
14+ * yield the same PHP instance. This preserves the WP_Parser_Node contract
15+ * that mutations performed on a child via `get_first_child_node()` remain
16+ * visible when the same child is reached again, including after the parent
17+ * has materialized.
1118 */
1219class WP_MySQL_Native_Parser_Node extends WP_Parser_Node {
1320 private $ native_ast = null ;
1421 private $ native_node_index = null ;
1522 private $ was_mutated = false ;
1623
24+ /**
25+ * Per-AST identity map shared between every interned wrapper.
26+ *
27+ * Created lazily on the first child access; the root wrapper is the
28+ * first entry. Children inherit the same cache instance by reference.
29+ *
30+ * @var WP_MySQL_Native_AST_Cache|null
31+ */
32+ private $ cache = null ;
33+
1734 public function __construct ( $ rule_id , $ rule_name , $ native_ast = null , $ native_node_index = null ) {
1835 parent ::__construct ( $ rule_id , $ rule_name );
1936
2037 $ this ->native_ast = $ native_ast ;
2138 $ this ->native_node_index = $ native_node_index ;
2239 }
2340
41+ /**
42+ * Native node index in the Rust-owned arena.
43+ *
44+ * Exposed so the identity cache can key on it. Returns null after
45+ * the wrapper has materialized — at that point the node is detached
46+ * from the native arena and behaves like a plain WP_Parser_Node.
47+ *
48+ * @return int|null
49+ */
50+ public function get_native_node_index (): ?int {
51+ return $ this ->native_node_index ;
52+ }
53+
2454 /** @inheritDoc */
2555 public function append_child ( $ node ) {
2656 $ this ->materialize_native_children ();
@@ -65,15 +95,15 @@ public function get_first_child() {
6595 if ( $ this ->was_mutated () ) {
6696 return parent ::get_first_child ();
6797 }
68- return wp_sqlite_mysql_native_ast_get_first_child ( $ this ->native_ast , $ this ->native_node_index );
98+ return $ this -> intern ( wp_sqlite_mysql_native_ast_get_first_child ( $ this ->native_ast , $ this ->native_node_index ) );
6999 }
70100
71101 /** @inheritDoc */
72102 public function get_first_child_node ( ?string $ rule_name = null ): ?WP_Parser_Node {
73103 if ( $ this ->was_mutated () ) {
74104 return parent ::get_first_child_node ( $ rule_name );
75105 }
76- return wp_sqlite_mysql_native_ast_get_first_child_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
106+ return $ this -> intern ( wp_sqlite_mysql_native_ast_get_first_child_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name ) );
77107 }
78108
79109 /** @inheritDoc */
@@ -89,7 +119,7 @@ public function get_first_descendant_node( ?string $rule_name = null ): ?WP_Pars
89119 if ( $ this ->was_mutated () ) {
90120 return parent ::get_first_descendant_node ( $ rule_name );
91121 }
92- return wp_sqlite_mysql_native_ast_get_first_descendant_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
122+ return $ this -> intern ( wp_sqlite_mysql_native_ast_get_first_descendant_node ( $ this ->native_ast , $ this ->native_node_index , $ rule_name ) );
93123 }
94124
95125 /** @inheritDoc */
@@ -105,15 +135,15 @@ public function get_children(): array {
105135 if ( $ this ->was_mutated () ) {
106136 return parent ::get_children ();
107137 }
108- return wp_sqlite_mysql_native_ast_get_children ( $ this ->native_ast , $ this ->native_node_index );
138+ return $ this -> intern_all ( wp_sqlite_mysql_native_ast_get_children ( $ this ->native_ast , $ this ->native_node_index ) );
109139 }
110140
111141 /** @inheritDoc */
112142 public function get_child_nodes ( ?string $ rule_name = null ): array {
113143 if ( $ this ->was_mutated () ) {
114144 return parent ::get_child_nodes ( $ rule_name );
115145 }
116- return wp_sqlite_mysql_native_ast_get_child_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
146+ return $ this -> intern_all ( wp_sqlite_mysql_native_ast_get_child_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name ) );
117147 }
118148
119149 /** @inheritDoc */
@@ -129,15 +159,15 @@ public function get_descendants(): array {
129159 if ( $ this ->was_mutated () ) {
130160 return parent ::get_descendants ();
131161 }
132- return wp_sqlite_mysql_native_ast_get_descendants ( $ this ->native_ast , $ this ->native_node_index );
162+ return $ this -> intern_all ( wp_sqlite_mysql_native_ast_get_descendants ( $ this ->native_ast , $ this ->native_node_index ) );
133163 }
134164
135165 /** @inheritDoc */
136166 public function get_descendant_nodes ( ?string $ rule_name = null ): array {
137167 if ( $ this ->was_mutated () ) {
138168 return parent ::get_descendant_nodes ( $ rule_name );
139169 }
140- return wp_sqlite_mysql_native_ast_get_descendant_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name );
170+ return $ this -> intern_all ( wp_sqlite_mysql_native_ast_get_descendant_nodes ( $ this ->native_ast , $ this ->native_node_index , $ rule_name ) );
141171 }
142172
143173 /** @inheritDoc */
@@ -168,12 +198,78 @@ private function was_mutated(): bool {
168198 return $ this ->was_mutated ;
169199 }
170200
201+ /**
202+ * Intern a single accessor return value through the per-AST cache.
203+ *
204+ * Tokens and nulls pass through untouched. Native node wrappers are
205+ * keyed on their `native_node_index`: on cache miss, the freshly
206+ * constructed wrapper is stored and given the cache reference; on
207+ * cache hit, the canonical instance is returned and the new wrapper
208+ * is discarded so callers see stable identity and surviving mutations.
209+ *
210+ * @param mixed $value Return value from the Rust bridge.
211+ * @return mixed
212+ */
213+ private function intern ( $ value ) {
214+ if ( ! $ value instanceof WP_MySQL_Native_Parser_Node ) {
215+ return $ value ;
216+ }
217+
218+ $ cache = $ this ->ensure_cache ();
219+ $ index = $ value ->native_node_index ;
220+ if ( null === $ index ) {
221+ return $ value ;
222+ }
223+ if ( isset ( $ cache ->nodes [ $ index ] ) ) {
224+ return $ cache ->nodes [ $ index ];
225+ }
226+ $ value ->cache = $ cache ;
227+ $ cache ->nodes [ $ index ] = $ value ;
228+ return $ value ;
229+ }
230+
231+ /**
232+ * Intern every entry in an accessor return array.
233+ *
234+ * @param array $values
235+ * @return array
236+ */
237+ private function intern_all ( array $ values ): array {
238+ foreach ( $ values as $ i => $ value ) {
239+ $ values [ $ i ] = $ this ->intern ( $ value );
240+ }
241+ return $ values ;
242+ }
243+
244+ /**
245+ * Lazily build (or reuse) the per-AST identity map.
246+ *
247+ * The root wrapper is constructed without a cache, so the first time
248+ * any accessor needs to intern a child, it creates the cache and
249+ * registers itself as the root entry. Subsequent interns on this
250+ * wrapper or any descendant share the same cache by reference.
251+ *
252+ * @return WP_MySQL_Native_AST_Cache
253+ */
254+ private function ensure_cache (): WP_MySQL_Native_AST_Cache {
255+ if ( null === $ this ->cache ) {
256+ $ this ->cache = new WP_MySQL_Native_AST_Cache ();
257+ if ( null !== $ this ->native_node_index ) {
258+ $ this ->cache ->nodes [ $ this ->native_node_index ] = $ this ;
259+ }
260+ }
261+ return $ this ->cache ;
262+ }
263+
171264 private function materialize_native_children (): void {
172265 if ( $ this ->was_mutated ) {
173266 return ;
174267 }
175268
176- $ this ->children = wp_sqlite_mysql_native_ast_get_children ( $ this ->native_ast , $ this ->native_node_index );
269+ // Pull children through the cache so any wrapper a caller already
270+ // mutated via get_first_child_node() etc. survives the transition
271+ // into $this->children — same instance, same mutations.
272+ $ this ->children = $ this ->intern_all ( wp_sqlite_mysql_native_ast_get_children ( $ this ->native_ast , $ this ->native_node_index ) );
177273 $ this ->native_ast = null ;
178274 $ this ->native_node_index = null ;
179275 $ this ->was_mutated = true ;
0 commit comments