diff --git a/crates/plotnik-cli/src/commands/exec.rs b/crates/plotnik-cli/src/commands/exec.rs index 1d2e2857..af555177 100644 --- a/crates/plotnik-cli/src/commands/exec.rs +++ b/crates/plotnik-cli/src/commands/exec.rs @@ -3,9 +3,7 @@ use std::path::PathBuf; use plotnik_lib::Colors; -use plotnik_lib::engine::{ - FuelLimits, Materializer, RuntimeError, VM, ValueMaterializer, debug_verify_type, -}; +use plotnik_lib::engine::{Materializer, RuntimeError, VM, ValueMaterializer, debug_verify_type}; use super::run_common::{self, PreparedQuery, QueryInput}; @@ -37,7 +35,7 @@ pub fn run(args: ExecArgs) { color: args.color, }); - let vm = VM::new(&tree, trivia_types, FuelLimits::default()); + let vm = VM::builder(&tree).trivia_types(trivia_types).build(); let effects = match vm.execute(&module, 0, &entrypoint) { Ok(effects) => effects, Err(RuntimeError::NoMatch) => { @@ -50,12 +48,12 @@ pub fn run(args: ExecArgs) { }; let materializer = ValueMaterializer::new(&source_code, module.types(), module.strings()); - let value = materializer.materialize(effects.as_slice(), entrypoint.result_type); + let value = materializer.materialize(effects.as_slice(), entrypoint.result_type()); let colors = Colors::new(args.color); // Debug-only: verify output matches declared type - debug_verify_type(&value, entrypoint.result_type, &module, colors); + debug_verify_type(&value, entrypoint.result_type(), &module, colors); let output = value.format(args.pretty, colors); println!("{}", output); diff --git a/crates/plotnik-cli/src/commands/infer.rs b/crates/plotnik-cli/src/commands/infer.rs index a0140c27..c8e5aaf6 100644 --- a/crates/plotnik-cli/src/commands/infer.rs +++ b/crates/plotnik-cli/src/commands/infer.rs @@ -2,7 +2,6 @@ use std::fs; use std::io::{self, Write}; use std::path::PathBuf; -use plotnik_lib::Colors; use plotnik_lib::QueryBuilder; use plotnik_lib::bytecode::Module; use plotnik_lib::typegen::typescript; @@ -106,13 +105,12 @@ pub fn run(args: InferArgs) { }; // Only use colors when outputting to stdout (not to file) let use_colors = args.color && args.output.is_none(); - let config = typescript::Config { - export: args.export, - emit_node_type: !args.no_node_type, - verbose_nodes: args.verbose_nodes, - void_type, - colors: Colors::new(use_colors), - }; + let config = typescript::Config::new() + .export(args.export) + .emit_node_type(!args.no_node_type) + .verbose_nodes(args.verbose_nodes) + .void_type(void_type) + .colored(use_colors); let output = typescript::emit_with_config(&module, config); // Write output diff --git a/crates/plotnik-cli/src/commands/run_common.rs b/crates/plotnik-cli/src/commands/run_common.rs index f6dee92c..77333813 100644 --- a/crates/plotnik-cli/src/commands/run_common.rs +++ b/crates/plotnik-cli/src/commands/run_common.rs @@ -122,7 +122,7 @@ pub fn validate( pub fn build_trivia_types(module: &Module) -> Vec { let trivia_view = module.trivia(); (0..trivia_view.len()) - .map(|i| trivia_view.get(i).node_type) + .map(|i| trivia_view.get(i).node_type()) .collect() } diff --git a/crates/plotnik-cli/src/commands/trace.rs b/crates/plotnik-cli/src/commands/trace.rs index 902a5dc4..e8e9a1a2 100644 --- a/crates/plotnik-cli/src/commands/trace.rs +++ b/crates/plotnik-cli/src/commands/trace.rs @@ -4,8 +4,7 @@ use std::path::PathBuf; use plotnik_lib::Colors; use plotnik_lib::engine::{ - FuelLimits, Materializer, PrintTracer, RuntimeError, VM, ValueMaterializer, Verbosity, - debug_verify_type, + Materializer, PrintTracer, RuntimeError, VM, ValueMaterializer, Verbosity, debug_verify_type, }; use super::run_common::{self, PreparedQuery, QueryInput}; @@ -40,13 +39,15 @@ pub fn run(args: TraceArgs) { color: args.color, }); - let limits = FuelLimits { - exec_fuel: args.fuel, - ..Default::default() - }; - let vm = VM::new(&tree, trivia_types, limits); + let vm = VM::builder(&tree) + .trivia_types(trivia_types) + .exec_fuel(args.fuel) + .build(); let colors = Colors::new(args.color); - let mut tracer = PrintTracer::new(&source_code, &module, args.verbosity, colors); + let mut tracer = PrintTracer::builder(&source_code, &module) + .verbosity(args.verbosity) + .colored(args.color) + .build(); let effects = match vm.execute_with(&module, 0, &entrypoint, &mut tracer) { Ok(effects) => { @@ -70,10 +71,10 @@ pub fn run(args: TraceArgs) { println!("{}---{}", colors.dim, colors.reset); let materializer = ValueMaterializer::new(&source_code, module.types(), module.strings()); - let value = materializer.materialize(effects.as_slice(), entrypoint.result_type); + let value = materializer.materialize(effects.as_slice(), entrypoint.result_type()); // Debug-only: verify output matches declared type - debug_verify_type(&value, entrypoint.result_type, &module, colors); + debug_verify_type(&value, entrypoint.result_type(), &module, colors); let output = value.format(true, colors); println!("{}", output); diff --git a/crates/plotnik-lib/src/bytecode/dump.rs b/crates/plotnik-lib/src/bytecode/dump.rs index 759373f4..2fd6f068 100644 --- a/crates/plotnik-lib/src/bytecode/dump.rs +++ b/crates/plotnik-lib/src/bytecode/dump.rs @@ -12,7 +12,7 @@ use super::format::{LineBuilder, Symbol, format_effect, nav_symbol_epsilon, widt use super::ids::TypeId; use super::instructions::StepId; use super::module::{Instruction, Module}; -use super::type_meta::TypeKind; +use super::type_meta::{TypeData, TypeKind}; use super::{Call, Match, Return, Trampoline}; /// Generate a human-readable dump of the bytecode module. @@ -79,20 +79,20 @@ impl DumpContext { step_labels.insert(0, "_ObjWrap".to_string()); for i in 0..entrypoints.len() { let ep = entrypoints.get(i); - let name = strings.get(ep.name).to_string(); - step_labels.insert(ep.target, name); + let name = strings.get(ep.name()).to_string(); + step_labels.insert(ep.target(), name); } let mut node_type_names = BTreeMap::new(); for i in 0..node_types.len() { let t = node_types.get(i); - node_type_names.insert(t.id, strings.get(t.name).to_string()); + node_type_names.insert(t.id(), strings.get(t.name()).to_string()); } let mut node_field_names = BTreeMap::new(); for i in 0..node_fields.len() { let f = node_fields.get(i); - node_field_names.insert(f.id, strings.get(f.name).to_string()); + node_field_names.insert(f.id(), strings.get(f.name()).to_string()); } // Collect all strings for unlinked mode lookups @@ -182,53 +182,74 @@ fn dump_types_defs(out: &mut String, module: &Module, ctx: &DumpContext) { // All types are now in type_defs, including builtins for i in 0..types.defs_count() { let def = types.get_def(i); - let kind = def.type_kind().expect("valid type kind"); - - let formatted = match kind { - // Primitive types - TypeKind::Void => "".to_string(), - TypeKind::Node => "".to_string(), - TypeKind::String => "".to_string(), - // Composite types - TypeKind::Struct => format!("Struct M{:0mw$}:{}", def.data, def.count), - TypeKind::Enum => format!("Enum M{:0mw$}:{}", def.data, def.count), - // Wrapper types - TypeKind::Optional => format!("Optional(T{:0tw$})", def.data), - TypeKind::ArrayZeroOrMore => format!("ArrayStar(T{:0tw$})", def.data), - TypeKind::ArrayOneOrMore => format!("ArrayPlus(T{:0tw$})", def.data), - TypeKind::Alias => format!("Alias(T{:0tw$})", def.data), - }; - // Generate comment for non-primitives (comments are dim) - let comment = match kind { - TypeKind::Void | TypeKind::Node | TypeKind::String => String::new(), - TypeKind::Struct => { - let fields: Vec<_> = types - .members_of(&def) - .map(|m| strings.get(m.name).to_string()) - .collect(); - format!("{} ; {{ {} }}{}", c.dim, fields.join(", "), c.reset) - } - TypeKind::Enum => { - let variants: Vec<_> = types - .members_of(&def) - .map(|m| strings.get(m.name).to_string()) - .collect(); - format!("{} ; {}{}", c.dim, variants.join(" | "), c.reset) - } - TypeKind::Optional => { - let inner_name = format_type_name(TypeId(def.data), module, ctx); - format!("{} ; {}?{}", c.dim, inner_name, c.reset) + let (formatted, comment) = match def.classify() { + TypeData::Primitive(kind) => { + let name = match kind { + TypeKind::Void => "", + TypeKind::Node => "", + TypeKind::String => "", + _ => unreachable!(), + }; + (name.to_string(), String::new()) } - TypeKind::ArrayZeroOrMore => { - let inner_name = format_type_name(TypeId(def.data), module, ctx); - format!("{} ; {}*{}", c.dim, inner_name, c.reset) + TypeData::Wrapper { kind, inner } => { + let formatted = match kind { + TypeKind::Optional => format!("Optional(T{:0tw$})", inner.0), + TypeKind::ArrayZeroOrMore => format!("ArrayStar(T{:0tw$})", inner.0), + TypeKind::ArrayOneOrMore => format!("ArrayPlus(T{:0tw$})", inner.0), + TypeKind::Alias => format!("Alias(T{:0tw$})", inner.0), + _ => unreachable!(), + }; + let comment = match kind { + TypeKind::Optional => { + let inner_name = format_type_name(inner, module, ctx); + format!("{} ; {}?{}", c.dim, inner_name, c.reset) + } + TypeKind::ArrayZeroOrMore => { + let inner_name = format_type_name(inner, module, ctx); + format!("{} ; {}*{}", c.dim, inner_name, c.reset) + } + TypeKind::ArrayOneOrMore => { + let inner_name = format_type_name(inner, module, ctx); + format!("{} ; {}+{}", c.dim, inner_name, c.reset) + } + TypeKind::Alias => String::new(), + _ => unreachable!(), + }; + (formatted, comment) } - TypeKind::ArrayOneOrMore => { - let inner_name = format_type_name(TypeId(def.data), module, ctx); - format!("{} ; {}+{}", c.dim, inner_name, c.reset) + TypeData::Composite { + kind, + member_start, + member_count, + } => { + let formatted = match kind { + TypeKind::Struct => { + format!("Struct M{:0mw$}:{}", member_start, member_count) + } + TypeKind::Enum => format!("Enum M{:0mw$}:{}", member_start, member_count), + _ => unreachable!(), + }; + let comment = match kind { + TypeKind::Struct => { + let fields: Vec<_> = types + .members_of(&def) + .map(|m| strings.get(m.name()).to_string()) + .collect(); + format!("{} ; {{ {} }}{}", c.dim, fields.join(", "), c.reset) + } + TypeKind::Enum => { + let variants: Vec<_> = types + .members_of(&def) + .map(|m| strings.get(m.name()).to_string()) + .collect(); + format!("{} ; {}{}", c.dim, variants.join(" | "), c.reset) + } + _ => unreachable!(), + }; + (formatted, comment) } - TypeKind::Alias => String::new(), }; writeln!(out, "T{i:0tw$} = {formatted}{comment}").unwrap(); @@ -288,7 +309,7 @@ fn format_type_name(type_id: TypeId, module: &Module, ctx: &DumpContext) -> Stri // Check if it's a primitive type if let Some(def) = types.get(type_id) - && let Some(kind) = def.type_kind() + && let TypeData::Primitive(kind) = def.classify() && let Some(name) = kind.primitive_name() { return format!("<{}>", name); @@ -297,8 +318,8 @@ fn format_type_name(type_id: TypeId, module: &Module, ctx: &DumpContext) -> Stri // Try to find a name in types.names for i in 0..types.names_count() { let entry = types.get_name(i); - if entry.type_id == type_id { - return strings.get(entry.name).to_string(); + if entry.type_id() == type_id { + return strings.get(entry.name()).to_string(); } } @@ -320,8 +341,8 @@ fn dump_entrypoints(out: &mut String, module: &Module, ctx: &DumpContext) { let mut entries: Vec<_> = (0..entrypoints.len()) .map(|i| { let ep = entrypoints.get(i); - let name = strings.get(ep.name); - (name, ep.target, ep.result_type.0) + let name = strings.get(ep.name()); + (name, ep.target(), ep.result_type().0) }) .collect(); entries.sort_by_key(|(name, _, _)| *name); diff --git a/crates/plotnik-lib/src/bytecode/effects.rs b/crates/plotnik-lib/src/bytecode/effects.rs index a78cd13b..c4adad9c 100644 --- a/crates/plotnik-lib/src/bytecode/effects.rs +++ b/crates/plotnik-lib/src/bytecode/effects.rs @@ -43,11 +43,16 @@ impl EffectOpcode { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct EffectOp { - pub opcode: EffectOpcode, - pub payload: usize, + pub(crate) opcode: EffectOpcode, + pub(crate) payload: usize, } impl EffectOp { + /// Create a new effect operation. + pub fn new(opcode: EffectOpcode, payload: usize) -> Self { + Self { opcode, payload } + } + pub fn from_bytes(bytes: [u8; 2]) -> Self { let raw = u16::from_le_bytes(bytes); let opcode = EffectOpcode::from_u8((raw >> 10) as u8); @@ -64,4 +69,11 @@ impl EffectOp { let raw = ((self.opcode as u16) << 10) | ((self.payload as u16) & 0x3FF); raw.to_le_bytes() } + + pub fn opcode(&self) -> EffectOpcode { + self.opcode + } + pub fn payload(&self) -> usize { + self.payload + } } diff --git a/crates/plotnik-lib/src/bytecode/effects_tests.rs b/crates/plotnik-lib/src/bytecode/effects_tests.rs index 720be22c..dac1b114 100644 --- a/crates/plotnik-lib/src/bytecode/effects_tests.rs +++ b/crates/plotnik-lib/src/bytecode/effects_tests.rs @@ -2,37 +2,28 @@ use super::*; #[test] fn roundtrip_with_payload() { - let op = EffectOp { - opcode: EffectOpcode::Set, - payload: 42, - }; + let op = EffectOp::new(EffectOpcode::Set, 42); let bytes = op.to_bytes(); let decoded = EffectOp::from_bytes(bytes); - assert_eq!(decoded.opcode, EffectOpcode::Set); - assert_eq!(decoded.payload, 42); + assert_eq!(decoded.opcode(), EffectOpcode::Set); + assert_eq!(decoded.payload(), 42); } #[test] fn roundtrip_no_payload() { - let op = EffectOp { - opcode: EffectOpcode::Node, - payload: 0, - }; + let op = EffectOp::new(EffectOpcode::Node, 0); let bytes = op.to_bytes(); let decoded = EffectOp::from_bytes(bytes); - assert_eq!(decoded.opcode, EffectOpcode::Node); - assert_eq!(decoded.payload, 0); + assert_eq!(decoded.opcode(), EffectOpcode::Node); + assert_eq!(decoded.payload(), 0); } #[test] fn max_payload() { - let op = EffectOp { - opcode: EffectOpcode::Enum, - payload: 1023, - }; + let op = EffectOp::new(EffectOpcode::Enum, 1023); let bytes = op.to_bytes(); let decoded = EffectOp::from_bytes(bytes); - assert_eq!(decoded.payload, 1023); + assert_eq!(decoded.payload(), 1023); } #[test] diff --git a/crates/plotnik-lib/src/bytecode/entrypoint.rs b/crates/plotnik-lib/src/bytecode/entrypoint.rs index a7e05424..0536de5d 100644 --- a/crates/plotnik-lib/src/bytecode/entrypoint.rs +++ b/crates/plotnik-lib/src/bytecode/entrypoint.rs @@ -8,12 +8,44 @@ use super::{StringId, TypeId}; #[repr(C)] pub struct Entrypoint { /// Definition name. - pub name: StringId, + pub(crate) name: StringId, /// Starting instruction address. - pub target: StepAddr, + pub(crate) target: StepAddr, /// Result type. - pub result_type: TypeId, + pub(crate) result_type: TypeId, pub(crate) _pad: u16, } const _: () = assert!(std::mem::size_of::() == 8); + +impl Entrypoint { + /// Create a new entrypoint. + pub fn new(name: StringId, target: StepAddr, result_type: TypeId) -> Self { + Self { + name, + target, + result_type, + _pad: 0, + } + } + + /// Decode from 8 bytes (crate-internal deserialization). + pub(crate) fn from_bytes(bytes: &[u8]) -> Self { + Self { + name: StringId::new(u16::from_le_bytes([bytes[0], bytes[1]])), + target: u16::from_le_bytes([bytes[2], bytes[3]]), + result_type: TypeId(u16::from_le_bytes([bytes[4], bytes[5]])), + _pad: 0, + } + } + + pub fn name(&self) -> StringId { + self.name + } + pub fn target(&self) -> StepAddr { + self.target + } + pub fn result_type(&self) -> TypeId { + self.result_type + } +} diff --git a/crates/plotnik-lib/src/bytecode/instructions.rs b/crates/plotnik-lib/src/bytecode/instructions.rs index 701dd4f0..c53dfecf 100644 --- a/crates/plotnik-lib/src/bytecode/instructions.rs +++ b/crates/plotnik-lib/src/bytecode/instructions.rs @@ -283,20 +283,31 @@ impl<'a> Match<'a> { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Call { /// Segment index (0-15). - pub segment: u8, + pub(crate) segment: u8, /// Navigation to apply before jumping to target. - pub nav: Nav, + pub(crate) nav: Nav, /// Field constraint (None = no constraint). - pub node_field: Option, + pub(crate) node_field: Option, /// Return address (current segment). - pub next: StepId, + pub(crate) next: StepId, /// Callee entry point (target segment from type_id). - pub target: StepId, + pub(crate) target: StepId, } impl Call { + /// Create a new Call instruction. + pub fn new(nav: Nav, node_field: Option, next: StepId, target: StepId) -> Self { + Self { + segment: 0, + nav, + node_field, + next, + target, + } + } + /// Decode from 8-byte bytecode. - pub fn from_bytes(bytes: [u8; 8]) -> Self { + pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self { let type_id_byte = bytes[0]; let segment = type_id_byte >> 4; assert!( @@ -325,18 +336,36 @@ impl Call { bytes[6..8].copy_from_slice(&self.target.get().to_le_bytes()); bytes } + + pub fn nav(&self) -> Nav { + self.nav + } + pub fn node_field(&self) -> Option { + self.node_field + } + pub fn next(&self) -> StepId { + self.next + } + pub fn target(&self) -> StepId { + self.target + } } /// Return instruction for returning from definitions. #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Return { /// Segment index (0-15). - pub segment: u8, + pub(crate) segment: u8, } impl Return { + /// Create a new Return instruction. + pub fn new() -> Self { + Self { segment: 0 } + } + /// Decode from 8-byte bytecode. - pub fn from_bytes(bytes: [u8; 8]) -> Self { + pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self { let type_id_byte = bytes[0]; let segment = type_id_byte >> 4; assert!( @@ -358,6 +387,12 @@ impl Return { } } +impl Default for Return { + fn default() -> Self { + Self::new() + } +} + /// Trampoline instruction for universal entry. /// /// Like Call, but the target comes from VM context (external parameter) @@ -366,14 +401,19 @@ impl Return { #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub struct Trampoline { /// Segment index (0-15). - pub segment: u8, + pub(crate) segment: u8, /// Return address (where to continue after entrypoint returns). - pub next: StepId, + pub(crate) next: StepId, } impl Trampoline { + /// Create a new Trampoline instruction. + pub fn new(next: StepId) -> Self { + Self { segment: 0, next } + } + /// Decode from 8-byte bytecode. - pub fn from_bytes(bytes: [u8; 8]) -> Self { + pub(crate) fn from_bytes(bytes: [u8; 8]) -> Self { let type_id_byte = bytes[0]; let segment = type_id_byte >> 4; assert!( @@ -398,6 +438,10 @@ impl Trampoline { // bytes[4..8] are reserved/padding bytes } + + pub fn next(&self) -> StepId { + self.next + } } /// Select the smallest Match variant that fits the given payload. diff --git a/crates/plotnik-lib/src/bytecode/instructions_tests.rs b/crates/plotnik-lib/src/bytecode/instructions_tests.rs index 57bdeae2..ffdfcdeb 100644 --- a/crates/plotnik-lib/src/bytecode/instructions_tests.rs +++ b/crates/plotnik-lib/src/bytecode/instructions_tests.rs @@ -63,13 +63,12 @@ fn align_to_section_works() { #[test] fn call_roundtrip() { - let c = Call { - segment: 0, - nav: Nav::Down, - node_field: NonZeroU16::new(42), - next: StepId::new(100), - target: StepId::new(500), - }; + let c = Call::new( + Nav::Down, + NonZeroU16::new(42), + StepId::new(100), + StepId::new(500), + ); let bytes = c.to_bytes(); let decoded = Call::from_bytes(bytes); @@ -78,7 +77,7 @@ fn call_roundtrip() { #[test] fn return_roundtrip() { - let r = Return { segment: 0 }; + let r = Return::new(); let bytes = r.to_bytes(); let decoded = Return::from_bytes(bytes); diff --git a/crates/plotnik-lib/src/bytecode/ir.rs b/crates/plotnik-lib/src/bytecode/ir.rs index 5369aa8d..a5fd70b8 100644 --- a/crates/plotnik-lib/src/bytecode/ir.rs +++ b/crates/plotnik-lib/src/bytecode/ir.rs @@ -206,10 +206,7 @@ impl EffectIR { } else { self.payload }; - EffectOp { - opcode: self.opcode, - payload, - } + EffectOp::new(self.opcode, payload) } } @@ -542,14 +539,13 @@ impl CallIR { /// Resolve labels and serialize to bytecode bytes. pub fn resolve(&self, map: &BTreeMap) -> [u8; 8] { - let c = Call { - segment: 0, - nav: self.nav, - node_field: self.node_field, - next: StepId::new(self.next.resolve(map)), - target: StepId::new(self.target.resolve(map)), - }; - c.to_bytes() + Call::new( + self.nav, + self.node_field, + StepId::new(self.next.resolve(map)), + StepId::new(self.target.resolve(map)), + ) + .to_bytes() } } @@ -574,8 +570,7 @@ impl ReturnIR { /// Serialize to bytecode bytes (no labels to resolve). pub fn resolve(&self) -> [u8; 8] { - let r = Return { segment: 0 }; - r.to_bytes() + Return::new().to_bytes() } } @@ -605,11 +600,7 @@ impl TrampolineIR { /// Resolve labels and serialize to bytecode bytes. pub fn resolve(&self, map: &BTreeMap) -> [u8; 8] { - let t = Trampoline { - segment: 0, - next: StepId::new(self.next.resolve(map)), - }; - t.to_bytes() + Trampoline::new(StepId::new(self.next.resolve(map))).to_bytes() } } @@ -623,7 +614,32 @@ impl From for InstructionIR { #[derive(Clone, Debug)] pub struct LayoutResult { /// Mapping from symbolic labels to concrete step addresses (raw u16). - pub label_to_step: BTreeMap, + pub(crate) label_to_step: BTreeMap, /// Total number of steps (for header). - pub total_steps: u16, + pub(crate) total_steps: u16, +} + +impl LayoutResult { + /// Create a new layout result. + pub fn new(label_to_step: BTreeMap, total_steps: u16) -> Self { + Self { + label_to_step, + total_steps, + } + } + + /// Create an empty layout result. + pub fn empty() -> Self { + Self { + label_to_step: BTreeMap::new(), + total_steps: 0, + } + } + + pub fn label_to_step(&self) -> &BTreeMap { + &self.label_to_step + } + pub fn total_steps(&self) -> u16 { + self.total_steps + } } diff --git a/crates/plotnik-lib/src/bytecode/mod.rs b/crates/plotnik-lib/src/bytecode/mod.rs index 0b6af1cf..8a85e860 100644 --- a/crates/plotnik-lib/src/bytecode/mod.rs +++ b/crates/plotnik-lib/src/bytecode/mod.rs @@ -28,7 +28,7 @@ pub use sections::{FieldSymbol, NodeSymbol, Slice, TriviaEntry}; pub use entrypoint::Entrypoint; -pub use type_meta::{TypeDef, TypeKind, TypeMember, TypeMetaHeader, TypeName}; +pub use type_meta::{TypeData, TypeDef, TypeKind, TypeMember, TypeMetaHeader, TypeName}; pub use nav::Nav; diff --git a/crates/plotnik-lib/src/bytecode/module.rs b/crates/plotnik-lib/src/bytecode/module.rs index f965d1c9..79c538a1 100644 --- a/crates/plotnik-lib/src/bytecode/module.rs +++ b/crates/plotnik-lib/src/bytecode/module.rs @@ -11,7 +11,7 @@ use super::header::Header; use super::ids::{StringId, TypeId}; use super::instructions::{Call, Match, Opcode, Return, Trampoline}; use super::sections::{FieldSymbol, NodeSymbol, TriviaEntry}; -use super::type_meta::{TypeDef, TypeMember, TypeMetaHeader, TypeName}; +use super::type_meta::{TypeData, TypeDef, TypeKind, TypeMember, TypeMetaHeader, TypeName}; use super::{Entrypoint, SECTION_ALIGN, STEP_SIZE, VERSION}; /// Read a little-endian u16 from bytes at the given offset. @@ -295,10 +295,10 @@ impl<'a> SymbolsView<'a, NodeSymbol> { pub fn get(&self, idx: usize) -> NodeSymbol { assert!(idx < self.count, "node symbol index out of bounds"); let offset = idx * 4; - NodeSymbol { - id: read_u16_le(self.bytes, offset), - name: StringId::new(read_u16_le(self.bytes, offset + 2)), - } + NodeSymbol::new( + read_u16_le(self.bytes, offset), + StringId::new(read_u16_le(self.bytes, offset + 2)), + ) } /// Number of entries. @@ -317,10 +317,10 @@ impl<'a> SymbolsView<'a, FieldSymbol> { pub fn get(&self, idx: usize) -> FieldSymbol { assert!(idx < self.count, "field symbol index out of bounds"); let offset = idx * 4; - FieldSymbol { - id: read_u16_le(self.bytes, offset), - name: StringId::new(read_u16_le(self.bytes, offset + 2)), - } + FieldSymbol::new( + read_u16_le(self.bytes, offset), + StringId::new(read_u16_le(self.bytes, offset + 2)), + ) } /// Number of entries. @@ -344,9 +344,7 @@ impl<'a> TriviaView<'a> { /// Get a trivia entry by index. pub fn get(&self, idx: usize) -> TriviaEntry { assert!(idx < self.count, "trivia index out of bounds"); - TriviaEntry { - node_type: read_u16_le(self.bytes, idx * 2), - } + TriviaEntry::new(read_u16_le(self.bytes, idx * 2)) } /// Number of entries. @@ -361,7 +359,7 @@ impl<'a> TriviaView<'a> { /// Check if a node type is trivia. pub fn contains(&self, node_type: u16) -> bool { - (0..self.count).any(|i| self.get(i).node_type == node_type) + (0..self.count).any(|i| self.get(i).node_type() == node_type) } } @@ -385,11 +383,7 @@ impl<'a> TypesView<'a> { pub fn get_def(&self, idx: usize) -> TypeDef { assert!(idx < self.defs_count, "type def index out of bounds"); let offset = idx * 4; - TypeDef { - data: read_u16_le(self.defs_bytes, offset), - count: self.defs_bytes[offset + 2], - kind: self.defs_bytes[offset + 3], - } + TypeDef::from_bytes(&self.defs_bytes[offset..]) } /// Get a type definition by TypeId. @@ -406,20 +400,20 @@ impl<'a> TypesView<'a> { pub fn get_member(&self, idx: usize) -> TypeMember { assert!(idx < self.members_count, "type member index out of bounds"); let offset = idx * 4; - TypeMember { - name: StringId::new(read_u16_le(self.members_bytes, offset)), - type_id: TypeId(read_u16_le(self.members_bytes, offset + 2)), - } + TypeMember::new( + StringId::new(read_u16_le(self.members_bytes, offset)), + TypeId(read_u16_le(self.members_bytes, offset + 2)), + ) } /// Get a type name entry by index. pub fn get_name(&self, idx: usize) -> TypeName { assert!(idx < self.names_count, "type name index out of bounds"); let offset = idx * 4; - TypeName { - name: StringId::new(read_u16_le(self.names_bytes, offset)), - type_id: TypeId(read_u16_le(self.names_bytes, offset + 2)), - } + TypeName::new( + StringId::new(read_u16_le(self.names_bytes, offset)), + TypeId(read_u16_le(self.names_bytes, offset + 2)), + ) } /// Number of type definitions. @@ -439,8 +433,14 @@ impl<'a> TypesView<'a> { /// Iterate over members of a struct or enum type. pub fn members_of(&self, def: &TypeDef) -> impl Iterator + '_ { - let start = def.data as usize; - let count = def.count as usize; + let (start, count) = match def.classify() { + TypeData::Composite { + member_start, + member_count, + .. + } => (member_start as usize, member_count as usize), + _ => (0, 0), + }; (0..count).map(move |i| self.get_member(start + i)) } @@ -450,10 +450,13 @@ impl<'a> TypesView<'a> { let Some(type_def) = self.get(type_id) else { return (type_id, false); }; - if !type_def.is_optional() { - return (type_id, false); + match type_def.classify() { + TypeData::Wrapper { + kind: TypeKind::Optional, + inner, + } => (inner, true), + _ => (type_id, false), } - (TypeId(type_def.data), true) } } @@ -468,12 +471,7 @@ impl<'a> EntrypointsView<'a> { pub fn get(&self, idx: usize) -> Entrypoint { assert!(idx < self.count, "entrypoint index out of bounds"); let offset = idx * 8; - Entrypoint { - name: StringId::new(read_u16_le(self.bytes, offset)), - target: read_u16_le(self.bytes, offset + 2), - result_type: TypeId(read_u16_le(self.bytes, offset + 4)), - _pad: 0, - } + Entrypoint::from_bytes(&self.bytes[offset..]) } /// Number of entrypoints. @@ -490,6 +488,6 @@ impl<'a> EntrypointsView<'a> { pub fn find_by_name(&self, name: &str, strings: &StringsView<'_>) -> Option { (0..self.count) .map(|i| self.get(i)) - .find(|e| strings.get(e.name) == name) + .find(|e| strings.get(e.name()) == name) } } diff --git a/crates/plotnik-lib/src/bytecode/sections.rs b/crates/plotnik-lib/src/bytecode/sections.rs index 0fff5f40..0d950e9e 100644 --- a/crates/plotnik-lib/src/bytecode/sections.rs +++ b/crates/plotnik-lib/src/bytecode/sections.rs @@ -27,9 +27,23 @@ impl Slice { #[repr(C)] pub struct NodeSymbol { /// Tree-sitter node type ID - pub id: u16, + pub(crate) id: u16, /// StringId for the node kind name - pub name: StringId, + pub(crate) name: StringId, +} + +impl NodeSymbol { + /// Create a new node symbol. + pub fn new(id: u16, name: StringId) -> Self { + Self { id, name } + } + + pub fn id(&self) -> u16 { + self.id + } + pub fn name(&self) -> StringId { + self.name + } } /// Maps tree-sitter NodeFieldId to its string name. @@ -37,14 +51,39 @@ pub struct NodeSymbol { #[repr(C)] pub struct FieldSymbol { /// Tree-sitter field ID - pub id: u16, + pub(crate) id: u16, /// StringId for the field name - pub name: StringId, + pub(crate) name: StringId, +} + +impl FieldSymbol { + /// Create a new field symbol. + pub fn new(id: u16, name: StringId) -> Self { + Self { id, name } + } + + pub fn id(&self) -> u16 { + self.id + } + pub fn name(&self) -> StringId { + self.name + } } /// A node type ID that counts as trivia (whitespace, comments). #[derive(Clone, Copy, Debug)] #[repr(C)] pub struct TriviaEntry { - pub node_type: u16, + pub(crate) node_type: u16, +} + +impl TriviaEntry { + /// Create a new trivia entry. + pub fn new(node_type: u16) -> Self { + Self { node_type } + } + + pub fn node_type(&self) -> u16 { + self.node_type + } } diff --git a/crates/plotnik-lib/src/bytecode/type_meta.rs b/crates/plotnik-lib/src/bytecode/type_meta.rs index 904f50d9..25b090bf 100644 --- a/crates/plotnik-lib/src/bytecode/type_meta.rs +++ b/crates/plotnik-lib/src/bytecode/type_meta.rs @@ -20,11 +20,11 @@ impl TypeKind { #[repr(C)] pub struct TypeMetaHeader { /// Number of TypeDef entries. - pub type_defs_count: u16, + pub(crate) type_defs_count: u16, /// Number of TypeMember entries. - pub type_members_count: u16, + pub(crate) type_members_count: u16, /// Number of TypeName entries. - pub type_names_count: u16, + pub(crate) type_names_count: u16, /// Padding for alignment. pub(crate) _pad: u16, } @@ -32,6 +32,16 @@ pub struct TypeMetaHeader { const _: () = assert!(std::mem::size_of::() == 8); impl TypeMetaHeader { + /// Create a new header. + pub fn new(type_defs_count: u16, type_members_count: u16, type_names_count: u16) -> Self { + Self { + type_defs_count, + type_members_count, + type_names_count, + _pad: 0, + } + } + /// Decode from 8 bytes. pub fn from_bytes(bytes: &[u8]) -> Self { assert!(bytes.len() >= 8, "TypeMetaHeader too short"); @@ -39,7 +49,7 @@ impl TypeMetaHeader { type_defs_count: u16::from_le_bytes([bytes[0], bytes[1]]), type_members_count: u16::from_le_bytes([bytes[2], bytes[3]]), type_names_count: u16::from_le_bytes([bytes[4], bytes[5]]), - _pad: u16::from_le_bytes([bytes[6], bytes[7]]), + _pad: 0, } } @@ -49,9 +59,19 @@ impl TypeMetaHeader { bytes[0..2].copy_from_slice(&self.type_defs_count.to_le_bytes()); bytes[2..4].copy_from_slice(&self.type_members_count.to_le_bytes()); bytes[4..6].copy_from_slice(&self.type_names_count.to_le_bytes()); - bytes[6..8].copy_from_slice(&self._pad.to_le_bytes()); + // _pad is always 0 bytes } + + pub fn type_defs_count(&self) -> u16 { + self.type_defs_count + } + pub fn type_members_count(&self) -> u16 { + self.type_members_count + } + pub fn type_names_count(&self) -> u16 { + self.type_names_count + } } /// Type definition entry (4 bytes). @@ -65,64 +85,137 @@ impl TypeMetaHeader { pub struct TypeDef { /// For wrappers/alias: inner/target TypeId. /// For Struct/Enum: index into TypeMembers section. - pub data: u16, + data: u16, /// Member count (0 for wrappers/alias, field/variant count for composites). - pub count: u8, + count: u8, /// TypeKind discriminant. - pub kind: u8, + kind: u8, } const _: () = assert!(std::mem::size_of::() == 4); +/// Structured view of TypeDef data, eliminating the need for Option-returning accessors. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TypeData { + /// Primitive types: Void, Node, String. + Primitive(TypeKind), + /// Wrapper types: Optional, ArrayZeroOrMore, ArrayOneOrMore, Alias. + Wrapper { kind: TypeKind, inner: TypeId }, + /// Composite types: Struct, Enum. + Composite { + kind: TypeKind, + member_start: u16, + member_count: u8, + }, +} + impl TypeDef { - /// For wrapper types, get the inner type. - #[inline] - pub fn inner_type(&self) -> Option { - TypeKind::from_u8(self.kind) - .filter(|k| k.is_wrapper()) - .map(|_| TypeId(self.data)) + /// Create a builtin type (Void, Node, String). + pub fn builtin(kind: TypeKind) -> Self { + Self { + data: 0, + count: 0, + kind: kind as u8, + } } - /// Get the TypeKind for this definition. - #[inline] - pub fn type_kind(&self) -> Option { - TypeKind::from_u8(self.kind) + /// Create a placeholder slot (to be filled later). + pub fn placeholder() -> Self { + Self { + data: 0, + count: 0, + kind: 0, + } } - /// Whether this is an alias type. - #[inline] - pub fn is_alias(&self) -> bool { - TypeKind::from_u8(self.kind).is_some_and(|k| k.is_alias()) + /// Create a wrapper type (Optional, ArrayStar, ArrayPlus). + pub fn wrapper(kind: TypeKind, inner: TypeId) -> Self { + Self { + data: inner.0, + count: 0, + kind: kind as u8, + } + } + + /// Create a composite type (Struct, Enum). + pub fn composite(kind: TypeKind, member_start: u16, member_count: u8) -> Self { + Self { + data: member_start, + count: member_count, + kind: kind as u8, + } } - /// Whether this is an Optional wrapper type. - #[inline] - pub fn is_optional(&self) -> bool { - TypeKind::from_u8(self.kind).is_some_and(|k| k == TypeKind::Optional) + /// Create an optional wrapper type. + pub fn optional(inner: TypeId) -> Self { + Self::wrapper(TypeKind::Optional, inner) } - /// For alias types, get the target type. - #[inline] - pub fn alias_target(&self) -> Option { - TypeKind::from_u8(self.kind) - .filter(|k| k.is_alias()) - .map(|_| TypeId(self.data)) + /// Create an alias type. + pub fn alias(target: TypeId) -> Self { + Self::wrapper(TypeKind::Alias, target) } - /// For Struct/Enum types, get the member index. - #[inline] - pub fn member_index(&self) -> Option { - TypeKind::from_u8(self.kind) - .filter(|k| k.is_composite()) - .map(|_| self.data) + /// Create an ArrayStar (T*) wrapper type. + pub fn array_star(element: TypeId) -> Self { + Self::wrapper(TypeKind::ARRAY_STAR, element) } - /// For Struct/Enum types, get the member count. - #[inline] - pub fn member_count(&self) -> Option { - TypeKind::from_u8(self.kind) - .filter(|k| k.is_composite()) - .map(|_| self.count) + /// Create an ArrayPlus (T+) wrapper type. + pub fn array_plus(element: TypeId) -> Self { + Self::wrapper(TypeKind::ARRAY_PLUS, element) + } + + /// Create a struct type. + pub fn struct_type(member_start: u16, member_count: u8) -> Self { + Self::composite(TypeKind::Struct, member_start, member_count) + } + + /// Create an enum type. + pub fn enum_type(member_start: u16, member_count: u8) -> Self { + Self::composite(TypeKind::Enum, member_start, member_count) + } + + /// Decode from 4 bytes (crate-internal deserialization). + pub(crate) fn from_bytes(bytes: &[u8]) -> Self { + Self { + data: u16::from_le_bytes([bytes[0], bytes[1]]), + count: bytes[2], + kind: bytes[3], + } + } + + /// Encode to 4 bytes. + pub fn to_bytes(&self) -> [u8; 4] { + let mut bytes = [0u8; 4]; + bytes[0..2].copy_from_slice(&self.data.to_le_bytes()); + bytes[2] = self.count; + bytes[3] = self.kind; + bytes + } + + /// Classify this type definition into a structured enum. + /// + /// # Panics + /// Panics if the kind byte is invalid (corrupted bytecode). + pub fn classify(&self) -> TypeData { + let kind = TypeKind::from_u8(self.kind) + .unwrap_or_else(|| panic!("invalid TypeKind byte: {}", self.kind)); + match kind { + TypeKind::Void | TypeKind::Node | TypeKind::String => TypeData::Primitive(kind), + TypeKind::Optional + | TypeKind::ArrayZeroOrMore + | TypeKind::ArrayOneOrMore + | TypeKind::Alias => TypeData::Wrapper { + kind, + inner: TypeId(self.data), + }, + TypeKind::Struct | TypeKind::Enum => TypeData::Composite { + kind, + member_start: self.data, + member_count: self.count, + }, + } } } @@ -134,21 +227,65 @@ impl TypeDef { #[repr(C)] pub struct TypeName { /// StringId of the type name. - pub name: StringId, + pub(crate) name: StringId, /// TypeId this name refers to. - pub type_id: TypeId, + pub(crate) type_id: TypeId, } const _: () = assert!(std::mem::size_of::() == 4); +impl TypeName { + /// Create a new type name entry. + pub fn new(name: StringId, type_id: TypeId) -> Self { + Self { name, type_id } + } + + /// Encode to 4 bytes. + pub fn to_bytes(&self) -> [u8; 4] { + let mut bytes = [0u8; 4]; + bytes[0..2].copy_from_slice(&self.name.get().to_le_bytes()); + bytes[2..4].copy_from_slice(&self.type_id.0.to_le_bytes()); + bytes + } + + pub fn name(&self) -> StringId { + self.name + } + pub fn type_id(&self) -> TypeId { + self.type_id + } +} + /// Field or variant entry (4 bytes). #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(C)] pub struct TypeMember { /// Field/variant name. - pub name: StringId, + pub(crate) name: StringId, /// Type of this field/variant. - pub type_id: TypeId, + pub(crate) type_id: TypeId, } const _: () = assert!(std::mem::size_of::() == 4); + +impl TypeMember { + /// Create a new type member entry. + pub fn new(name: StringId, type_id: TypeId) -> Self { + Self { name, type_id } + } + + /// Encode to 4 bytes. + pub fn to_bytes(&self) -> [u8; 4] { + let mut bytes = [0u8; 4]; + bytes[0..2].copy_from_slice(&self.name.get().to_le_bytes()); + bytes[2..4].copy_from_slice(&self.type_id.0.to_le_bytes()); + bytes + } + + pub fn name(&self) -> StringId { + self.name + } + pub fn type_id(&self) -> TypeId { + self.type_id + } +} diff --git a/crates/plotnik-lib/src/bytecode/type_meta_tests.rs b/crates/plotnik-lib/src/bytecode/type_meta_tests.rs index d289233f..ce7ee680 100644 --- a/crates/plotnik-lib/src/bytecode/type_meta_tests.rs +++ b/crates/plotnik-lib/src/bytecode/type_meta_tests.rs @@ -7,12 +7,7 @@ fn type_meta_header_size() { #[test] fn type_meta_header_roundtrip() { - let header = TypeMetaHeader { - type_defs_count: 42, - type_members_count: 100, - type_names_count: 5, - ..Default::default() - }; + let header = TypeMetaHeader::new(42, 100, 5); let bytes = header.to_bytes(); let decoded = TypeMetaHeader::from_bytes(&bytes); assert_eq!(decoded, header); diff --git a/crates/plotnik-lib/src/compile/compiler.rs b/crates/plotnik-lib/src/compile/compiler.rs index 9a7f50bb..33ce415b 100644 --- a/crates/plotnik-lib/src/compile/compiler.rs +++ b/crates/plotnik-lib/src/compile/compiler.rs @@ -18,41 +18,84 @@ use super::scope::StructScope; pub struct Compiler<'a> { pub(super) interner: &'a Interner, pub(super) type_ctx: &'a TypeContext, - symbol_table: &'a SymbolTable, + pub(crate) symbol_table: &'a SymbolTable, pub(super) strings: &'a mut StringTableBuilder, pub(super) node_type_ids: Option<&'a IndexMap>, pub(super) node_field_ids: Option<&'a IndexMap>, pub(super) instructions: Vec, - next_label_id: u32, + pub(crate) next_label_id: u32, pub(super) def_entries: IndexMap, /// Stack of active struct scopes for capture lookup. /// Innermost scope is at the end. pub(super) scope_stack: Vec, } -impl<'a> Compiler<'a> { - /// Create a new compiler. +/// Builder for `Compiler`. +pub struct CompilerBuilder<'a> { + interner: &'a Interner, + type_ctx: &'a TypeContext, + symbol_table: &'a SymbolTable, + strings: &'a mut StringTableBuilder, + node_type_ids: Option<&'a IndexMap>, + node_field_ids: Option<&'a IndexMap>, +} + +impl<'a> CompilerBuilder<'a> { + /// Create a new builder with required parameters. pub fn new( interner: &'a Interner, type_ctx: &'a TypeContext, symbol_table: &'a SymbolTable, strings: &'a mut StringTableBuilder, - node_type_ids: Option<&'a IndexMap>, - node_field_ids: Option<&'a IndexMap>, ) -> Self { Self { interner, type_ctx, symbol_table, strings, - node_type_ids, - node_field_ids, + node_type_ids: None, + node_field_ids: None, + } + } + + /// Set node type and field IDs for linked compilation. + pub fn linked( + mut self, + node_type_ids: &'a IndexMap, + node_field_ids: &'a IndexMap, + ) -> Self { + self.node_type_ids = Some(node_type_ids); + self.node_field_ids = Some(node_field_ids); + self + } + + /// Build the Compiler. + pub fn build(self) -> Compiler<'a> { + Compiler { + interner: self.interner, + type_ctx: self.type_ctx, + symbol_table: self.symbol_table, + strings: self.strings, + node_type_ids: self.node_type_ids, + node_field_ids: self.node_field_ids, instructions: Vec::new(), next_label_id: 0, def_entries: IndexMap::new(), scope_stack: Vec::new(), } } +} + +impl<'a> Compiler<'a> { + /// Create a builder for Compiler. + pub fn builder( + interner: &'a Interner, + type_ctx: &'a TypeContext, + symbol_table: &'a SymbolTable, + strings: &'a mut StringTableBuilder, + ) -> CompilerBuilder<'a> { + CompilerBuilder::new(interner, type_ctx, symbol_table, strings) + } /// Compile all definitions in the query. pub fn compile( @@ -63,14 +106,14 @@ impl<'a> Compiler<'a> { node_type_ids: Option<&'a IndexMap>, node_field_ids: Option<&'a IndexMap>, ) -> Result { - let mut compiler = Self::new( - interner, - type_ctx, - symbol_table, - strings, - node_type_ids, - node_field_ids, - ); + let mut compiler = + if let (Some(type_ids), Some(field_ids)) = (node_type_ids, node_field_ids) { + Compiler::builder(interner, type_ctx, symbol_table, strings) + .linked(type_ids, field_ids) + .build() + } else { + Compiler::builder(interner, type_ctx, symbol_table, strings).build() + }; // Emit universal preamble first: Obj -> Trampoline -> EndObj -> Return // This wraps any entrypoint to create the top-level scope. diff --git a/crates/plotnik-lib/src/emit/emitter.rs b/crates/plotnik-lib/src/emit/emitter.rs index de9b88f2..7157790a 100644 --- a/crates/plotnik-lib/src/emit/emitter.rs +++ b/crates/plotnik-lib/src/emit/emitter.rs @@ -79,10 +79,7 @@ fn emit_inner( if let Some(ids) = node_type_ids { for (&sym, &node_id) in ids { let name = strings.get_or_intern(sym, interner)?; - node_symbols.push(NodeSymbol { - id: node_id.get(), - name, - }); + node_symbols.push(NodeSymbol::new(node_id.get(), name)); } } @@ -91,10 +88,7 @@ fn emit_inner( if let Some(ids) = node_field_ids { for (&sym, &field_id) in ids { let name = strings.get_or_intern(sym, interner)?; - field_symbols.push(FieldSymbol { - id: field_id.get(), - name, - }); + field_symbols.push(FieldSymbol::new(field_id.get(), name)); } } @@ -109,16 +103,11 @@ fn emit_inner( let target = compile_result .def_entries .get(&def_id) - .and_then(|label| layout.label_to_step.get(label)) + .and_then(|label| layout.label_to_step().get(label)) .copied() .expect("entrypoint must have compiled target"); - entrypoints.push(Entrypoint { - name, - target, - result_type, - _pad: 0, - }); + entrypoints.push(Entrypoint::new(name, target, result_type)); } // Validate counts @@ -156,12 +145,11 @@ fn emit_inner( // Type metadata section (header + 3 aligned sub-sections) let type_meta_offset = emit_section( &mut output, - &TypeMetaHeader { - type_defs_count: types.type_defs_count() as u16, - type_members_count: types.type_members_count() as u16, - type_names_count: types.type_names_count() as u16, - _pad: 0, - } + &TypeMetaHeader::new( + types.type_defs_count() as u16, + types.type_members_count() as u16, + types.type_names_count() as u16, + ) .to_bytes(), ); emit_section(&mut output, &type_defs_bytes); diff --git a/crates/plotnik-lib/src/emit/layout.rs b/crates/plotnik-lib/src/emit/layout.rs index 5aa91f3e..11cc88ba 100644 --- a/crates/plotnik-lib/src/emit/layout.rs +++ b/crates/plotnik-lib/src/emit/layout.rs @@ -60,10 +60,7 @@ impl CacheAligned { /// Returns mapping from labels to step IDs and total step count. pub fn layout(instructions: &[InstructionIR], entries: &[Label]) -> LayoutResult { if instructions.is_empty() { - return LayoutResult { - label_to_step: BTreeMap::new(), - total_steps: 0, - }; + return LayoutResult::empty(); } let graph = Graph::build(instructions); @@ -183,8 +180,5 @@ fn assign_step_ids( } } - LayoutResult { - label_to_step: mapping, - total_steps: current_step, - } + LayoutResult::new(mapping, current_step) } diff --git a/crates/plotnik-lib/src/emit/type_table.rs b/crates/plotnik-lib/src/emit/type_table.rs index 616062d7..6f0756dd 100644 --- a/crates/plotnik-lib/src/emit/type_table.rs +++ b/crates/plotnik-lib/src/emit/type_table.rs @@ -9,7 +9,9 @@ use plotnik_core::{Interner, Symbol}; use crate::analyze::type_check::{ FieldInfo, TYPE_NODE, TYPE_STRING, TYPE_VOID, TypeContext, TypeId, TypeShape, }; -use crate::bytecode::{StringId, TypeDef, TypeId as BytecodeTypeId, TypeMember, TypeName}; +use crate::bytecode::{ + StringId, TypeData, TypeDef, TypeId as BytecodeTypeId, TypeMember, TypeName, +}; use crate::type_system::TypeKind; use super::{EmitError, StringTableBuilder}; @@ -97,11 +99,7 @@ impl TypeTableBuilder { if used_builtins[i] { let bc_id = BytecodeTypeId(self.type_defs.len() as u16); self.mapping.insert(builtin_id, bc_id); - self.type_defs.push(TypeDef { - data: 0, - count: 0, - kind: kind as u8, - }); + self.type_defs.push(TypeDef::builtin(kind)); } } @@ -109,11 +107,7 @@ impl TypeTableBuilder { for &type_id in &ordered_types { let bc_id = BytecodeTypeId(self.type_defs.len() as u16); self.mapping.insert(type_id, bc_id); - self.type_defs.push(TypeDef { - data: 0, - count: 0, - kind: 0, // Placeholder - }); + self.type_defs.push(TypeDef::placeholder()); } // Phase 3: Fill in custom type definitions @@ -136,10 +130,7 @@ impl TypeTableBuilder { .get(&type_id) .copied() .unwrap_or(BytecodeTypeId(0)); - self.type_names.push(TypeName { - name, - type_id: bc_type_id, - }); + self.type_names.push(TypeName::new(name, bc_type_id)); } // Collect TypeName entries for explicit type annotations on struct captures @@ -147,10 +138,7 @@ impl TypeTableBuilder { for (type_id, name_sym) in type_ctx.iter_type_names() { if let Some(&bc_type_id) = self.mapping.get(&type_id) { let name = strings.get_or_intern(name_sym, interner)?; - self.type_names.push(TypeName { - name, - type_id: bc_type_id, - }); + self.type_names.push(TypeName::new(name, bc_type_id)); } } @@ -179,10 +167,7 @@ impl TypeTableBuilder { // Add TypeName entry for the custom type let name = strings.get_or_intern(*sym, interner)?; - self.type_names.push(TypeName { - name, - type_id: bc_type_id, - }); + self.type_names.push(TypeName::new(name, bc_type_id)); // Custom types alias Node - look up Node's actual bytecode ID let node_bc_id = self @@ -190,37 +175,22 @@ impl TypeTableBuilder { .get(&TYPE_NODE) .copied() .unwrap_or(BytecodeTypeId(0)); - self.type_defs[slot_index] = TypeDef { - data: node_bc_id.0, - count: 0, - kind: TypeKind::Alias as u8, - }; + self.type_defs[slot_index] = TypeDef::alias(node_bc_id); Ok(()) } TypeShape::Optional(inner) => { let inner_bc = self.resolve_type(*inner, type_ctx)?; - - self.type_defs[slot_index] = TypeDef { - data: inner_bc.0, - count: 0, - kind: TypeKind::Optional as u8, - }; + self.type_defs[slot_index] = TypeDef::optional(inner_bc); Ok(()) } TypeShape::Array { element, non_empty } => { let element_bc = self.resolve_type(*element, type_ctx)?; - - let kind = if *non_empty { - TypeKind::ArrayOneOrMore + self.type_defs[slot_index] = if *non_empty { + TypeDef::array_plus(element_bc) } else { - TypeKind::ArrayZeroOrMore - }; - self.type_defs[slot_index] = TypeDef { - data: element_bc.0, - count: 0, - kind: kind as u8, + TypeDef::array_star(element_bc) }; Ok(()) } @@ -237,18 +207,12 @@ impl TypeTableBuilder { // Emit members contiguously for this struct let member_start = self.type_members.len() as u16; for (field_name, field_type) in resolved_fields { - self.type_members.push(TypeMember { - name: field_name, - type_id: field_type, - }); + self.type_members + .push(TypeMember::new(field_name, field_type)); } let member_count = fields.len() as u8; - self.type_defs[slot_index] = TypeDef { - data: member_start, - count: member_count, - kind: TypeKind::Struct as u8, - }; + self.type_defs[slot_index] = TypeDef::struct_type(member_start, member_count); Ok(()) } @@ -264,18 +228,12 @@ impl TypeTableBuilder { // Now emit the members and update the placeholder let member_start = self.type_members.len() as u16; for (variant_name, variant_type) in resolved_variants { - self.type_members.push(TypeMember { - name: variant_name, - type_id: variant_type, - }); + self.type_members + .push(TypeMember::new(variant_name, variant_type)); } let member_count = variants.len() as u8; - self.type_defs[slot_index] = TypeDef { - data: member_start, - count: member_count, - kind: TypeKind::Enum as u8, - }; + self.type_defs[slot_index] = TypeDef::enum_type(member_start, member_count); Ok(()) } @@ -339,13 +297,7 @@ impl TypeTableBuilder { // Create new Optional wrapper at the next available index let optional_id = BytecodeTypeId(self.type_defs.len() as u16); - - self.type_defs.push(TypeDef { - data: base_type.0, - count: 0, - kind: TypeKind::Optional as u8, - }); - + self.type_defs.push(TypeDef::optional(base_type)); self.optional_wrappers.insert(base_type, optional_id); Ok(optional_id) } @@ -373,13 +325,9 @@ impl TypeTableBuilder { pub fn get_member_base(&self, type_id: TypeId) -> Option { let bc_type_id = self.mapping.get(&type_id)?; let type_def = self.type_defs.get(bc_type_id.0 as usize)?; - - // Only Struct and Enum have member bases - let kind = TypeKind::from_u8(type_def.kind)?; - if kind.is_composite() { - Some(type_def.data) - } else { - None + match type_def.classify() { + TypeData::Composite { member_start, .. } => Some(member_start), + _ => None, } } @@ -405,21 +353,17 @@ impl TypeTableBuilder { pub fn emit(&self) -> (Vec, Vec, Vec) { let mut defs_bytes = Vec::with_capacity(self.type_defs.len() * 4); for def in &self.type_defs { - defs_bytes.extend_from_slice(&def.data.to_le_bytes()); - defs_bytes.push(def.count); - defs_bytes.push(def.kind); + defs_bytes.extend_from_slice(&def.to_bytes()); } let mut members_bytes = Vec::with_capacity(self.type_members.len() * 4); for member in &self.type_members { - members_bytes.extend_from_slice(&member.name.get().to_le_bytes()); - members_bytes.extend_from_slice(&member.type_id.0.to_le_bytes()); + members_bytes.extend_from_slice(&member.to_bytes()); } let mut names_bytes = Vec::with_capacity(self.type_names.len() * 4); for type_name in &self.type_names { - names_bytes.extend_from_slice(&type_name.name.get().to_le_bytes()); - names_bytes.extend_from_slice(&type_name.type_id.0.to_le_bytes()); + names_bytes.extend_from_slice(&type_name.to_bytes()); } (defs_bytes, members_bytes, names_bytes) diff --git a/crates/plotnik-lib/src/engine/checkpoint.rs b/crates/plotnik-lib/src/engine/checkpoint.rs index 2bf5c770..34765f32 100644 --- a/crates/plotnik-lib/src/engine/checkpoint.rs +++ b/crates/plotnik-lib/src/engine/checkpoint.rs @@ -10,21 +10,67 @@ use super::cursor::SkipPolicy; #[derive(Clone, Copy, Debug)] pub struct Checkpoint { /// Cursor position (tree-sitter descendant_index). - pub descendant_index: u32, + pub(crate) descendant_index: u32, /// Effect stream length at checkpoint. - pub effect_watermark: usize, + pub(crate) effect_watermark: usize, /// Frame arena state at checkpoint. - pub frame_index: Option, + pub(crate) frame_index: Option, /// Recursion depth at checkpoint. - pub recursion_depth: u32, + pub(crate) recursion_depth: u32, /// Resume point (raw step index). - pub ip: u16, + pub(crate) ip: u16, /// If set, advance cursor before retrying (for Call instruction retry). /// When a Call navigates and the callee fails, we need to try the next /// sibling. This policy determines how to advance. - pub skip_policy: Option, + pub(crate) skip_policy: Option, /// Suppression depth at checkpoint. - pub suppress_depth: u16, + pub(crate) suppress_depth: u16, +} + +#[allow(dead_code)] // Getters useful for debugging/tracing +impl Checkpoint { + /// Create a new checkpoint. + pub fn new( + descendant_index: u32, + effect_watermark: usize, + frame_index: Option, + recursion_depth: u32, + ip: u16, + skip_policy: Option, + suppress_depth: u16, + ) -> Self { + Self { + descendant_index, + effect_watermark, + frame_index, + recursion_depth, + ip, + skip_policy, + suppress_depth, + } + } + + pub fn descendant_index(&self) -> u32 { + self.descendant_index + } + pub fn effect_watermark(&self) -> usize { + self.effect_watermark + } + pub fn frame_index(&self) -> Option { + self.frame_index + } + pub fn recursion_depth(&self) -> u32 { + self.recursion_depth + } + pub fn ip(&self) -> u16 { + self.ip + } + pub fn skip_policy(&self) -> Option { + self.skip_policy + } + pub fn suppress_depth(&self) -> u16 { + self.suppress_depth + } } /// Stack of checkpoints with O(1) max_frame_ref tracking. diff --git a/crates/plotnik-lib/src/engine/engine_tests.rs b/crates/plotnik-lib/src/engine/engine_tests.rs index 23e901fc..22dace73 100644 --- a/crates/plotnik-lib/src/engine/engine_tests.rs +++ b/crates/plotnik-lib/src/engine/engine_tests.rs @@ -9,7 +9,7 @@ use crate::QueryBuilder; use crate::bytecode::Module; use crate::emit::emit_linked; -use super::{FuelLimits, Materializer, VM, ValueMaterializer}; +use super::{Materializer, VM, ValueMaterializer}; /// Execute a query against source code and return the JSON output. fn execute(query: &str, source: &str) -> String { @@ -33,7 +33,7 @@ fn execute_with_entry(query: &str, source: &str, entry: Option<&str>) -> String let tree = lang.parse(source); let trivia = build_trivia_types(&module); - let vm = VM::new(&tree, trivia, FuelLimits::default()); + let vm = VM::builder(&tree).trivia_types(trivia).build(); let entrypoint = resolve_entrypoint(&module, entry); let effects = vm diff --git a/crates/plotnik-lib/src/engine/materializer.rs b/crates/plotnik-lib/src/engine/materializer.rs index 63f07bc5..51018181 100644 --- a/crates/plotnik-lib/src/engine/materializer.rs +++ b/crates/plotnik-lib/src/engine/materializer.rs @@ -1,6 +1,6 @@ //! Materializer transforms effect logs into output values. -use crate::bytecode::{StringsView, TypeId, TypeKind, TypesView}; +use crate::bytecode::{StringsView, TypeData, TypeId, TypeKind, TypesView}; use super::effect::RuntimeEffect; use super::value::{NodeHandle, Value}; @@ -30,18 +30,17 @@ impl<'a> ValueMaterializer<'a> { fn resolve_member_name(&self, idx: u16) -> String { let member = self.types.get_member(idx as usize); - self.strings.get(member.name).to_owned() + self.strings.get(member.name()).to_owned() } fn resolve_member_type(&self, idx: u16) -> TypeId { - self.types.get_member(idx as usize).type_id + self.types.get_member(idx as usize).type_id() } fn is_void_type(&self, type_id: TypeId) -> bool { self.types .get(type_id) - .and_then(|def| def.type_kind()) - .is_some_and(|k| k == TypeKind::Void) + .is_some_and(|def| matches!(def.classify(), TypeData::Primitive(TypeKind::Void))) } /// Create initial builder based on result type. @@ -51,10 +50,19 @@ impl<'a> ValueMaterializer<'a> { .get(type_id) .unwrap_or_else(|| panic!("unknown type_id {}", type_id.0)); - match TypeKind::from_u8(def.kind) { - Some(TypeKind::Struct) => Builder::Object(vec![]), - Some(TypeKind::Enum) => Builder::Scalar(None), // Enum gets built when Enum effect comes - Some(TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore) => Builder::Array(vec![]), + match def.classify() { + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => Builder::Object(vec![]), + TypeData::Composite { + kind: TypeKind::Enum, + .. + } => Builder::Scalar(None), + TypeData::Wrapper { + kind: TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore, + .. + } => Builder::Array(vec![]), _ => Builder::Scalar(None), } } diff --git a/crates/plotnik-lib/src/engine/trace.rs b/crates/plotnik-lib/src/engine/trace.rs index 4787272c..824c6c1c 100644 --- a/crates/plotnik-lib/src/engine/trace.rs +++ b/crates/plotnik-lib/src/engine/trace.rs @@ -164,41 +164,72 @@ use std::collections::BTreeMap; /// Tracer that collects execution trace for debugging. pub struct PrintTracer<'s> { /// Source code for extracting node text. - source: &'s [u8], + pub(crate) source: &'s [u8], /// Verbosity level for output filtering. - verbosity: Verbosity, + pub(crate) verbosity: Verbosity, /// Collected trace lines. - lines: Vec, + pub(crate) lines: Vec, /// Line builder for formatting. - builder: LineBuilder, + pub(crate) builder: LineBuilder, /// Maps node type ID to name. - node_type_names: BTreeMap, + pub(crate) node_type_names: BTreeMap, /// Maps node field ID to name. - node_field_names: BTreeMap, + pub(crate) node_field_names: BTreeMap, /// Maps member index to name (for Set/Enum effect display). - member_names: Vec, + pub(crate) member_names: Vec, /// Maps entrypoint target IP to name (for labels and call/return). - entrypoint_by_ip: BTreeMap, + pub(crate) entrypoint_by_ip: BTreeMap, /// Parallel stack of checkpoint creation IPs (for backtrack display). - checkpoint_ips: Vec, + pub(crate) checkpoint_ips: Vec, /// Stack of definition names (for return display). - definition_stack: Vec, + pub(crate) definition_stack: Vec, /// Pending return instruction IP (for consolidated return line). - pending_return_ip: Option, + pub(crate) pending_return_ip: Option, /// Step width for formatting. - step_width: usize, + pub(crate) step_width: usize, /// Color palette. + pub(crate) colors: Colors, +} + +/// Builder for `PrintTracer`. +pub struct PrintTracerBuilder<'s, 'm> { + source: &'s str, + module: &'m Module, + verbosity: Verbosity, colors: Colors, } -impl<'s> PrintTracer<'s> { - pub fn new(source: &'s str, module: &Module, verbosity: Verbosity, colors: Colors) -> Self { - let header = module.header(); - let strings = module.strings(); - let types = module.types(); - let node_types = module.node_types(); - let node_fields = module.node_fields(); - let entrypoints = module.entrypoints(); +impl<'s, 'm> PrintTracerBuilder<'s, 'm> { + /// Create a new builder with required parameters. + pub fn new(source: &'s str, module: &'m Module) -> Self { + Self { + source, + module, + verbosity: Verbosity::Default, + colors: Colors::OFF, + } + } + + /// Set the verbosity level. + pub fn verbosity(mut self, verbosity: Verbosity) -> Self { + self.verbosity = verbosity; + self + } + + /// Set whether to use colored output. + pub fn colored(mut self, enabled: bool) -> Self { + self.colors = Colors::new(enabled); + self + } + + /// Build the PrintTracer. + pub fn build(self) -> PrintTracer<'s> { + let header = self.module.header(); + let strings = self.module.strings(); + let types = self.module.types(); + let node_types = self.module.node_types(); + let node_fields = self.module.node_fields(); + let entrypoints = self.module.entrypoints(); let mut node_type_names = BTreeMap::new(); for i in 0..node_types.len() { @@ -212,12 +243,12 @@ impl<'s> PrintTracer<'s> { node_field_names.insert(f.id, strings.get(f.name).to_string()); } - // Build member names lookup (index → name) + // Build member names lookup (index -> name) let member_names: Vec = (0..types.members_count()) .map(|i| strings.get(types.get_member(i).name).to_string()) .collect(); - // Build entrypoint IP → name lookup + // Build entrypoint IP -> name lookup let mut entrypoint_by_ip = BTreeMap::new(); for i in 0..entrypoints.len() { let e = entrypoints.get(i); @@ -226,9 +257,9 @@ impl<'s> PrintTracer<'s> { let step_width = width_for_count(header.transitions_count as usize); - Self { - source: source.as_bytes(), - verbosity, + PrintTracer { + source: self.source.as_bytes(), + verbosity: self.verbosity, lines: Vec::new(), builder: LineBuilder::new(step_width), node_type_names, @@ -239,9 +270,16 @@ impl<'s> PrintTracer<'s> { definition_stack: Vec::new(), pending_return_ip: None, step_width, - colors, + colors: self.colors, } } +} + +impl<'s> PrintTracer<'s> { + /// Create a builder for PrintTracer. + pub fn builder<'m>(source: &'s str, module: &'m Module) -> PrintTracerBuilder<'s, 'm> { + PrintTracerBuilder::new(source, module) + } fn node_type_name(&self, id: u16) -> &str { self.node_type_names.get(&id).map_or("?", |s| s.as_str()) diff --git a/crates/plotnik-lib/src/engine/verify.rs b/crates/plotnik-lib/src/engine/verify.rs index 53d72045..233dcbfc 100644 --- a/crates/plotnik-lib/src/engine/verify.rs +++ b/crates/plotnik-lib/src/engine/verify.rs @@ -4,8 +4,7 @@ //! Zero-cost in release builds. use crate::Colors; -use crate::bytecode::{Module, StringsView, TypeId, TypesView}; -use crate::type_system::TypeKind; +use crate::bytecode::{Module, StringsView, TypeData, TypeId, TypeKind, TypesView}; use crate::typegen::typescript::{self, Config, VoidType}; use super::Value; @@ -64,63 +63,55 @@ fn verify_type( return; }; - let Some(kind) = type_def.type_kind() else { - errors.push(format_error(path, "invalid type kind")); - return; - }; - - match kind { - TypeKind::Void => { - if !matches!(value, Value::Null) { - errors.push(format_error( - path, - &format!("type: void, value: {}", value_kind_name(value)), - )); + match type_def.classify() { + TypeData::Primitive(kind) => match kind { + TypeKind::Void => { + if !matches!(value, Value::Null) { + errors.push(format_error( + path, + &format!("type: void, value: {}", value_kind_name(value)), + )); + } } - } - - TypeKind::Node => { - if !matches!(value, Value::Node(_)) { - errors.push(format_error( - path, - &format!("type: Node, value: {}", value_kind_name(value)), - )); + TypeKind::Node => { + if !matches!(value, Value::Node(_)) { + errors.push(format_error( + path, + &format!("type: Node, value: {}", value_kind_name(value)), + )); + } } - } - - TypeKind::String => { - if !matches!(value, Value::String(_)) { - errors.push(format_error( - path, - &format!("type: string, value: {}", value_kind_name(value)), - )); + TypeKind::String => { + if !matches!(value, Value::String(_)) { + errors.push(format_error( + path, + &format!("type: string, value: {}", value_kind_name(value)), + )); + } } - } + _ => unreachable!(), + }, - TypeKind::Alias => { - if !matches!(value, Value::Node(_)) { - errors.push(format_error( - path, - &format!("type: Node (alias), value: {}", value_kind_name(value)), - )); + TypeData::Wrapper { kind, inner } => match kind { + TypeKind::Alias => { + if !matches!(value, Value::Node(_)) { + errors.push(format_error( + path, + &format!("type: Node (alias), value: {}", value_kind_name(value)), + )); + } } - } - - TypeKind::Optional => { - let inner_type = TypeId(type_def.data); - if !matches!(value, Value::Null) { - verify_type(value, inner_type, types, strings, path, errors); + TypeKind::Optional => { + if !matches!(value, Value::Null) { + verify_type(value, inner, types, strings, path, errors); + } } - } - - TypeKind::ArrayZeroOrMore => { - let inner_type = TypeId(type_def.data); - match value { + TypeKind::ArrayZeroOrMore => match value { Value::Array(items) => { for (i, item) in items.iter().enumerate() { let prev_len = path.len(); path.push_str(&format!("[{}]", i)); - verify_type(item, inner_type, types, strings, path, errors); + verify_type(item, inner, types, strings, path, errors); path.truncate(prev_len); } } @@ -130,12 +121,8 @@ fn verify_type( &format!("type: array, value: {}", value_kind_name(value)), )); } - } - } - - TypeKind::ArrayOneOrMore => { - let inner_type = TypeId(type_def.data); - match value { + }, + TypeKind::ArrayOneOrMore => match value { Value::Array(items) => { if items.is_empty() { errors.push(format_error( @@ -146,7 +133,7 @@ fn verify_type( for (i, item) in items.iter().enumerate() { let prev_len = path.len(); path.push_str(&format!("[{}]", i)); - verify_type(item, inner_type, types, strings, path, errors); + verify_type(item, inner, types, strings, path, errors); path.truncate(prev_len); } } @@ -156,100 +143,109 @@ fn verify_type( &format!("type: array, value: {}", value_kind_name(value)), )); } - } - } + }, + _ => unreachable!(), + }, - TypeKind::Struct => match value { - Value::Object(fields) => { - for member in types.members_of(&type_def) { - let field_name = strings.get(member.name); - let (inner_type, is_optional) = types.unwrap_optional(member.type_id); + TypeData::Composite { kind, .. } => match kind { + TypeKind::Struct => match value { + Value::Object(fields) => { + for member in types.members_of(&type_def) { + let field_name = strings.get(member.name()); + let (inner_type, is_optional) = types.unwrap_optional(member.type_id()); - let field_value = fields.iter().find(|(k, _)| k == field_name); - match field_value { - Some((_, v)) => { - if is_optional && matches!(v, Value::Null) { - continue; // null is valid for optional field + let field_value = fields.iter().find(|(k, _)| k == field_name); + match field_value { + Some((_, v)) => { + if is_optional && matches!(v, Value::Null) { + continue; + } + let prev_len = path.len(); + path.push('.'); + path.push_str(field_name); + verify_type(v, inner_type, types, strings, path, errors); + path.truncate(prev_len); } - let prev_len = path.len(); - path.push('.'); - path.push_str(field_name); - verify_type(v, inner_type, types, strings, path, errors); - path.truncate(prev_len); - } - None => { - if !is_optional { - errors.push(format!( - "{}: required field missing", - append_path(path, field_name) - )); + None => { + if !is_optional { + errors.push(format!( + "{}: required field missing", + append_path(path, field_name) + )); + } } } } } - } - _ => { - errors.push(format_error( - path, - &format!("type: object, value: {}", value_kind_name(value)), - )); - } - }, - - TypeKind::Enum => match value { - Value::Tagged { tag, data } => { - let variant = types - .members_of(&type_def) - .find(|m| strings.get(m.name) == tag); + _ => { + errors.push(format_error( + path, + &format!("type: object, value: {}", value_kind_name(value)), + )); + } + }, + TypeKind::Enum => match value { + Value::Tagged { tag, data } => { + let variant = types + .members_of(&type_def) + .find(|m| strings.get(m.name()) == tag); - match variant { - Some(member) => { - let is_void = types - .get(member.type_id) - .and_then(|d| d.type_kind()) - .is_some_and(|k| k == TypeKind::Void); + match variant { + Some(member) => { + let is_void = types.get(member.type_id()).is_some_and(|d| { + matches!(d.classify(), TypeData::Primitive(TypeKind::Void)) + }); - if is_void { - if data.is_some() { - errors.push(format!( - "{}: void variant '{}' should have no $data", - append_path(path, "$data"), - tag - )); - } - } else { - match data { - Some(d) => { - let prev_len = path.len(); - path.push_str(".$data"); - verify_type(d, member.type_id, types, strings, path, errors); - path.truncate(prev_len); - } - None => { + if is_void { + if data.is_some() { errors.push(format!( - "{}: non-void variant '{}' should have $data", + "{}: void variant '{}' should have no $data", append_path(path, "$data"), tag )); } + } else { + match data { + Some(d) => { + let prev_len = path.len(); + path.push_str(".$data"); + verify_type( + d, + member.type_id(), + types, + strings, + path, + errors, + ); + path.truncate(prev_len); + } + None => { + errors.push(format!( + "{}: non-void variant '{}' should have $data", + append_path(path, "$data"), + tag + )); + } + } } } - } - None => { - errors.push(format!( - "{}: unknown variant '{}'", - append_path(path, "$tag"), - tag - )); + None => { + errors.push(format!( + "{}: unknown variant '{}'", + append_path(path, "$tag"), + tag + )); + } } } - } - _ => { - errors.push(format_error( - path, - &format!("type: tagged union, value: {}", value_kind_name(value)), - )); - } + _ => { + errors.push(format_error( + path, + &format!("type: tagged union, value: {}", value_kind_name(value)), + )); + } + }, + _ => unreachable!(), }, } } @@ -333,21 +329,20 @@ fn panic_with_mismatch( let type_name = (0..entrypoints.len()) .find_map(|i| { let e = entrypoints.get(i); - if e.result_type == declared_type { - Some(strings.get(e.name)) + if e.result_type() == declared_type { + Some(strings.get(e.name())) } else { None } }) .unwrap_or("unknown"); - let config = Config { - export: true, - emit_node_type: true, - verbose_nodes: false, - void_type: VoidType::Null, - colors: Colors::OFF, - }; + let config = Config::new() + .export(true) + .emit_node_type(true) + .verbose_nodes(false) + .void_type(VoidType::Null) + .colored(false); let type_str = typescript::emit_with_config(module, config); let value_str = value.format(true, colors); let details_str = errors.join("\n"); diff --git a/crates/plotnik-lib/src/engine/vm.rs b/crates/plotnik-lib/src/engine/vm.rs index 2d3ee8b6..ca9712ed 100644 --- a/crates/plotnik-lib/src/engine/vm.rs +++ b/crates/plotnik-lib/src/engine/vm.rs @@ -43,9 +43,9 @@ use super::trace::{NoopTracer, Tracer}; #[derive(Clone, Copy, Debug)] pub struct FuelLimits { /// Maximum total steps (default: 1,000,000). - pub exec_fuel: u32, + pub(crate) exec_fuel: u32, /// Maximum call depth (default: 1,024). - pub recursion_limit: u32, + pub(crate) recursion_limit: u32, } impl Default for FuelLimits { @@ -57,54 +57,149 @@ impl Default for FuelLimits { } } +impl FuelLimits { + /// Create new fuel limits with defaults. + pub fn new() -> Self { + Self::default() + } + + /// Set the execution fuel limit. + pub fn exec_fuel(mut self, fuel: u32) -> Self { + self.exec_fuel = fuel; + self + } + + /// Set the recursion limit. + pub fn recursion_limit(mut self, limit: u32) -> Self { + self.recursion_limit = limit; + self + } + + pub fn get_exec_fuel(&self) -> u32 { + self.exec_fuel + } + pub fn get_recursion_limit(&self) -> u32 { + self.recursion_limit + } +} + /// Virtual machine state for query execution. pub struct VM<'t> { - cursor: CursorWrapper<'t>, + pub(crate) cursor: CursorWrapper<'t>, /// Current instruction pointer (raw u16, 0 is valid at runtime). - ip: u16, - frames: FrameArena, - checkpoints: CheckpointStack, - effects: EffectLog<'t>, - matched_node: Option>, + pub(crate) ip: u16, + pub(crate) frames: FrameArena, + pub(crate) checkpoints: CheckpointStack, + pub(crate) effects: EffectLog<'t>, + pub(crate) matched_node: Option>, // Fuel tracking - exec_fuel: u32, - recursion_depth: u32, - limits: FuelLimits, + pub(crate) exec_fuel: u32, + pub(crate) recursion_depth: u32, + pub(crate) limits: FuelLimits, /// When true, the next Call instruction should skip navigation (use Stay). /// This is set when backtracking to a Call retry checkpoint after advancing /// the cursor to a new sibling. The Call's navigation was already done, and /// we're now at the correct position for the callee to match. - skip_call_nav: bool, + pub(crate) skip_call_nav: bool, /// Suppression depth counter. When > 0, effects are suppressed (not emitted to log). /// Incremented by SuppressBegin, decremented by SuppressEnd. - suppress_depth: u16, + pub(crate) suppress_depth: u16, /// Target address for Trampoline instruction. /// Set from entrypoint before execution; Trampoline jumps to this address. - entrypoint_target: u16, + pub(crate) entrypoint_target: u16, } -impl<'t> VM<'t> { - /// Create a new VM for execution. - pub fn new(tree: &'t Tree, trivia_types: Vec, limits: FuelLimits) -> Self { +/// Builder for VM instances. +pub struct VMBuilder<'t> { + tree: &'t Tree, + trivia_types: Vec, + limits: FuelLimits, +} + +impl<'t> VMBuilder<'t> { + /// Create a new VM builder. + pub fn new(tree: &'t Tree) -> Self { Self { - cursor: CursorWrapper::new(tree.walk(), trivia_types), + tree, + trivia_types: Vec::new(), + limits: FuelLimits::default(), + } + } + + /// Set the trivia types to skip during navigation. + pub fn trivia_types(mut self, types: Vec) -> Self { + self.trivia_types = types; + self + } + + /// Set the fuel limits. + pub fn limits(mut self, limits: FuelLimits) -> Self { + self.limits = limits; + self + } + + /// Set the execution fuel limit. + pub fn exec_fuel(mut self, fuel: u32) -> Self { + self.limits = self.limits.exec_fuel(fuel); + self + } + + /// Set the recursion limit. + pub fn recursion_limit(mut self, limit: u32) -> Self { + self.limits = self.limits.recursion_limit(limit); + self + } + + /// Build the VM. + pub fn build(self) -> VM<'t> { + VM { + cursor: CursorWrapper::new(self.tree.walk(), self.trivia_types), ip: 0, frames: FrameArena::new(), checkpoints: CheckpointStack::new(), effects: EffectLog::new(), matched_node: None, - exec_fuel: limits.exec_fuel, + exec_fuel: self.limits.get_exec_fuel(), recursion_depth: 0, - limits, + limits: self.limits, skip_call_nav: false, suppress_depth: 0, entrypoint_target: 0, } } +} + +impl<'t> VM<'t> { + /// Create a VM builder. + pub fn builder(tree: &'t Tree) -> VMBuilder<'t> { + VMBuilder::new(tree) + } + + /// Create a new VM for execution. + #[deprecated(note = "Use VM::builder(tree).trivia_types(...).build() instead")] + pub fn new(tree: &'t Tree, trivia_types: Vec, limits: FuelLimits) -> Self { + Self::builder(tree) + .trivia_types(trivia_types) + .limits(limits) + .build() + } + + /// Helper for internal checkpoint creation (eliminates duplication). + fn create_checkpoint(&self, ip: u16, skip_policy: Option) -> Checkpoint { + Checkpoint::new( + self.cursor.descendant_index(), + self.effects.len(), + self.frames.current(), + self.recursion_depth, + ip, + skip_policy, + self.suppress_depth, + ) + } /// Execute query from entrypoint, returning effect log. /// @@ -252,15 +347,8 @@ impl<'t> VM<'t> { // Push checkpoints for alternate branches (in reverse order) for i in (1..m.succ_count()).rev() { - self.checkpoints.push(Checkpoint { - descendant_index: self.cursor.descendant_index(), - effect_watermark: self.effects.len(), - frame_index: self.frames.current(), - recursion_depth: self.recursion_depth, - ip: m.successor(i).get(), - skip_policy: None, - suppress_depth: self.suppress_depth, - }); + self.checkpoints + .push(self.create_checkpoint(m.successor(i).get(), None)); tracer.trace_checkpoint_created(self.ip); } @@ -288,15 +376,8 @@ impl<'t> VM<'t> { if let Some(policy) = skip_policy && policy != SkipPolicy::Exact { - self.checkpoints.push(Checkpoint { - descendant_index: self.cursor.descendant_index(), - effect_watermark: self.effects.len(), - frame_index: self.frames.current(), - recursion_depth: self.recursion_depth, - ip: self.ip, - skip_policy: Some(policy), - suppress_depth: self.suppress_depth, - }); + self.checkpoints + .push(self.create_checkpoint(self.ip, Some(policy))); tracer.trace_checkpoint_created(self.ip); } diff --git a/crates/plotnik-lib/src/parser/core.rs b/crates/plotnik-lib/src/parser/core.rs index 36b8d737..248a3916 100644 --- a/crates/plotnik-lib/src/parser/core.rs +++ b/crates/plotnik-lib/src/parser/core.rs @@ -21,6 +21,11 @@ pub(super) struct OpenDelimiter { pub span: TextRange, } +/// Default parsing fuel limit. +const DEFAULT_FUEL: u32 = 1_000_000; +/// Default maximum recursion depth. +const DEFAULT_MAX_DEPTH: u32 = 4096; + /// Trivia tokens are buffered and flushed when starting a new node. pub struct Parser<'q, 'd> { pub(super) source: &'q str, @@ -34,39 +39,99 @@ pub struct Parser<'q, 'd> { pub(super) last_diagnostic_pos: Option, pub(super) delimiter_stack: Vec, pub(super) debug_fuel: std::cell::Cell, - fuel_initial: u32, - fuel_remaining: u32, + pub(crate) fuel_initial: u32, + pub(crate) fuel_remaining: u32, + pub(crate) max_depth: u32, + pub(crate) fatal_error: Option, +} + +/// Builder for `Parser`. +pub struct ParserBuilder<'q, 'd> { + source: &'q str, + source_id: SourceId, + tokens: Vec, + diagnostics: &'d mut Diagnostics, + fuel: u32, max_depth: u32, - fatal_error: Option, } -impl<'q, 'd> Parser<'q, 'd> { +impl<'q, 'd> ParserBuilder<'q, 'd> { + /// Create a new builder with required parameters. pub fn new( source: &'q str, source_id: SourceId, tokens: Vec, diagnostics: &'d mut Diagnostics, - fuel: u32, - max_depth: u32, ) -> Self { Self { source, source_id, tokens, + diagnostics, + fuel: DEFAULT_FUEL, + max_depth: DEFAULT_MAX_DEPTH, + } + } + + /// Set the fuel limit. + pub fn fuel(mut self, fuel: u32) -> Self { + self.fuel = fuel; + self + } + + /// Set the maximum recursion depth. + pub fn max_depth(mut self, depth: u32) -> Self { + self.max_depth = depth; + self + } + + /// Build the Parser. + pub fn build(self) -> Parser<'q, 'd> { + Parser { + source: self.source, + source_id: self.source_id, + tokens: self.tokens, pos: 0, trivia_buffer: Vec::with_capacity(4), builder: GreenNodeBuilder::new(), - diagnostics, + diagnostics: self.diagnostics, depth: 0, last_diagnostic_pos: None, delimiter_stack: Vec::with_capacity(8), debug_fuel: std::cell::Cell::new(256), - fuel_initial: fuel, - fuel_remaining: fuel, - max_depth, + fuel_initial: self.fuel, + fuel_remaining: self.fuel, + max_depth: self.max_depth, fatal_error: None, } } +} + +impl<'q, 'd> Parser<'q, 'd> { + /// Create a builder for Parser. + pub fn builder( + source: &'q str, + source_id: SourceId, + tokens: Vec, + diagnostics: &'d mut Diagnostics, + ) -> ParserBuilder<'q, 'd> { + ParserBuilder::new(source, source_id, tokens, diagnostics) + } + + /// Create a new parser with the specified parameters. + pub fn new( + source: &'q str, + source_id: SourceId, + tokens: Vec, + diagnostics: &'d mut Diagnostics, + fuel: u32, + max_depth: u32, + ) -> Self { + Parser::builder(source, source_id, tokens, diagnostics) + .fuel(fuel) + .max_depth(max_depth) + .build() + } pub fn parse(mut self) -> Result { self.parse_root(); diff --git a/crates/plotnik-lib/src/typegen/typescript/analysis.rs b/crates/plotnik-lib/src/typegen/typescript/analysis.rs index f0d602f7..83aa16fc 100644 --- a/crates/plotnik-lib/src/typegen/typescript/analysis.rs +++ b/crates/plotnik-lib/src/typegen/typescript/analysis.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet}; -use crate::bytecode::{TypeId, TypeKind}; +use crate::bytecode::{TypeData, TypeId, TypeKind}; use super::Emitter; @@ -10,7 +10,7 @@ impl Emitter<'_> { pub(super) fn collect_builtin_references(&mut self) { for i in 0..self.entrypoints.len() { let ep = self.entrypoints.get(i); - self.collect_refs_recursive(ep.result_type); + self.collect_refs_recursive(ep.result_type()); } } @@ -24,34 +24,30 @@ impl Emitter<'_> { return; }; - let Some(kind) = type_def.type_kind() else { - return; - }; - - match kind { - TypeKind::Node => { + match type_def.classify() { + TypeData::Primitive(TypeKind::Node) => { self.node_referenced = true; } - TypeKind::String | TypeKind::Void => { - // No action needed for primitives + TypeData::Primitive(_) => {} + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => { + self.node_referenced = true; } - TypeKind::Struct | TypeKind::Enum => { + TypeData::Wrapper { inner, .. } => { + self.collect_refs_recursive(inner); + } + TypeData::Composite { .. } => { let member_types: Vec<_> = self .types .members_of(&type_def) - .map(|m| m.type_id) + .map(|m| m.type_id()) .collect(); for ty in member_types { self.collect_refs_recursive(ty); } } - TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore | TypeKind::Optional => { - self.collect_refs_recursive(TypeId(type_def.data)); - } - TypeKind::Alias => { - // Alias to Node - self.node_referenced = true; - } } } @@ -111,30 +107,36 @@ impl Emitter<'_> { return; }; - let Some(kind) = type_def.type_kind() else { - return; - }; - - match kind { - TypeKind::Void | TypeKind::Node | TypeKind::String => {} - TypeKind::Struct => { + match type_def.classify() { + TypeData::Primitive(_) => {} + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => { out.insert(type_id); - for member in self.types.members_of(&type_def) { - self.collect_reachable_types(member.type_id, out); - } } - TypeKind::Enum => { + TypeData::Wrapper { inner, .. } => { + self.collect_reachable_types(inner, out); + } + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => { out.insert(type_id); for member in self.types.members_of(&type_def) { - self.collect_enum_variant_refs(member.type_id, out); + self.collect_reachable_types(member.type_id(), out); } } - TypeKind::Alias => { + TypeData::Composite { + kind: TypeKind::Enum, + .. + } => { out.insert(type_id); + for member in self.types.members_of(&type_def) { + self.collect_enum_variant_refs(member.type_id(), out); + } } - TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore | TypeKind::Optional => { - self.collect_reachable_types(TypeId(type_def.data), out); - } + TypeData::Composite { .. } => {} } } @@ -147,9 +149,15 @@ impl Emitter<'_> { // For struct payloads, don't add the struct itself (it will be inlined), // but recurse into its fields to find named types. - if type_def.type_kind() == Some(TypeKind::Struct) { + if matches!( + type_def.classify(), + TypeData::Composite { + kind: TypeKind::Struct, + .. + } + ) { for member in self.types.members_of(&type_def) { - self.collect_reachable_types(member.type_id, out); + self.collect_reachable_types(member.type_id(), out); } } else { // For non-struct payloads, fall back to regular collection. @@ -162,20 +170,18 @@ impl Emitter<'_> { return vec![]; }; - let Some(kind) = type_def.type_kind() else { - return vec![]; - }; - - match kind { - TypeKind::Void | TypeKind::Node | TypeKind::String | TypeKind::Alias => vec![], - TypeKind::Struct | TypeKind::Enum => self + match type_def.classify() { + TypeData::Primitive(_) => vec![], + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => vec![], + TypeData::Wrapper { inner, .. } => self.unwrap_for_deps(inner), + TypeData::Composite { .. } => self .types .members_of(&type_def) - .flat_map(|member| self.unwrap_for_deps(member.type_id)) + .flat_map(|member| self.unwrap_for_deps(member.type_id())) .collect(), - TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore | TypeKind::Optional => { - self.unwrap_for_deps(TypeId(type_def.data)) - } } } @@ -184,16 +190,16 @@ impl Emitter<'_> { return vec![]; }; - let Some(kind) = type_def.type_kind() else { - return vec![]; - }; - - match kind { - TypeKind::Void | TypeKind::Node | TypeKind::String => vec![], - TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore | TypeKind::Optional => { - self.unwrap_for_deps(TypeId(type_def.data)) - } - TypeKind::Struct | TypeKind::Enum | TypeKind::Alias => vec![type_id], + match type_def.classify() { + TypeData::Primitive(_) => vec![], + // Alias is a named type, so it's a dependency itself + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => vec![type_id], + // Other wrappers: recurse into inner type + TypeData::Wrapper { inner, .. } => self.unwrap_for_deps(inner), + TypeData::Composite { .. } => vec![type_id], } } } diff --git a/crates/plotnik-lib/src/typegen/typescript/config.rs b/crates/plotnik-lib/src/typegen/typescript/config.rs index 595f7452..c19241cb 100644 --- a/crates/plotnik-lib/src/typegen/typescript/config.rs +++ b/crates/plotnik-lib/src/typegen/typescript/config.rs @@ -16,15 +16,15 @@ pub enum VoidType { #[derive(Clone, Debug)] pub struct Config { /// Whether to export types - pub export: bool, + pub(crate) export: bool, /// Whether to emit the Node type definition - pub emit_node_type: bool, + pub(crate) emit_node_type: bool, /// Use verbose node representation (with kind, text, etc.) - pub verbose_nodes: bool, + pub(crate) verbose_nodes: bool, /// How to represent the void type - pub void_type: VoidType, + pub(crate) void_type: VoidType, /// Color configuration for output - pub colors: Colors, + pub(crate) colors: Colors, } impl Default for Config { @@ -38,3 +38,40 @@ impl Default for Config { } } } + +impl Config { + /// Create a new Config with default values. + pub fn new() -> Self { + Self::default() + } + + /// Set whether to export types. + pub fn export(mut self, value: bool) -> Self { + self.export = value; + self + } + + /// Set whether to emit the Node type definition. + pub fn emit_node_type(mut self, value: bool) -> Self { + self.emit_node_type = value; + self + } + + /// Set whether to use verbose node representation. + pub fn verbose_nodes(mut self, value: bool) -> Self { + self.verbose_nodes = value; + self + } + + /// Set the void type representation. + pub fn void_type(mut self, value: VoidType) -> Self { + self.void_type = value; + self + } + + /// Set whether to use colored output. + pub fn colored(mut self, enabled: bool) -> Self { + self.colors = Colors::new(enabled); + self + } +} diff --git a/crates/plotnik-lib/src/typegen/typescript/convert.rs b/crates/plotnik-lib/src/typegen/typescript/convert.rs index a8fb65e7..f6240ec1 100644 --- a/crates/plotnik-lib/src/typegen/typescript/convert.rs +++ b/crates/plotnik-lib/src/typegen/typescript/convert.rs @@ -1,6 +1,6 @@ //! Type to TypeScript string conversion. -use crate::bytecode::{TypeDef, TypeId, TypeKind}; +use crate::bytecode::{TypeData, TypeDef, TypeId, TypeKind}; use super::Emitter; use super::config::VoidType; @@ -12,50 +12,60 @@ impl Emitter<'_> { return "unknown".to_string(); }; - let Some(kind) = type_def.type_kind() else { - return "unknown".to_string(); - }; - - match kind { - TypeKind::Void => match self.config.void_type { + match type_def.classify() { + TypeData::Primitive(TypeKind::Void) => match self.config.void_type { VoidType::Undefined => "undefined".to_string(), VoidType::Null => "null".to_string(), }, - TypeKind::Node => "Node".to_string(), - TypeKind::String => "string".to_string(), - TypeKind::Struct | TypeKind::Enum => { - if let Some(name) = self.type_names.get(&type_id) { - format!("{}{}{}", c.blue, name, c.reset) - } else { - self.inline_composite(type_id, &type_def, &kind) - } - } - TypeKind::Alias => { + TypeData::Primitive(TypeKind::Node) => "Node".to_string(), + TypeData::Primitive(TypeKind::String) => "string".to_string(), + TypeData::Primitive(_) => "unknown".to_string(), + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => { if let Some(name) = self.type_names.get(&type_id) { format!("{}{}{}", c.blue, name, c.reset) } else { "Node".to_string() } } - TypeKind::ArrayZeroOrMore => { - let elem_type = self.type_to_ts(TypeId(type_def.data)); + TypeData::Wrapper { + kind: TypeKind::ArrayZeroOrMore, + inner, + } => { + let elem_type = self.type_to_ts(inner); format!("{}{}[]{}", elem_type, c.dim, c.reset) } - TypeKind::ArrayOneOrMore => { - let elem_type = self.type_to_ts(TypeId(type_def.data)); + TypeData::Wrapper { + kind: TypeKind::ArrayOneOrMore, + inner, + } => { + let elem_type = self.type_to_ts(inner); format!( "{}[{}{}{}, ...{}{}{}[]]{}", c.dim, c.reset, elem_type, c.dim, c.reset, elem_type, c.dim, c.reset ) } - TypeKind::Optional => { - let inner_type = self.type_to_ts(TypeId(type_def.data)); + TypeData::Wrapper { + kind: TypeKind::Optional, + inner, + } => { + let inner_type = self.type_to_ts(inner); format!("{} {}|{} null", inner_type, c.dim, c.reset) } + TypeData::Wrapper { .. } => "unknown".to_string(), + TypeData::Composite { kind, .. } => { + if let Some(name) = self.type_names.get(&type_id) { + format!("{}{}{}", c.blue, name, c.reset) + } else { + self.inline_composite(&type_def, kind) + } + } } } - fn inline_composite(&self, _type_id: TypeId, type_def: &TypeDef, kind: &TypeKind) -> String { + fn inline_composite(&self, type_def: &TypeDef, kind: TypeKind) -> String { match kind { TypeKind::Struct => self.inline_struct(type_def), TypeKind::Enum => self.inline_enum(type_def), @@ -65,7 +75,11 @@ impl Emitter<'_> { pub(super) fn inline_struct(&self, type_def: &TypeDef) -> String { let c = self.c(); - if type_def.count == 0 { + let member_count = match type_def.classify() { + TypeData::Composite { member_count, .. } => member_count, + _ => 0, + }; + if member_count == 0 { return format!("{}{{}}{}", c.dim, c.reset); } @@ -73,8 +87,8 @@ impl Emitter<'_> { .types .members_of(type_def) .map(|member| { - let field_name = self.strings.get(member.name).to_string(); - let (inner_type, optional) = self.types.unwrap_optional(member.type_id); + let field_name = self.strings.get(member.name()).to_string(); + let (inner_type, optional) = self.types.unwrap_optional(member.type_id()); (field_name, inner_type, optional) }) .collect(); @@ -109,15 +123,15 @@ impl Emitter<'_> { .types .members_of(type_def) .map(|member| { - let name = self.strings.get(member.name); - if self.is_void_type(member.type_id) { + let name = self.strings.get(member.name()); + if self.is_void_type(member.type_id()) { // Void payload: omit $data format!( "{}{{{} $tag{}:{} {}\"{}\"{}{}}}{}", c.dim, c.reset, c.dim, c.reset, c.green, name, c.reset, c.dim, c.reset ) } else { - let data_type = self.type_to_ts(member.type_id); + let data_type = self.type_to_ts(member.type_id()); format!( "{}{{{} $tag{}:{} {}\"{}\"{}{}; $data{}:{} {} {}}}{}", c.dim, @@ -147,25 +161,19 @@ impl Emitter<'_> { return self.type_to_ts(type_id); }; - let Some(kind) = type_def.type_kind() else { - return self.type_to_ts(type_id); - }; - - if kind == TypeKind::Void { - return format!("{}{{}}{}", c.dim, c.reset); - } - - if kind == TypeKind::Struct { - self.inline_struct(&type_def) - } else { - self.type_to_ts(type_id) + match type_def.classify() { + TypeData::Primitive(TypeKind::Void) => format!("{}{{}}{}", c.dim, c.reset), + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => self.inline_struct(&type_def), + _ => self.type_to_ts(type_id), } } pub(super) fn is_void_type(&self, type_id: TypeId) -> bool { self.types .get(type_id) - .and_then(|def| def.type_kind()) - .is_some_and(|k| k == TypeKind::Void) + .is_some_and(|def| matches!(def.classify(), TypeData::Primitive(TypeKind::Void))) } } diff --git a/crates/plotnik-lib/src/typegen/typescript/naming.rs b/crates/plotnik-lib/src/typegen/typescript/naming.rs index cced6a9d..f2908b50 100644 --- a/crates/plotnik-lib/src/typegen/typescript/naming.rs +++ b/crates/plotnik-lib/src/typegen/typescript/naming.rs @@ -4,7 +4,7 @@ use std::collections::HashMap; use plotnik_core::utils::to_pascal_case; -use crate::bytecode::{TypeId, TypeKind}; +use crate::bytecode::{TypeData, TypeId, TypeKind}; use super::Emitter; @@ -21,9 +21,9 @@ impl Emitter<'_> { for i in 0..self.entrypoints.len() { let ep = self.entrypoints.get(i); - let def_name = self.strings.get(ep.name); + let def_name = self.strings.get(ep.name()); self.collect_naming_contexts( - ep.result_type, + ep.result_type(), &NamingContext { def_name: def_name.to_string(), field_name: None, @@ -67,17 +67,23 @@ impl Emitter<'_> { return; }; - let Some(kind) = type_def.type_kind() else { - return; - }; - - match kind { - TypeKind::Void | TypeKind::Node | TypeKind::String | TypeKind::Alias => {} - TypeKind::Struct => { + match type_def.classify() { + TypeData::Primitive(_) => {} + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => {} + TypeData::Wrapper { inner, .. } => { + self.collect_naming_contexts(inner, ctx, contexts); + } + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => { contexts.entry(type_id).or_insert_with(|| ctx.clone()); for member in self.types.members_of(&type_def) { - let field_name = self.strings.get(member.name); - let (inner_type, _) = self.types.unwrap_optional(member.type_id); + let field_name = self.strings.get(member.name()); + let (inner_type, _) = self.types.unwrap_optional(member.type_id()); let field_ctx = NamingContext { def_name: ctx.def_name.clone(), field_name: Some(field_name.to_string()), @@ -85,19 +91,23 @@ impl Emitter<'_> { self.collect_naming_contexts(inner_type, &field_ctx, contexts); } } - TypeKind::Enum => { + TypeData::Composite { + kind: TypeKind::Enum, + .. + } => { contexts.entry(type_id).or_insert_with(|| ctx.clone()); } - TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore | TypeKind::Optional => { - self.collect_naming_contexts(TypeId(type_def.data), ctx, contexts); - } + TypeData::Composite { .. } => {} } } pub(super) fn needs_generated_name(&self, type_def: &crate::bytecode::TypeDef) -> bool { matches!( - type_def.type_kind(), - Some(TypeKind::Struct) | Some(TypeKind::Enum) + type_def.classify(), + TypeData::Composite { + kind: TypeKind::Struct | TypeKind::Enum, + .. + } ) } @@ -111,9 +121,15 @@ impl Emitter<'_> { } pub(super) fn generate_fallback_name(&mut self, type_def: &crate::bytecode::TypeDef) -> String { - let base = match type_def.type_kind() { - Some(TypeKind::Struct) => "Struct", - Some(TypeKind::Enum) => "Enum", + let base = match type_def.classify() { + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => "Struct", + TypeData::Composite { + kind: TypeKind::Enum, + .. + } => "Enum", _ => "Type", }; self.unique_name(base) diff --git a/crates/plotnik-lib/src/typegen/typescript/render.rs b/crates/plotnik-lib/src/typegen/typescript/render.rs index 7925443c..570fa07b 100644 --- a/crates/plotnik-lib/src/typegen/typescript/render.rs +++ b/crates/plotnik-lib/src/typegen/typescript/render.rs @@ -2,7 +2,7 @@ use plotnik_core::utils::to_pascal_case; -use crate::bytecode::{TypeDef, TypeId, TypeKind}; +use crate::bytecode::{TypeData, TypeDef, TypeId, TypeKind}; use super::Emitter; @@ -11,16 +11,16 @@ impl Emitter<'_> { // Reserve entrypoint names to avoid collisions for i in 0..self.entrypoints.len() { let ep = self.entrypoints.get(i); - let name = self.strings.get(ep.name); + let name = self.strings.get(ep.name()); self.used_names.insert(to_pascal_case(name)); } // Assign names to named types from TypeNames section for i in 0..self.types.names_count() { let type_name = self.types.get_name(i); - let name = self.strings.get(type_name.name); + let name = self.strings.get(type_name.name()); self.type_names - .insert(type_name.type_id, to_pascal_case(name)); + .insert(type_name.type_id(), to_pascal_case(name)); } // Assign names to struct/enum types that need them but don't have names @@ -44,26 +44,23 @@ impl Emitter<'_> { return; }; - let Some(kind) = type_def.type_kind() else { - return; - }; - - if kind.is_primitive() { - return; - } - - // Check if this is an alias type (custom type annotation) - if type_def.is_alias() { - if let Some(name) = self.type_names.get(&type_id).cloned() { - self.emit_custom_type_alias(&name); - self.emitted.insert(type_id); + match type_def.classify() { + TypeData::Primitive(_) => (), + TypeData::Wrapper { + kind: TypeKind::Alias, + .. + } => { + if let Some(name) = self.type_names.get(&type_id).cloned() { + self.emit_custom_type_alias(&name); + self.emitted.insert(type_id); + } + } + TypeData::Wrapper { .. } => (), + TypeData::Composite { .. } => { + if let Some(name) = self.type_names.get(&type_id).cloned() { + self.emit_generated_type_def(type_id, &name); + } } - return; - } - - // Check if we have a generated name - if let Some(name) = self.type_names.get(&type_id).cloned() { - self.emit_generated_type_def(type_id, &name); } } @@ -74,13 +71,15 @@ impl Emitter<'_> { return; }; - let Some(kind) = type_def.type_kind() else { - return; - }; - - match kind { - TypeKind::Struct => self.emit_interface(name, &type_def), - TypeKind::Enum => self.emit_tagged_union(name, &type_def), + match type_def.classify() { + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => self.emit_interface(name, &type_def), + TypeData::Composite { + kind: TypeKind::Enum, + .. + } => self.emit_tagged_union(name, &type_def), _ => {} } } @@ -96,13 +95,15 @@ impl Emitter<'_> { return; }; - let Some(kind) = type_def.type_kind() else { - return; - }; - - match kind { - TypeKind::Struct => self.emit_interface(&type_name, &type_def), - TypeKind::Enum => self.emit_tagged_union(&type_name, &type_def), + match type_def.classify() { + TypeData::Composite { + kind: TypeKind::Struct, + .. + } => self.emit_interface(&type_name, &type_def), + TypeData::Composite { + kind: TypeKind::Enum, + .. + } => self.emit_tagged_union(&type_name, &type_def), _ => { let ts_type = self.type_to_ts(type_id); self.emit_type_decl(&type_name, &ts_type); @@ -142,8 +143,8 @@ impl Emitter<'_> { .types .members_of(type_def) .map(|member| { - let field_name = self.strings.get(member.name).to_string(); - let (inner_type, optional) = self.types.unwrap_optional(member.type_id); + let field_name = self.strings.get(member.name()).to_string(); + let (inner_type, optional) = self.types.unwrap_optional(member.type_id()); (field_name, inner_type, optional) }) .collect(); @@ -166,11 +167,11 @@ impl Emitter<'_> { let mut variant_types = Vec::new(); for member in self.types.members_of(type_def) { - let variant_name = self.strings.get(member.name); + let variant_name = self.strings.get(member.name()); let variant_type_name = format!("{}{}", name, to_pascal_case(variant_name)); variant_types.push(variant_type_name.clone()); - let is_void = self.is_void_type(member.type_id); + let is_void = self.is_void_type(member.type_id()); // Header: export interface NameVariant { if self.config.export { @@ -188,7 +189,7 @@ impl Emitter<'_> { )); // $data field (omit for Void payloads) if !is_void { - let data_str = self.inline_data_type(member.type_id); + let data_str = self.inline_data_type(member.type_id()); self.output.push_str(&format!( " $data{}:{} {}{};\n", c.dim, c.reset, data_str, c.dim