From 2ad3e1d1d8de34a23bc3d38f93b6bd9c422a4871 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Tue, 9 Jun 2026 23:06:34 -0300 Subject: [PATCH] Preserve named and anonymous node type identity --- .../src/bytecode/node_type_ir.rs | 8 +- crates/plotnik-compiler/src/analyze/link.rs | 38 +++++---- .../src/analyze/link_tests.rs | 57 ++++++++++++- crates/plotnik-compiler/src/bytecode/ir.rs | 16 ++-- .../src/compile/compile_tests.rs | 40 +++++++++ .../plotnik-compiler/src/compile/compiler.rs | 4 +- .../src/compile/expressions.rs | 29 +++---- crates/plotnik-compiler/src/compile/verify.rs | 14 ++-- crates/plotnik-compiler/src/emit/emitter.rs | 7 +- crates/plotnik-compiler/src/query/stages.rs | 4 +- crates/plotnik-compiler/src/test_utils.rs | 23 +++++ crates/plotnik-core/src/grammar/types.rs | 84 ++++++++++++------- .../plotnik-core/src/grammar/types_tests.rs | 16 ++-- crates/plotnik-core/src/interner.rs | 5 ++ crates/plotnik-core/src/lib.rs | 7 ++ crates/plotnik-vm/src/engine/vm.rs | 8 +- 16 files changed, 262 insertions(+), 98 deletions(-) diff --git a/crates/plotnik-bytecode/src/bytecode/node_type_ir.rs b/crates/plotnik-bytecode/src/bytecode/node_type_ir.rs index 306048d6e..f69fca514 100644 --- a/crates/plotnik-bytecode/src/bytecode/node_type_ir.rs +++ b/crates/plotnik-bytecode/src/bytecode/node_type_ir.rs @@ -12,8 +12,8 @@ use std::num::NonZeroU16; /// | `node_kind` | Value | Meaning | `node_type=0` | `node_type>0` | /// | ----------- | ----- | ------------ | ------------------- | ----------------- | /// | `00` | Any | `_` pattern | No check | (invalid) | -/// | `01` | Named | `(_)`/`(t)` | Check `is_named()` | Check `kind_id()` | -/// | `10` | Anon | `"text"` | Check `!is_named()` | Check `kind_id()` | +/// | `01` | Named | `(_)`/`(t)` | Check `is_named()` | Check `is_named()` + `kind_id()` | +/// | `10` | Anon | `"text"` | Check `!is_named()` | Check `!is_named()` + `kind_id()` | /// | `11` | - | Reserved | Error | Error | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub enum NodeTypeIR { @@ -22,11 +22,11 @@ pub enum NodeTypeIR { Any, /// Named node constraint (`(_)` or `(identifier)`). /// - `None` = any named node (check `is_named()`) - /// - `Some(id)` = specific named type (check `kind_id()`) + /// - `Some(id)` = specific named type (check `is_named()` and `kind_id()`) Named(Option), /// Anonymous node constraint (`"text"` literals). /// - `None` = any anonymous node (check `!is_named()`) - /// - `Some(id)` = specific anonymous type (check `kind_id()`) + /// - `Some(id)` = specific anonymous type (check `!is_named()` and `kind_id()`) Anonymous(Option), } diff --git a/crates/plotnik-compiler/src/analyze/link.rs b/crates/plotnik-compiler/src/analyze/link.rs index 668854c39..25e615cd4 100644 --- a/crates/plotnik-compiler/src/analyze/link.rs +++ b/crates/plotnik-compiler/src/analyze/link.rs @@ -8,14 +8,14 @@ use std::collections::HashMap; use indexmap::{IndexMap, IndexSet}; use plotnik_core::grammar::Grammar; -use plotnik_core::{Interner, NodeFieldId, NodeTypeId, Symbol}; +use plotnik_core::{Interner, NodeFieldId, NodeType, NodeTypeId, Symbol}; use rowan::TextRange; /// Output from the link phase for binary emission. #[derive(Default)] pub struct LinkOutput { - /// Interned name → NodeTypeId (for binary: StringId → NodeTypeId) - pub node_type_ids: IndexMap, + /// Interned named/anonymous node type → NodeTypeId (for binary: StringId → NodeTypeId) + pub node_type_ids: IndexMap, NodeTypeId>, /// Interned name → NodeFieldId (for binary: StringId → NodeFieldId) pub node_field_ids: IndexMap, } @@ -42,7 +42,7 @@ pub fn link<'q>( diagnostics: &mut Diagnostics, ) { // Local deduplication maps (not exposed in output) - let mut node_type_ids: HashMap<&'q str, Option> = HashMap::new(); + let mut node_type_ids: HashMap, Option> = HashMap::new(); let mut node_field_ids: HashMap<&'q str, Option> = HashMap::new(); for (&source_id, root) in ast_map { @@ -68,7 +68,7 @@ struct Linker<'a, 'q> { source_map: &'q SourceMap, symbol_table: &'a SymbolTable, source_id: SourceId, - node_type_ids: &'a mut HashMap<&'q str, Option>, + node_type_ids: &'a mut HashMap, Option>, node_field_ids: &'a mut HashMap<&'q str, Option>, output: &'a mut LinkOutput, diagnostics: &'a mut Diagnostics, @@ -103,15 +103,18 @@ impl<'a, 'q> Linker<'a, 'q> { return; } let type_name = type_token.text(); - if self.node_type_ids.contains_key(type_name) { + let key = NodeType::Named(token_src(&type_token, self.source())); + if self.node_type_ids.contains_key(&key) { return; } let resolved = self.grammar.resolve_named_node(type_name); - self.node_type_ids - .insert(token_src(&type_token, self.source()), resolved); + self.node_type_ids.insert(key, resolved); if let Some(id) = resolved { let sym = self.interner.intern(type_name); - self.output.node_type_ids.entry(sym).or_insert(id); + self.output + .node_type_ids + .entry(NodeType::Named(sym)) + .or_insert(id); } if resolved.is_none() { let all_types = self.grammar.all_named_node_kinds(); @@ -271,8 +274,8 @@ impl<'a, 'q> Linker<'a, 'q> { ) { return None; } - let type_name = type_token.text(); - let parent_id = self.node_type_ids.get(type_name).copied().flatten()?; + let key = NodeType::Named(token_src(&type_token, self.source())); + let parent_id = self.node_type_ids.get(&key).copied().flatten()?; // Verify the node type exists in the grammar self.grammar.node_type_name(parent_id)?; Some(ValidationContext { @@ -436,18 +439,21 @@ impl Visitor for SymbolResolver<'_, '_, '_> { return; }; let value = value_token.text(); - if self.linker.node_type_ids.contains_key(value) { + let key = NodeType::Anonymous(token_src(&value_token, self.linker.source())); + if self.linker.node_type_ids.contains_key(&key) { return; } let resolved = self.linker.grammar.resolve_anonymous_node(value); - self.linker - .node_type_ids - .insert(token_src(&value_token, self.linker.source()), resolved); + self.linker.node_type_ids.insert(key, resolved); if let Some(id) = resolved { let sym = self.linker.interner.intern(value); - self.linker.output.node_type_ids.entry(sym).or_insert(id); + self.linker + .output + .node_type_ids + .entry(NodeType::Anonymous(sym)) + .or_insert(id); return; } diff --git a/crates/plotnik-compiler/src/analyze/link_tests.rs b/crates/plotnik-compiler/src/analyze/link_tests.rs index 300aa8ecb..e68c33744 100644 --- a/crates/plotnik-compiler/src/analyze/link_tests.rs +++ b/crates/plotnik-compiler/src/analyze/link_tests.rs @@ -1,5 +1,48 @@ -use crate::Query; +use crate::test_utils::colliding_node_type_grammar; +use crate::{Query, QueryBuilder, SourceMap}; use indoc::indoc; +use plotnik_core::NodeType; + +fn assert_links_colliding_node_types(files: &[(&str, &str)]) { + let grammar = colliding_node_type_grammar(); + let mut source_map = SourceMap::new(); + for (path, content) in files { + source_map.add_file(path, content); + } + + let query = QueryBuilder::new(source_map).parse().unwrap().analyze(); + if !query.is_valid() { + panic!( + "Expected valid query, got error:\n{}", + query.dump_diagnostics() + ); + } + + let query = query.link(&grammar); + if !query.is_valid() { + panic!( + "Expected valid linking, got error:\n{}", + query.dump_diagnostics() + ); + } + + let sym = query + .interner() + .get("number") + .expect("linked node name must be interned"); + let named_id = grammar.resolve_named_node("number").unwrap(); + let anonymous_id = grammar.resolve_anonymous_node("number").unwrap(); + + assert_ne!(named_id, anonymous_id); + assert_eq!( + query.node_type_ids().get(&NodeType::Named(sym)), + Some(&named_id) + ); + assert_eq!( + query.node_type_ids().get(&NodeType::Anonymous(sym)), + Some(&anonymous_id) + ); +} #[test] fn predicate_on_non_leaf() { @@ -21,6 +64,18 @@ fn predicate_on_leaf_valid() { Query::expect_valid_linking(input); } +#[test] +fn resolves_named_and_anonymous_node_types_with_same_name() { + assert_links_colliding_node_types(&[ + ("named.ptk", "A = (number)"), + ("anonymous.ptk", "Q = \"number\""), + ]); + assert_links_colliding_node_types(&[ + ("anonymous.ptk", "Q = \"number\""), + ("named.ptk", "A = (number)"), + ]); +} + #[test] fn valid_query_with_field() { let input = indoc! {r#" diff --git a/crates/plotnik-compiler/src/bytecode/ir.rs b/crates/plotnik-compiler/src/bytecode/ir.rs index b042949b7..f90c50e32 100644 --- a/crates/plotnik-compiler/src/bytecode/ir.rs +++ b/crates/plotnik-compiler/src/bytecode/ir.rs @@ -18,12 +18,12 @@ use plotnik_bytecode::{ /// Distinguishes between named nodes (`(identifier)`), anonymous nodes (`"text"`), /// and wildcards (`_`, `(_)`). Encoded in bytecode header byte bits 5-4. /// -/// | `node_kind` | Value | Meaning | `node_type=0` | `node_type>0` | -/// | ----------- | ----- | ------------ | ------------------- | ----------------- | -/// | `00` | Any | `_` pattern | No check | (invalid) | -/// | `01` | Named | `(_)`/`(t)` | Check `is_named()` | Check `kind_id()` | -/// | `10` | Anon | `"text"` | Check `!is_named()` | Check `kind_id()` | -/// | `11` | - | Reserved | Error | Error | +/// | `node_kind` | Value | Meaning | `node_type=0` | `node_type>0` | +/// | ----------- | ----- | ------------ | ------------------- | --------------------------------- | +/// | `00` | Any | `_` pattern | No check | (invalid) | +/// | `01` | Named | `(_)`/`(t)` | Check `is_named()` | Check `is_named()` + `kind_id()` | +/// | `10` | Anon | `"text"` | Check `!is_named()` | Check `!is_named()` + `kind_id()` | +/// | `11` | - | Reserved | Error | Error | #[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] pub enum NodeTypeIR { /// Any node (`_` pattern) - no type check performed. @@ -31,11 +31,11 @@ pub enum NodeTypeIR { Any, /// Named node constraint (`(_)` or `(identifier)`). /// - `None` = any named node (check `is_named()`) - /// - `Some(id)` = specific named type (check `kind_id()`) + /// - `Some(id)` = specific named type (check `is_named()` and `kind_id()`) Named(Option), /// Anonymous node constraint (`"text"` literals). /// - `None` = any anonymous node (check `!is_named()`) - /// - `Some(id)` = specific anonymous type (check `kind_id()`) + /// - `Some(id)` = specific anonymous type (check `!is_named()` and `kind_id()`) Anonymous(Option), } diff --git a/crates/plotnik-compiler/src/compile/compile_tests.rs b/crates/plotnik-compiler/src/compile/compile_tests.rs index 4092afeb6..b967a25a2 100644 --- a/crates/plotnik-compiler/src/compile/compile_tests.rs +++ b/crates/plotnik-compiler/src/compile/compile_tests.rs @@ -1,5 +1,16 @@ //! Integration tests for the compilation pipeline. +use std::cell::RefCell; +use std::num::NonZeroU16; + +use indexmap::IndexMap; +use plotnik_core::{Interner, NodeType}; + +use crate::analyze::symbol_table::SymbolTable; +use crate::analyze::type_check::TypeContext; +use crate::bytecode::NodeTypeIR; +use crate::compile::{CompileCtx, Compiler}; +use crate::emit::StringTableBuilder; use crate::shot_bytecode; #[test] @@ -12,6 +23,35 @@ fn compile_alternation() { shot_bytecode!("Test = [(identifier) (number)]"); } +#[test] +fn resolve_anonymous_node_type_uses_anonymous_namespace() { + let mut interner = Interner::new(); + let number = interner.intern("number"); + let named_id = NonZeroU16::new(1).unwrap(); + let anonymous_id = NonZeroU16::new(2).unwrap(); + let node_types = IndexMap::from([ + (NodeType::Named(number), named_id), + (NodeType::Anonymous(number), anonymous_id), + ]); + let type_ctx = TypeContext::new(); + let symbol_table = SymbolTable::new(); + let strings = RefCell::new(StringTableBuilder::new()); + let ctx = CompileCtx { + interner: &interner, + type_ctx: &type_ctx, + symbol_table: &symbol_table, + strings: &strings, + node_types: Some(&node_types), + node_fields: None, + }; + let mut compiler = Compiler::new(&ctx); + + assert_eq!( + compiler.resolve_anonymous_node_type("number"), + NodeTypeIR::Anonymous(Some(anonymous_id)) + ); +} + #[test] fn compile_sequence() { shot_bytecode!("Test = {(comment) (identifier)}"); diff --git a/crates/plotnik-compiler/src/compile/compiler.rs b/crates/plotnik-compiler/src/compile/compiler.rs index 2ea1e8f15..73f96eb24 100644 --- a/crates/plotnik-compiler/src/compile/compiler.rs +++ b/crates/plotnik-compiler/src/compile/compiler.rs @@ -3,7 +3,7 @@ use std::cell::RefCell; use indexmap::IndexMap; -use plotnik_core::{Interner, NodeFieldId, NodeTypeId, Symbol}; +use plotnik_core::{Interner, NodeFieldId, NodeType, NodeTypeId, Symbol}; use crate::analyze::symbol_table::SymbolTable; use crate::analyze::type_check::{DefId, TypeContext}; @@ -31,7 +31,7 @@ pub struct CompileCtx<'a> { pub type_ctx: &'a TypeContext, pub symbol_table: &'a SymbolTable, pub strings: &'a RefCell, - pub node_types: Option<&'a IndexMap>, + pub node_types: Option<&'a IndexMap, NodeTypeId>>, pub node_fields: Option<&'a IndexMap>, } diff --git a/crates/plotnik-compiler/src/compile/expressions.rs b/crates/plotnik-compiler/src/compile/expressions.rs index e05bf9714..3bba19592 100644 --- a/crates/plotnik-compiler/src/compile/expressions.rs +++ b/crates/plotnik-compiler/src/compile/expressions.rs @@ -13,6 +13,7 @@ use crate::analyze::type_check::TypeShape; use crate::bytecode::{EffectIR, InstructionIR, Label, MatchIR, NodeTypeIR, PredicateIR}; use crate::parser::ast::{self, Expr}; use plotnik_bytecode::Nav; +use plotnik_core::NodeType; use super::Compiler; use super::capture::CaptureEffects; @@ -545,13 +546,14 @@ impl Compiler<'_> { pub(super) fn resolve_anonymous_node_type(&mut self, text: &str) -> NodeTypeIR { if let Some(ids) = self.ctx.node_types { // Linked mode: resolve to NodeTypeId from grammar - for (&sym, &id) in ids { - if self.ctx.interner.resolve(sym) == text { - return NodeTypeIR::Anonymous(NonZeroU16::new(id.get())); - } - } - // If not found in grammar, treat as anonymous wildcard - NodeTypeIR::Anonymous(None) + let Some(sym) = self.ctx.interner.get(text) else { + return NodeTypeIR::Anonymous(None); + }; + ids.get(&NodeType::Anonymous(sym)) + .and_then(|id| NonZeroU16::new(id.get())) + .map_or(NodeTypeIR::Anonymous(None), |id| { + NodeTypeIR::Anonymous(Some(id)) + }) } else { // Unlinked mode: store StringId referencing the literal text let string_id = self.ctx.strings.borrow_mut().intern_str(text); @@ -578,13 +580,12 @@ impl Compiler<'_> { if let Some(ids) = self.ctx.node_types { // Linked mode: resolve to NodeTypeId from grammar - for (&sym, &id) in ids { - if self.ctx.interner.resolve(sym) == type_name { - return NodeTypeIR::Named(NonZeroU16::new(id.get())); - } - } - // If not found in grammar, treat as any named (linked mode) - NodeTypeIR::Named(None) + let Some(sym) = self.ctx.interner.get(type_name) else { + return NodeTypeIR::Named(None); + }; + ids.get(&NodeType::Named(sym)) + .and_then(|id| NonZeroU16::new(id.get())) + .map_or(NodeTypeIR::Named(None), |id| NodeTypeIR::Named(Some(id))) } else { // Unlinked mode: store StringId referencing the type name let string_id = self.ctx.strings.borrow_mut().intern_str(type_name); diff --git a/crates/plotnik-compiler/src/compile/verify.rs b/crates/plotnik-compiler/src/compile/verify.rs index d6235343b..3c05a7586 100644 --- a/crates/plotnik-compiler/src/compile/verify.rs +++ b/crates/plotnik-compiler/src/compile/verify.rs @@ -18,7 +18,7 @@ use plotnik_bytecode::Nav; use std::collections::BTreeSet; #[cfg(debug_assertions)] -use plotnik_core::Symbol; +use plotnik_core::{NodeType, Symbol}; #[cfg(debug_assertions)] use crate::bytecode::{MatchIR, MemberRef, NodeTypeIR, PredicateValueIR}; @@ -119,14 +119,18 @@ fn resolve_member_name( #[cfg(debug_assertions)] fn resolve_node_type_name( id: std::num::NonZeroU16, - node_types: Option<&indexmap::IndexMap>, + node_types: Option<&indexmap::IndexMap, plotnik_core::NodeTypeId>>, interner: &plotnik_core::Interner, ) -> Option { let types = node_types?; - for (sym, type_id) in types { - if type_id.get() == id.get() { - return interner.try_resolve(*sym).map(|s| s.to_string()); + for (node_type, type_id) in types { + if type_id.get() != id.get() { + continue; } + let sym = match node_type { + NodeType::Named(sym) | NodeType::Anonymous(sym) => *sym, + }; + return interner.try_resolve(sym).map(|s| s.to_string()); } None } diff --git a/crates/plotnik-compiler/src/emit/emitter.rs b/crates/plotnik-compiler/src/emit/emitter.rs index ca8b1605c..36fdc164b 100644 --- a/crates/plotnik-compiler/src/emit/emitter.rs +++ b/crates/plotnik-compiler/src/emit/emitter.rs @@ -2,7 +2,7 @@ use std::cell::RefCell; -use plotnik_core::Symbol; +use plotnik_core::{NodeType, Symbol}; use crate::analyze::type_check::TypeId; use crate::bytecode::{InstructionIR, Label, PredicateValueIR}; @@ -51,7 +51,10 @@ pub fn emit(query: &LinkedQuery) -> Result, EmitError> { // Collect node symbols let mut node_symbols: Vec = Vec::new(); - for (&sym, &node_id) in node_type_ids { + for (node_type, &node_id) in node_type_ids { + let sym = match node_type { + NodeType::Named(sym) | NodeType::Anonymous(sym) => *sym, + }; let name = strings.borrow_mut().get_or_intern(sym, interner)?; node_symbols.push(NodeSymbol::new(node_id.get(), name)); } diff --git a/crates/plotnik-compiler/src/query/stages.rs b/crates/plotnik-compiler/src/query/stages.rs index 4252de957..c523e498d 100644 --- a/crates/plotnik-compiler/src/query/stages.rs +++ b/crates/plotnik-compiler/src/query/stages.rs @@ -3,7 +3,7 @@ use std::ops::{Deref, DerefMut}; use indexmap::IndexMap; use plotnik_core::grammar::Grammar; -use plotnik_core::{Interner, NodeFieldId, NodeTypeId, Symbol}; +use plotnik_core::{Interner, NodeFieldId, NodeType, NodeTypeId, Symbol}; use super::{SourceId, SourceMap}; use crate::Diagnostics; @@ -277,7 +277,7 @@ impl LinkedQuery { &self.inner.interner } - pub fn node_type_ids(&self) -> &IndexMap { + pub fn node_type_ids(&self) -> &IndexMap, NodeTypeId> { &self.linking.node_type_ids } diff --git a/crates/plotnik-compiler/src/test_utils.rs b/crates/plotnik-compiler/src/test_utils.rs index 0719965e2..0b90328f6 100644 --- a/crates/plotnik-compiler/src/test_utils.rs +++ b/crates/plotnik-compiler/src/test_utils.rs @@ -1,5 +1,28 @@ //! Test utilities and snapshot macros. +use plotnik_core::grammar::{Grammar, raw::RawGrammar}; + +pub fn colliding_node_type_grammar() -> Grammar { + let raw = RawGrammar::from_json( + r#"{ + "name": "collision", + "rules": { + "program": { + "type": "CHOICE", + "members": [ + { "type": "SYMBOL", "name": "number" }, + { "type": "STRING", "value": "number" } + ] + }, + "number": { "type": "STRING", "value": "literal" } + } + }"#, + ) + .expect("collision grammar fixture"); + + Grammar::from_raw(&raw).expect("collision grammar metadata") +} + /// Snapshot test for bytecode output. #[macro_export] macro_rules! shot_bytecode { diff --git a/crates/plotnik-core/src/grammar/types.rs b/crates/plotnik-core/src/grammar/types.rs index f2174ce2c..37ebab298 100644 --- a/crates/plotnik-core/src/grammar/types.rs +++ b/crates/plotnik-core/src/grammar/types.rs @@ -5,7 +5,7 @@ use std::num::NonZeroU16; use serde::{Deserialize, Serialize}; -use crate::{Cardinality, NodeFieldId, NodeTypeId}; +use crate::{Cardinality, NodeFieldId, NodeType, NodeTypeId}; use super::json::GrammarError; use super::raw::RawGrammar; @@ -155,7 +155,7 @@ impl Grammar { } fn from_metadata(name: String, metadata: GrammarMetadata) -> Result { - let mut node_ids = HashMap::<(String, bool), NodeTypeId>::new(); + let mut node_ids = HashMap::, NodeTypeId>::new(); let mut named_node_ids = HashMap::new(); let mut anonymous_node_ids = HashMap::new(); let mut node_names = HashMap::new(); @@ -173,9 +173,12 @@ impl Grammar { continue; } - node_ids - .entry((symbol.type_name.clone(), symbol.named)) - .or_insert(node_id); + let node_type = if symbol.named { + NodeType::Named(symbol.type_name.as_str()) + } else { + NodeType::Anonymous(symbol.type_name.as_str()) + }; + node_ids.entry(node_type).or_insert(node_id); if symbol.named { named_node_ids @@ -198,7 +201,7 @@ impl Grammar { let (node_constraints, extra_node_types, root_node_type) = build_node_constraints( &metadata.node_shapes, - |name, named| node_ids.get(&(name.to_string(), named)).copied(), + |node_type| node_ids.get(&node_type).copied(), |name| field_ids.get(name).copied(), ) .map_err(format_node_shape_error)?; @@ -208,24 +211,20 @@ impl Grammar { let Some(shape_subtypes) = &shape.subtypes else { continue; }; - let Some(supertype) = node_ids.get(&(shape.type_name.clone(), shape.named)) else { + let Some(supertype) = node_ids.get(&shape.node_type()) else { continue; }; let resolved = shape_subtypes .iter() - .filter_map(|subtype| { - node_ids - .get(&(subtype.type_name.clone(), subtype.named)) - .copied() - }) + .filter_map(|subtype| node_ids.get(&subtype.node_type()).copied()) .collect::>(); subtypes.insert(*supertype, resolved); } let mut fields_by_node = HashMap::new(); for shape in &metadata.node_shapes { - let Some(node_id) = node_ids.get(&(shape.type_name.clone(), shape.named)) else { + let Some(node_id) = node_ids.get(&shape.node_type()) else { continue; }; let mut fields = shape.fields.keys().cloned().collect::>(); @@ -437,6 +436,16 @@ pub(crate) struct NodeShape { pub(crate) subtypes: Option>, } +impl NodeShape { + fn node_type(&self) -> NodeType<&str> { + if self.named { + NodeType::Named(self.type_name.as_str()) + } else { + NodeType::Anonymous(self.type_name.as_str()) + } + } +} + /// Cardinality constraints for a field or children slot. #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct NodeSlot { @@ -453,6 +462,16 @@ pub(crate) struct NodeKindRef { pub(crate) named: bool, } +impl NodeKindRef { + fn node_type(&self) -> NodeType<&str> { + if self.named { + NodeType::Named(self.type_name.as_str()) + } else { + NodeType::Anonymous(self.type_name.as_str()) + } + } +} + /// Error while resolving grammar-derived node shapes. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum NodeShapeBuildError { @@ -531,11 +550,11 @@ type NodeConstraintBuild = ( pub(crate) fn build_node_constraints( node_shapes: &[NodeShape], - node_id_for_name: F, + node_id_for_type: F, field_id_for_name: G, ) -> Result where - F: Fn(&str, bool) -> Option, + F: Fn(NodeType<&str>) -> Option, G: Fn(&str) -> Option, { let mut node_constraints = HashMap::new(); @@ -543,11 +562,11 @@ where let mut root_node_type = None; let known_shapes = node_shapes .iter() - .map(|shape| (shape.type_name.as_str(), shape.named)) + .map(NodeShape::node_type) .collect::>(); for shape in node_shapes { - let Some(node_id) = node_id_for_name(&shape.type_name, shape.named) else { + let Some(node_id) = node_id_for_type(shape.node_type()) else { continue; }; @@ -560,8 +579,8 @@ where } let fields = - build_field_constraints(shape, &known_shapes, &node_id_for_name, &field_id_for_name)?; - let children = build_children_constraints(shape, &known_shapes, &node_id_for_name)?; + build_field_constraints(shape, &known_shapes, &node_id_for_type, &field_id_for_name)?; + let children = build_children_constraints(shape, &known_shapes, &node_id_for_type)?; node_constraints.insert(node_id, NodeConstraints { fields, children }); } @@ -571,12 +590,12 @@ where fn build_field_constraints( shape: &NodeShape, - known_shapes: &HashSet<(&str, bool)>, - node_id_for_name: &F, + known_shapes: &HashSet>, + node_id_for_type: &F, field_id_for_name: &G, ) -> Result, NodeShapeBuildError> where - F: Fn(&str, bool) -> Option, + F: Fn(NodeType<&str>) -> Option, G: Fn(&str) -> Option, { let mut fields = HashMap::new(); @@ -586,7 +605,7 @@ where field: field_name.clone(), })?; - let valid_types = resolve_slot_types(slot, known_shapes, node_id_for_name, |kind_ref| { + let valid_types = resolve_slot_types(slot, known_shapes, node_id_for_type, |kind_ref| { NodeShapeBuildError::FieldType { node_kind: shape.type_name.clone(), field: field_name.clone(), @@ -612,18 +631,18 @@ where fn build_children_constraints( shape: &NodeShape, - known_shapes: &HashSet<(&str, bool)>, - node_id_for_name: &F, + known_shapes: &HashSet>, + node_id_for_type: &F, ) -> Result, NodeShapeBuildError> where - F: Fn(&str, bool) -> Option, + F: Fn(NodeType<&str>) -> Option, { shape .children .as_ref() .map(|slot| { let valid_types = - resolve_slot_types(slot, known_shapes, node_id_for_name, |kind_ref| { + resolve_slot_types(slot, known_shapes, node_id_for_type, |kind_ref| { NodeShapeBuildError::ChildType { node_kind: shape.type_name.clone(), kind: kind_ref.type_name.clone(), @@ -644,22 +663,23 @@ where fn resolve_slot_types( slot: &NodeSlot, - known_shapes: &HashSet<(&str, bool)>, - node_id_for_name: &F, + known_shapes: &HashSet>, + node_id_for_type: &F, error: E, ) -> Result, NodeShapeBuildError> where - F: Fn(&str, bool) -> Option, + F: Fn(NodeType<&str>) -> Option, E: Fn(&NodeKindRef) -> NodeShapeBuildError, { let mut resolved = Vec::new(); for kind_ref in &slot.types { - if let Some(node_id) = node_id_for_name(&kind_ref.type_name, kind_ref.named) { + let node_type = kind_ref.node_type(); + if let Some(node_id) = node_id_for_type(node_type) { resolved.push(node_id); continue; } - if known_shapes.contains(&(kind_ref.type_name.as_str(), kind_ref.named)) { + if known_shapes.contains(&node_type) { continue; } diff --git a/crates/plotnik-core/src/grammar/types_tests.rs b/crates/plotnik-core/src/grammar/types_tests.rs index f428ba76d..48bb9266a 100644 --- a/crates/plotnik-core/src/grammar/types_tests.rs +++ b/crates/plotnik-core/src/grammar/types_tests.rs @@ -1,12 +1,12 @@ use std::num::NonZeroU16; use super::types::{NodeKindRef, NodeShape, NodeShapeBuildError, NodeSlot, build_node_constraints}; -use crate::NodeTypeId; +use crate::{NodeType, NodeTypeId}; -fn node_id_for_name(kind: &str, named: bool) -> Option { - match (kind, named) { - ("root", true) => NonZeroU16::new(1), - ("child", true) => NonZeroU16::new(2), +fn node_id_for_type(node_type: NodeType<&str>) -> Option { + match node_type { + NodeType::Named("root") => NonZeroU16::new(1), + NodeType::Named("child") => NonZeroU16::new(2), _ => None, } } @@ -31,7 +31,7 @@ fn builds_node_constraints_from_node_shapes() { }]; let (node_constraints, _, root_node_type) = - build_node_constraints(&shapes, node_id_for_name, field_id_for_name) + build_node_constraints(&shapes, node_id_for_type, field_id_for_name) .expect("node shapes should resolve"); assert_eq!(root_node_type, NonZeroU16::new(1)); @@ -57,7 +57,7 @@ fn rejects_unknown_child_types() { subtypes: None, }]; - let err = build_node_constraints(&shapes, node_id_for_name, field_id_for_name) + let err = build_node_constraints(&shapes, node_id_for_type, field_id_for_name) .expect_err("unknown child type should fail"); assert_eq!( @@ -101,7 +101,7 @@ fn skips_known_abstract_child_shapes() { ]; let (node_constraints, _, _) = - build_node_constraints(&shapes, node_id_for_name, field_id_for_name) + build_node_constraints(&shapes, node_id_for_type, field_id_for_name) .expect("known abstract shapes should not be runtime node ids"); let root_id = NonZeroU16::new(1).unwrap(); diff --git a/crates/plotnik-core/src/interner.rs b/crates/plotnik-core/src/interner.rs index 87a176949..2360b3761 100644 --- a/crates/plotnik-core/src/interner.rs +++ b/crates/plotnik-core/src/interner.rs @@ -79,6 +79,11 @@ impl Interner { sym } + /// Return the symbol for an already-interned string. + pub fn get(&self, s: &str) -> Option { + self.map.get(s).copied() + } + /// Resolve a Symbol back to its string. /// /// # Panics diff --git a/crates/plotnik-core/src/lib.rs b/crates/plotnik-core/src/lib.rs index 9da768ef4..b6311aa3f 100644 --- a/crates/plotnik-core/src/lib.rs +++ b/crates/plotnik-core/src/lib.rs @@ -18,6 +18,13 @@ mod utils_tests; pub use colors::Colors; pub use interner::{Interner, Symbol}; +/// Concrete node type identity, preserving tree-sitter's named/anonymous namespace. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum NodeType { + Named(T), + Anonymous(T), +} + /// Node type ID (tree-sitter uses u16, but 0 is internal-only). pub type NodeTypeId = NonZeroU16; diff --git a/crates/plotnik-vm/src/engine/vm.rs b/crates/plotnik-vm/src/engine/vm.rs index dd2c5defb..b329e1505 100644 --- a/crates/plotnik-vm/src/engine/vm.rs +++ b/crates/plotnik-vm/src/engine/vm.rs @@ -409,8 +409,8 @@ impl<'t> VM<'t> { } } NodeTypeIR::Named(Some(expected)) => { - // Specific named type: check kind_id - if node.kind_id() != expected.get() { + // Specific named type: check namedness and kind_id + if !node.is_named() || node.kind_id() != expected.get() { tracer.trace_match_failure(node); return false; } @@ -423,8 +423,8 @@ impl<'t> VM<'t> { } } NodeTypeIR::Anonymous(Some(expected)) => { - // Specific anonymous type: check kind_id - if node.kind_id() != expected.get() { + // Specific anonymous type: check namedness and kind_id + if node.is_named() || node.kind_id() != expected.get() { tracer.trace_match_failure(node); return false; }