Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions crates/plotnik-bytecode/src/bytecode/node_type_ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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<NonZeroU16>),
/// 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<NonZeroU16>),
}

Expand Down
38 changes: 22 additions & 16 deletions crates/plotnik-compiler/src/analyze/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Symbol, NodeTypeId>,
/// Interned named/anonymous node type → NodeTypeId (for binary: StringId → NodeTypeId)
pub node_type_ids: IndexMap<NodeType<Symbol>, NodeTypeId>,
/// Interned name → NodeFieldId (for binary: StringId → NodeFieldId)
pub node_field_ids: IndexMap<Symbol, NodeFieldId>,
}
Expand All @@ -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<NodeTypeId>> = HashMap::new();
let mut node_type_ids: HashMap<NodeType<&'q str>, Option<NodeTypeId>> = HashMap::new();
let mut node_field_ids: HashMap<&'q str, Option<NodeFieldId>> = HashMap::new();

for (&source_id, root) in ast_map {
Expand All @@ -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<NodeTypeId>>,
node_type_ids: &'a mut HashMap<NodeType<&'q str>, Option<NodeTypeId>>,
node_field_ids: &'a mut HashMap<&'q str, Option<NodeFieldId>>,
output: &'a mut LinkOutput,
diagnostics: &'a mut Diagnostics,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
}

Expand Down
57 changes: 56 additions & 1 deletion crates/plotnik-compiler/src/analyze/link_tests.rs
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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#"
Expand Down
16 changes: 8 additions & 8 deletions crates/plotnik-compiler/src/bytecode/ir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,24 @@ 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.
#[default]
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<NonZeroU16>),
/// 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<NonZeroU16>),
}

Expand Down
40 changes: 40 additions & 0 deletions crates/plotnik-compiler/src/compile/compile_tests.rs
Original file line number Diff line number Diff line change
@@ -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]
Expand All @@ -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)}");
Expand Down
4 changes: 2 additions & 2 deletions crates/plotnik-compiler/src/compile/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -31,7 +31,7 @@ pub struct CompileCtx<'a> {
pub type_ctx: &'a TypeContext,
pub symbol_table: &'a SymbolTable,
pub strings: &'a RefCell<StringTableBuilder>,
pub node_types: Option<&'a IndexMap<Symbol, NodeTypeId>>,
pub node_types: Option<&'a IndexMap<NodeType<Symbol>, NodeTypeId>>,
pub node_fields: Option<&'a IndexMap<Symbol, NodeFieldId>>,
}

Expand Down
29 changes: 15 additions & 14 deletions crates/plotnik-compiler/src/compile/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
14 changes: 9 additions & 5 deletions crates/plotnik-compiler/src/compile/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<Symbol, plotnik_core::NodeTypeId>>,
node_types: Option<&indexmap::IndexMap<NodeType<Symbol>, plotnik_core::NodeTypeId>>,
interner: &plotnik_core::Interner,
) -> Option<String> {
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
}
Expand Down
7 changes: 5 additions & 2 deletions crates/plotnik-compiler/src/emit/emitter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -51,7 +51,10 @@ pub fn emit(query: &LinkedQuery) -> Result<Vec<u8>, EmitError> {

// Collect node symbols
let mut node_symbols: Vec<NodeSymbol> = 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));
}
Expand Down
Loading
Loading