From 829359e24ae1922e5fc19a032ed4963b67b2088a Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Tue, 30 Dec 2025 13:16:48 -0300 Subject: [PATCH] refactor: switch TypeScript emission to bytecode path --- .gitignore | 2 +- crates/plotnik-cli/src/cli.rs | 4 - crates/plotnik-cli/src/commands/debug/mod.rs | 6 +- crates/plotnik-cli/src/commands/types.rs | 15 +- crates/plotnik-cli/src/main.rs | 1 - crates/plotnik-lib/src/query/dependencies.rs | 2 +- crates/plotnik-lib/src/query/dump.rs | 5 +- crates/plotnik-lib/src/query/query_tests.rs | 7 +- crates/plotnik-lib/src/query/symbol_table.rs | 2 +- .../src/query/type_check/context.rs | 2 +- .../src/query/type_check/emit_ts.rs | 699 ------------------ .../plotnik-lib/src/query/type_check/infer.rs | 1 - .../plotnik-lib/src/query/type_check/mod.rs | 2 - .../src/query/type_check/symbol.rs | 3 +- .../plotnik-lib/src/query/type_check/types.rs | 62 +- 15 files changed, 45 insertions(+), 768 deletions(-) delete mode 100644 crates/plotnik-lib/src/query/type_check/emit_ts.rs diff --git a/.gitignore b/.gitignore index 562977e5..98dbe7cb 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,4 @@ lcov.info *.profraw *.profdata /target/llvm-cov-target/ -/target/coverage/ +/target/coverage/ \ No newline at end of file diff --git a/crates/plotnik-cli/src/cli.rs b/crates/plotnik-cli/src/cli.rs index ebf2718a..553dda6c 100644 --- a/crates/plotnik-cli/src/cli.rs +++ b/crates/plotnik-cli/src/cli.rs @@ -124,10 +124,6 @@ pub struct TypesOutputArgs { #[arg(long, default_value = "typescript", value_name = "FORMAT")] pub format: String, - /// Name for the root type (for anonymous expressions) - #[arg(long, default_value = "Query", value_name = "NAME")] - pub root_type: String, - /// Use verbose node shape (matches exec --verbose-nodes) #[arg(long)] pub verbose_nodes: bool, diff --git a/crates/plotnik-cli/src/commands/debug/mod.rs b/crates/plotnik-cli/src/commands/debug/mod.rs index be2863c7..02682339 100644 --- a/crates/plotnik-cli/src/commands/debug/mod.rs +++ b/crates/plotnik-cli/src/commands/debug/mod.rs @@ -82,8 +82,10 @@ pub fn run(args: DebugArgs) { if args.types && let Some(ref q) = query { - let output = - plotnik_lib::query::type_check::emit_typescript(q.type_context(), q.interner()); + let bytecode = q.emit().expect("bytecode emission failed"); + let module = + plotnik_lib::bytecode::Module::from_bytes(bytecode).expect("module loading failed"); + let output = plotnik_lib::bytecode::emit::emit_typescript(&module); print!("{}", output); } diff --git a/crates/plotnik-cli/src/commands/types.rs b/crates/plotnik-cli/src/commands/types.rs index a913812c..6c2ef68c 100644 --- a/crates/plotnik-cli/src/commands/types.rs +++ b/crates/plotnik-cli/src/commands/types.rs @@ -4,14 +4,13 @@ use std::path::PathBuf; use plotnik_langs::Lang; use plotnik_lib::Query; -use plotnik_lib::query::type_check::{EmitConfig, emit_typescript_with_config}; +use plotnik_lib::bytecode::emit::{TsEmitConfig, emit_typescript_with_config}; pub struct TypesArgs { pub query_text: Option, pub query_file: Option, pub lang: Option, pub format: String, - pub root_type: String, pub verbose_nodes: bool, pub no_node_type: bool, pub export: bool, @@ -44,15 +43,19 @@ pub fn run(args: TypesArgs) { std::process::exit(1); } - // Emit TypeScript types - let config = EmitConfig { + // Emit to bytecode first + let bytecode = query.emit().expect("bytecode emission failed"); + let module = + plotnik_lib::bytecode::Module::from_bytes(bytecode).expect("module loading failed"); + + // Emit TypeScript types from bytecode + let config = TsEmitConfig { export: args.export, emit_node_type: !args.no_node_type, - root_type_name: args.root_type, verbose_nodes: args.verbose_nodes, }; - let output = emit_typescript_with_config(query.type_context(), query.interner(), config); + let output = emit_typescript_with_config(&module, config); // Write output if let Some(ref path) = args.output { diff --git a/crates/plotnik-cli/src/main.rs b/crates/plotnik-cli/src/main.rs index a0184782..9ad8eb72 100644 --- a/crates/plotnik-cli/src/main.rs +++ b/crates/plotnik-cli/src/main.rs @@ -64,7 +64,6 @@ fn main() { query_file: query.query_file, lang, format: output.format, - root_type: output.root_type, verbose_nodes: output.verbose_nodes, no_node_type: output.no_node_type, export: !output.no_export, diff --git a/crates/plotnik-lib/src/query/dependencies.rs b/crates/plotnik-lib/src/query/dependencies.rs index 343dd14c..bce3c9ce 100644 --- a/crates/plotnik-lib/src/query/dependencies.rs +++ b/crates/plotnik-lib/src/query/dependencies.rs @@ -24,7 +24,7 @@ use crate::query::type_check::DefId; use crate::query::visitor::{Visitor, walk_expr}; /// Result of dependency analysis. -#[derive(Debug, Clone, Default)] +#[derive(Clone, Debug, Default)] pub struct DependencyAnalysis { /// Strongly connected components in reverse topological order. /// diff --git a/crates/plotnik-lib/src/query/dump.rs b/crates/plotnik-lib/src/query/dump.rs index 6b5c5d17..96aefadf 100644 --- a/crates/plotnik-lib/src/query/dump.rs +++ b/crates/plotnik-lib/src/query/dump.rs @@ -38,7 +38,10 @@ mod test_helpers { } pub fn emit_typescript(&self) -> String { - crate::query::type_check::emit_typescript(self.type_context(), self.interner()) + let bytecode = self.emit().expect("bytecode emission should succeed"); + let module = crate::bytecode::Module::from_bytes(bytecode) + .expect("module loading should succeed"); + crate::bytecode::emit::emit_typescript(&module) } } } diff --git a/crates/plotnik-lib/src/query/query_tests.rs b/crates/plotnik-lib/src/query/query_tests.rs index ecbeebbd..75d14140 100644 --- a/crates/plotnik-lib/src/query/query_tests.rs +++ b/crates/plotnik-lib/src/query/query_tests.rs @@ -2,6 +2,7 @@ use plotnik_langs::Lang; use crate::{ SourceMap, + bytecode::Module, query::query::{LinkedQuery, QueryAnalyzed, QueryBuilder}, }; @@ -85,7 +86,11 @@ impl QueryAnalyzed { query.dump_diagnostics() ); } - query.emit_typescript() + + // Emit to bytecode and then emit TypeScript from the bytecode module + let bytecode = query.emit().expect("bytecode emission should succeed"); + let module = Module::from_bytes(bytecode).expect("module loading should succeed"); + crate::bytecode::emit::emit_typescript(&module) } #[track_caller] diff --git a/crates/plotnik-lib/src/query/symbol_table.rs b/crates/plotnik-lib/src/query/symbol_table.rs index 617b3c0a..7bd530c5 100644 --- a/crates/plotnik-lib/src/query/symbol_table.rs +++ b/crates/plotnik-lib/src/query/symbol_table.rs @@ -21,7 +21,7 @@ pub const UNNAMED_DEF: &str = "_"; /// /// Stores the mapping from definition names to their AST expressions, /// along with source file information for diagnostics. -#[derive(Debug, Clone, Default)] +#[derive(Clone, Debug, Default)] pub struct SymbolTable { /// Maps symbol name to its AST expression. table: IndexMap, diff --git a/crates/plotnik-lib/src/query/type_check/context.rs b/crates/plotnik-lib/src/query/type_check/context.rs index 5f45ff94..bc795c6e 100644 --- a/crates/plotnik-lib/src/query/type_check/context.rs +++ b/crates/plotnik-lib/src/query/type_check/context.rs @@ -14,7 +14,7 @@ use super::types::{ }; /// Central registry for types, symbols, and expression metadata. -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct TypeContext { types: Vec, type_map: HashMap, diff --git a/crates/plotnik-lib/src/query/type_check/emit_ts.rs b/crates/plotnik-lib/src/query/type_check/emit_ts.rs deleted file mode 100644 index c37e8420..00000000 --- a/crates/plotnik-lib/src/query/type_check/emit_ts.rs +++ /dev/null @@ -1,699 +0,0 @@ -//! TypeScript type emitter for testing type inference. -//! -//! Converts inferred types to TypeScript declarations. -//! Used as a test oracle to verify type inference correctness. - -use std::collections::hash_map::Entry; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; - -use plotnik_core::Interner; - -use super::context::TypeContext; -use super::symbol::Symbol; -use super::types::{FieldInfo, TYPE_NODE, TYPE_STRING, TYPE_VOID, TypeId, TypeKind}; - -/// Naming context for synthetic type names: (DefName, FieldName) -#[derive(Clone, Debug)] -struct NamingContext { - def_name: String, - field_name: Option, -} - -/// Configuration for TypeScript emission. -#[derive(Clone, Debug)] -pub struct EmitConfig { - /// Whether to export types - pub export: bool, - /// Whether to emit the Node type definition - pub emit_node_type: bool, - /// Name for the root type if unnamed - pub root_type_name: String, - /// Use verbose node representation (with kind, text, etc.) - pub verbose_nodes: bool, -} - -impl Default for EmitConfig { - fn default() -> Self { - Self { - export: true, - emit_node_type: true, - root_type_name: "Query".to_string(), - verbose_nodes: false, - } - } -} - -/// TypeScript emitter. -pub struct TsEmitter<'a> { - ctx: &'a TypeContext, - interner: &'a Interner, - config: EmitConfig, - - /// Generated type names, to avoid collisions - used_names: BTreeSet, - /// TypeId -> generated name mapping - type_names: HashMap, - - /// Track which builtin types are referenced - referenced_builtins: HashSet, - /// Track which types have been emitted - emitted: HashSet, - /// Output buffer - output: String, -} - -impl<'a> TsEmitter<'a> { - pub fn new(ctx: &'a TypeContext, interner: &'a Interner, config: EmitConfig) -> Self { - Self { - ctx, - interner, - config, - used_names: BTreeSet::new(), - type_names: HashMap::new(), - referenced_builtins: HashSet::new(), - emitted: HashSet::new(), - output: String::new(), - } - } - - /// Emit TypeScript for all definition types. - pub fn emit(mut self) -> String { - self.prepare_emission(); - - // Collect all definitions, tracking primary name per TypeId and aliases - let mut primary_names: HashMap = HashMap::new(); - let mut aliases: Vec<(String, TypeId)> = Vec::new(); - - for (def_id, type_id) in self.ctx.iter_def_types() { - let name = self.ctx.def_name(self.interner, def_id).to_string(); - if let Entry::Vacant(e) = primary_names.entry(type_id) { - e.insert(name); - } else { - // This TypeId already has a primary definition; this becomes an alias - aliases.push((name, type_id)); - } - } - - // Collect all reachable types starting from definitions - let mut to_emit = HashSet::new(); - for (_, type_id) in self.ctx.iter_def_types() { - self.collect_reachable_types(type_id, &mut to_emit); - } - - // Emit in topological order - for type_id in self.sort_topologically(to_emit) { - if let Some(def_name) = primary_names.get(&type_id) { - self.emit_type_definition(def_name, type_id); - } else { - self.emit_generated_or_custom(type_id); - } - } - - // Emit type aliases for definitions that share a TypeId with another definition - for (alias_name, type_id) in aliases { - if let Some(primary_name) = primary_names.get(&type_id) { - self.emit_type_alias(&alias_name, primary_name); - } - } - - self.output - } - - /// Emit TypeScript for a single definition. - pub fn emit_single(mut self, name: &str, type_id: TypeId) -> String { - self.prepare_emission(); - - let mut to_emit = HashSet::new(); - self.collect_reachable_types(type_id, &mut to_emit); - - let sorted = self.sort_topologically(to_emit); - - // Emit dependencies (everything except the root) - for &dep_id in &sorted { - if dep_id != type_id { - self.emit_generated_or_custom(dep_id); - } - } - - // Emit the main definition - self.emit_type_definition(name, type_id); - self.output - } - - fn prepare_emission(&mut self) { - self.assign_generated_names(); - self.collect_builtin_references(); - - if self.config.emit_node_type && self.referenced_builtins.contains(&TYPE_NODE) { - self.emit_node_interface(); - } - } - - fn assign_generated_names(&mut self) { - // 1. Reserve definition names to avoid collisions - for (def_id, _) in self.ctx.iter_def_types() { - let name = self.ctx.def_name(self.interner, def_id); - self.used_names.insert(to_pascal_case(name)); - } - - // 2. Collect naming contexts (path from definition to type) - let mut contexts = HashMap::new(); - for (def_id, type_id) in self.ctx.iter_def_types() { - let def_name = self.ctx.def_name(self.interner, def_id); - self.collect_naming_contexts( - type_id, - &NamingContext { - def_name: def_name.to_string(), - field_name: None, - }, - &mut contexts, - ); - } - - // 3. Assign names to types that need them - for (id, kind) in self.ctx.iter_types() { - if !self.needs_generated_name(kind) || self.type_names.contains_key(&id) { - continue; - } - - let name = if let Some(ctx) = contexts.get(&id) { - self.generate_contextual_name(ctx) - } else { - self.generate_fallback_name(kind) - }; - self.type_names.insert(id, name); - } - } - - fn collect_naming_contexts( - &self, - type_id: TypeId, - ctx: &NamingContext, - contexts: &mut HashMap, - ) { - if type_id.is_builtin() || contexts.contains_key(&type_id) { - return; - } - - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for naming context"); - - match kind { - TypeKind::Struct(fields) => { - contexts.entry(type_id).or_insert_with(|| ctx.clone()); - for (&field_sym, info) in fields { - let field_name = self.interner.resolve(field_sym); - let field_ctx = NamingContext { - def_name: ctx.def_name.clone(), - field_name: Some(field_name.to_string()), - }; - self.collect_naming_contexts(info.type_id, &field_ctx, contexts); - } - } - TypeKind::Enum(_) => { - contexts.entry(type_id).or_insert_with(|| ctx.clone()); - } - TypeKind::Array { element, .. } => { - self.collect_naming_contexts(*element, ctx, contexts); - } - TypeKind::Optional(inner) => { - self.collect_naming_contexts(*inner, ctx, contexts); - } - _ => {} - } - } - - fn collect_builtin_references(&mut self) { - for (_, type_id) in self.ctx.iter_def_types() { - self.collect_refs_recursive(type_id); - } - } - - fn collect_refs_recursive(&mut self, type_id: TypeId) { - if type_id == TYPE_NODE || type_id == TYPE_STRING { - self.referenced_builtins.insert(type_id); - return; - } - if type_id == TYPE_VOID { - return; - } - - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for builtin collection"); - - match kind { - TypeKind::Node | TypeKind::Custom(_) => { - self.referenced_builtins.insert(TYPE_NODE); - } - TypeKind::String => { - self.referenced_builtins.insert(TYPE_STRING); - } - TypeKind::Struct(fields) => { - fields - .values() - .for_each(|info| self.collect_refs_recursive(info.type_id)); - } - TypeKind::Enum(variants) => { - variants - .values() - .for_each(|&tid| self.collect_refs_recursive(tid)); - } - TypeKind::Array { element, .. } => self.collect_refs_recursive(*element), - TypeKind::Optional(inner) => self.collect_refs_recursive(*inner), - _ => {} - } - } - - fn sort_topologically(&self, types: HashSet) -> Vec { - let mut deps: HashMap> = HashMap::new(); - let mut rdeps: HashMap> = HashMap::new(); - - for &tid in &types { - deps.entry(tid).or_default(); - rdeps.entry(tid).or_default(); - } - - // Build dependency graph - for &tid in &types { - for dep in self.get_direct_deps(tid) { - if types.contains(&dep) && dep != tid { - deps.entry(tid).or_default().insert(dep); - rdeps.entry(dep).or_default().insert(tid); - } - } - } - - // Kahn's algorithm - let mut result = Vec::with_capacity(types.len()); - let mut queue: Vec = deps - .iter() - .filter(|(_, d)| d.is_empty()) - .map(|(&tid, _)| tid) - .collect(); - - // Sort for deterministic output - queue.sort_by_key(|tid| tid.0); - - while let Some(tid) = queue.pop() { - result.push(tid); - if let Some(dependents) = rdeps.get(&tid) { - for &dependent in dependents { - if let Some(dep_set) = deps.get_mut(&dependent) { - dep_set.remove(&tid); - if dep_set.is_empty() { - queue.push(dependent); - queue.sort_by_key(|t| t.0); - } - } - } - } - } - - result - } - - fn collect_reachable_types(&self, type_id: TypeId, out: &mut HashSet) { - if type_id.is_builtin() || out.contains(&type_id) { - return; - } - - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for reachability"); - - match kind { - TypeKind::Struct(fields) => { - out.insert(type_id); - for info in fields.values() { - self.collect_reachable_types(info.type_id, out); - } - } - TypeKind::Enum(_) | TypeKind::Custom(_) => { - out.insert(type_id); - } - TypeKind::Array { element, .. } => self.collect_reachable_types(*element, out), - TypeKind::Optional(inner) => self.collect_reachable_types(*inner, out), - _ => {} - } - } - - fn get_direct_deps(&self, type_id: TypeId) -> Vec { - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for dependency calculation"); - match kind { - TypeKind::Struct(fields) => fields - .values() - .flat_map(|info| self.unwrap_for_deps(info.type_id)) - .collect(), - TypeKind::Enum(variants) => variants - .values() - .flat_map(|&tid| self.unwrap_for_deps(tid)) - .collect(), - TypeKind::Array { element, .. } => self.unwrap_for_deps(*element), - TypeKind::Optional(inner) => self.unwrap_for_deps(*inner), - _ => vec![], - } - } - - fn unwrap_for_deps(&self, type_id: TypeId) -> Vec { - if type_id.is_builtin() { - return vec![]; - } - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for unwrapping"); - match kind { - TypeKind::Array { element, .. } => self.unwrap_for_deps(*element), - TypeKind::Optional(inner) => self.unwrap_for_deps(*inner), - TypeKind::Struct(_) | TypeKind::Enum(_) | TypeKind::Custom(_) => vec![type_id], - _ => vec![], - } - } - - fn emit_generated_or_custom(&mut self, type_id: TypeId) { - if self.emitted.contains(&type_id) || type_id.is_builtin() { - return; - } - - if let Some(name) = self.type_names.get(&type_id).cloned() { - self.emit_generated_type_def(type_id, &name); - } else if let Some(TypeKind::Custom(sym)) = self.ctx.get_type(type_id) { - self.emit_custom_type_alias(self.interner.resolve(*sym)); - self.emitted.insert(type_id); - } - } - - fn emit_generated_type_def(&mut self, type_id: TypeId, name: &str) { - self.emitted.insert(type_id); - let export = if self.config.export { "export " } else { "" }; - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for generation"); - - match kind { - TypeKind::Struct(fields) => self.emit_interface(name, fields, export), - TypeKind::Enum(variants) => self.emit_tagged_union(name, variants, export), - _ => {} - } - } - - fn emit_type_definition(&mut self, name: &str, type_id: TypeId) { - self.emitted.insert(type_id); - let export = if self.config.export { "export " } else { "" }; - let type_name = to_pascal_case(name); - - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for definition emission"); - - match kind { - TypeKind::Struct(fields) => { - self.emit_interface(&type_name, fields, export); - } - TypeKind::Enum(variants) => { - self.emit_tagged_union(&type_name, variants, export); - } - _ => { - let ts_type = self.type_to_ts(type_id); - self.output - .push_str(&format!("{}type {} = {};\n\n", export, type_name, ts_type)); - } - } - } - - fn emit_interface(&mut self, name: &str, fields: &BTreeMap, export: &str) { - self.output - .push_str(&format!("{}interface {} {{\n", export, name)); - - for (&sym, info) in self.sort_map_by_name(fields) { - let field_name = self.interner.resolve(sym); - let ts_type = self.type_to_ts(info.type_id); - let optional = if info.optional { "?" } else { "" }; - self.output - .push_str(&format!(" {}{}: {};\n", field_name, optional, ts_type)); - } - - self.output.push_str("}\n\n"); - } - - fn emit_tagged_union(&mut self, name: &str, variants: &BTreeMap, export: &str) { - let mut variant_types = Vec::new(); - - for (&sym, &type_id) in variants { - let variant_name = self.interner.resolve(sym); - let variant_type_name = format!("{}{}", name, to_pascal_case(variant_name)); - variant_types.push(variant_type_name.clone()); - - let data_str = self.inline_data_type(type_id); - self.output.push_str(&format!( - "{}interface {} {{\n $tag: \"{}\";\n $data: {};\n}}\n\n", - export, variant_type_name, variant_name, data_str - )); - } - - let union = variant_types.join(" | "); - self.output - .push_str(&format!("{}type {} = {};\n\n", export, name, union)); - } - - fn emit_custom_type_alias(&mut self, name: &str) { - let export = if self.config.export { "export " } else { "" }; - self.output - .push_str(&format!("{}type {} = Node;\n\n", export, name)); - } - - fn emit_type_alias(&mut self, alias_name: &str, target_name: &str) { - let export = if self.config.export { "export " } else { "" }; - self.output.push_str(&format!( - "{}type {} = {};\n\n", - export, alias_name, target_name - )); - } - - fn emit_node_interface(&mut self) { - let export = if self.config.export { "export " } else { "" }; - if self.config.verbose_nodes { - self.output.push_str(&format!( - "{}interface Node {{\n kind: string;\n text: string;\n startPosition: {{ row: number; column: number }};\n endPosition: {{ row: number; column: number }};\n}}\n\n", - export - )); - } else { - self.output.push_str(&format!( - "{}interface Node {{\n kind: string;\n text: string;\n}}\n\n", - export - )); - } - } - - fn type_to_ts(&self, type_id: TypeId) -> String { - match type_id { - TYPE_VOID => return "void".to_string(), - TYPE_NODE => return "Node".to_string(), - TYPE_STRING => return "string".to_string(), - _ => {} - } - - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for TS conversion"); - - match kind { - TypeKind::Void => "void".to_string(), - TypeKind::Node => "Node".to_string(), - TypeKind::String => "string".to_string(), - TypeKind::Custom(sym) => self.interner.resolve(*sym).to_string(), - TypeKind::Ref(def_id) => to_pascal_case(self.ctx.def_name(self.interner, *def_id)), - - TypeKind::Struct(fields) => { - if let Some(name) = self.type_names.get(&type_id) { - name.clone() - } else { - self.inline_struct(fields) - } - } - TypeKind::Enum(variants) => { - if let Some(name) = self.type_names.get(&type_id) { - name.clone() - } else { - self.inline_enum(variants) - } - } - TypeKind::Array { element, non_empty } => { - let elem_type = self.type_to_ts(*element); - if *non_empty { - format!("[{}, ...{}[]]", elem_type, elem_type) - } else { - format!("{}[]", elem_type) - } - } - TypeKind::Optional(inner) => format!("{} | null", self.type_to_ts(*inner)), - } - } - - fn inline_struct(&self, fields: &BTreeMap) -> String { - if fields.is_empty() { - return "{}".to_string(); - } - - let field_strs: Vec<_> = self - .sort_map_by_name(fields) - .into_iter() - .map(|(&sym, info)| { - let name = self.interner.resolve(sym); - let ts_type = self.type_to_ts(info.type_id); - let optional = if info.optional { "?" } else { "" }; - format!("{}{}: {}", name, optional, ts_type) - }) - .collect(); - - format!("{{ {} }}", field_strs.join("; ")) - } - - fn inline_enum(&self, variants: &BTreeMap) -> String { - let variant_strs: Vec<_> = self - .sort_map_by_name(variants) - .into_iter() - .map(|(&sym, &type_id)| { - let name = self.interner.resolve(sym); - let data_type = self.type_to_ts(type_id); - format!("{{ $tag: \"{}\"; $data: {} }}", name, data_type) - }) - .collect(); - - variant_strs.join(" | ") - } - - fn inline_data_type(&self, type_id: TypeId) -> String { - let kind = self - .ctx - .get_type(type_id) - .expect("valid TypeId required for inline data emission"); - - match kind { - TypeKind::Struct(fields) => self.inline_struct(fields), - TypeKind::Void => "{}".to_string(), - _ => self.type_to_ts(type_id), - } - } - - fn needs_generated_name(&self, kind: &TypeKind) -> bool { - matches!(kind, TypeKind::Struct(_) | TypeKind::Enum(_)) - } - - fn generate_contextual_name(&mut self, ctx: &NamingContext) -> String { - let base = if let Some(field) = &ctx.field_name { - format!("{}{}", to_pascal_case(&ctx.def_name), to_pascal_case(field)) - } else { - to_pascal_case(&ctx.def_name) - }; - self.unique_name(&base) - } - - fn generate_fallback_name(&mut self, kind: &TypeKind) -> String { - let base = match kind { - TypeKind::Struct(_) => "Struct", - TypeKind::Enum(_) => "Enum", - _ => "Type", - }; - self.unique_name(base) - } - - fn unique_name(&mut self, base: &str) -> String { - let base = to_pascal_case(base); - if self.used_names.insert(base.clone()) { - return base; - } - - let mut counter = 2; - loop { - let name = format!("{}{}", base, counter); - if self.used_names.insert(name.clone()) { - return name; - } - counter += 1; - } - } - - /// Helper to iterate map sorted by resolved symbol name. - fn sort_map_by_name<'b, T>(&self, map: &'b BTreeMap) -> Vec<(&'b Symbol, &'b T)> { - let mut items: Vec<_> = map.iter().collect(); - items.sort_by_key(|&(&sym, _)| self.interner.resolve(sym)); - items - } -} - -fn to_pascal_case(s: &str) -> String { - let mut result = String::with_capacity(s.len()); - let mut capitalize_next = true; - - for c in s.chars() { - if c == '_' || c == '-' || c == '.' { - capitalize_next = true; - } else if capitalize_next { - result.extend(c.to_uppercase()); - capitalize_next = false; - } else { - result.push(c); - } - } - result -} - -pub fn emit_typescript(ctx: &TypeContext, interner: &Interner) -> String { - TsEmitter::new(ctx, interner, EmitConfig::default()).emit() -} - -pub fn emit_typescript_with_config( - ctx: &TypeContext, - interner: &Interner, - config: EmitConfig, -) -> String { - TsEmitter::new(ctx, interner, config).emit() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn to_pascal_case_works() { - assert_eq!(to_pascal_case("foo"), "Foo"); - assert_eq!(to_pascal_case("foo_bar"), "FooBar"); - assert_eq!(to_pascal_case("foo-bar"), "FooBar"); - assert_eq!(to_pascal_case("_"), ""); - assert_eq!(to_pascal_case("FooBar"), "FooBar"); - } - - #[test] - fn emit_node_type_only_when_referenced() { - // Empty context - Node should not be emitted - let ctx = TypeContext::new(); - let interner = Interner::new(); - let output = TsEmitter::new(&ctx, &interner, EmitConfig::default()).emit(); - assert!(!output.contains("interface Node")); - - // Context with a definition using Node - should emit Node - let mut ctx = TypeContext::new(); - let mut interner = Interner::new(); - let x_sym = interner.intern("x"); - let mut fields = BTreeMap::new(); - fields.insert(x_sym, FieldInfo::required(TYPE_NODE)); - let struct_id = ctx.intern_type(TypeKind::Struct(fields)); - ctx.set_def_type_by_name(&mut interner, "Q", struct_id); - - let output = TsEmitter::new(&ctx, &interner, EmitConfig::default()).emit(); - assert!(output.contains("interface Node")); - assert!(output.contains("kind: string")); - } -} diff --git a/crates/plotnik-lib/src/query/type_check/infer.rs b/crates/plotnik-lib/src/query/type_check/infer.rs index 56fc8d17..9b0bb4c2 100644 --- a/crates/plotnik-lib/src/query/type_check/infer.rs +++ b/crates/plotnik-lib/src/query/type_check/infer.rs @@ -330,7 +330,6 @@ impl<'a, 'd> InferenceVisitor<'a, 'd> { } else { FieldInfo::required(captured_type) }; - TermInfo::new( inner_info.arity, TypeFlow::Bubble(self.ctx.intern_single_field(capture_name, field_info)), diff --git a/crates/plotnik-lib/src/query/type_check/mod.rs b/crates/plotnik-lib/src/query/type_check/mod.rs index 9a5e116b..7cd56d0e 100644 --- a/crates/plotnik-lib/src/query/type_check/mod.rs +++ b/crates/plotnik-lib/src/query/type_check/mod.rs @@ -4,7 +4,6 @@ //! (for TypeScript emission) in a single traversal. mod context; -mod emit_ts; mod infer; mod symbol; mod types; @@ -14,7 +13,6 @@ mod unify; mod tests; pub use context::TypeContext; -pub use emit_ts::{EmitConfig, TsEmitter, emit_typescript, emit_typescript_with_config}; pub use symbol::{DefId, Interner, Symbol}; pub use types::{ Arity, FieldInfo, QuantifierKind, TYPE_NODE, TYPE_STRING, TYPE_VOID, TermInfo, TypeFlow, diff --git a/crates/plotnik-lib/src/query/type_check/symbol.rs b/crates/plotnik-lib/src/query/type_check/symbol.rs index 5fbeeddf..e0fe3a3e 100644 --- a/crates/plotnik-lib/src/query/type_check/symbol.rs +++ b/crates/plotnik-lib/src/query/type_check/symbol.rs @@ -2,7 +2,8 @@ pub use plotnik_core::{Interner, Symbol}; /// A lightweight handle to a named definition. /// -/// Assigned during dependency analysis. +/// Assigned during dependency analysis. Used as a key for looking up +/// definition-level metadata (types, recursion status) in TypeContext. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct DefId(u32); diff --git a/crates/plotnik-lib/src/query/type_check/types.rs b/crates/plotnik-lib/src/query/type_check/types.rs index cb724937..ce4c8429 100644 --- a/crates/plotnik-lib/src/query/type_check/types.rs +++ b/crates/plotnik-lib/src/query/type_check/types.rs @@ -8,17 +8,24 @@ use std::collections::BTreeMap; use super::symbol::{DefId, Symbol}; +// Re-export shared type system components +pub use crate::type_system::{Arity, QuantifierKind}; +use crate::type_system::{PrimitiveType, TYPE_STRING as PRIM_TYPE_STRING}; + /// Interned type identifier. -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] +/// +/// Index into the type registry. Values 0-2 are reserved for builtins +/// (Void, Node, String); custom types start at index 3. +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct TypeId(pub u32); -pub const TYPE_VOID: TypeId = TypeId(0); -pub const TYPE_NODE: TypeId = TypeId(1); -pub const TYPE_STRING: TypeId = TypeId(2); +pub const TYPE_VOID: TypeId = TypeId(PrimitiveType::Void.index() as u32); +pub const TYPE_NODE: TypeId = TypeId(PrimitiveType::Node.index() as u32); +pub const TYPE_STRING: TypeId = TypeId(PrimitiveType::String.index() as u32); impl TypeId { pub fn is_builtin(self) -> bool { - self.0 <= TYPE_STRING.0 + self.0 <= PRIM_TYPE_STRING as u32 } } @@ -58,7 +65,9 @@ impl TypeKind { /// Field information within a struct type. #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] pub struct FieldInfo { + /// The type of this field's value. pub type_id: TypeId, + /// Whether this field may be absent (from alternation branches). pub optional: bool, } @@ -85,25 +94,6 @@ impl FieldInfo { } } -/// Structural arity - whether an expression matches one or many positions. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum Arity { - /// Exactly one node position. - One, - /// Multiple sequential positions. - Many, -} - -impl Arity { - /// Combine arities: Many wins. - pub fn combine(self, other: Self) -> Self { - if self == Self::One && other == Self::One { - return Self::One; - } - Self::Many - } -} - /// Data flow through an expression. #[derive(Clone, Debug)] pub enum TypeFlow { @@ -139,7 +129,9 @@ impl TypeFlow { /// Combined arity and type flow information for an expression. #[derive(Clone, Debug)] pub struct TermInfo { + /// How many times this expression matches (one vs many). pub arity: Arity, + /// What data flows through this expression. pub flow: TypeFlow, } @@ -176,25 +168,3 @@ impl TermInfo { } } } - -/// Quantifier kind for type inference. -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum QuantifierKind { - /// `?` or `??` - zero or one. - Optional, - /// `*` or `*?` - zero or more. - ZeroOrMore, - /// `+` or `+?` - one or more. - OneOrMore, -} - -impl QuantifierKind { - /// Whether this quantifier requires strict dimensionality (row capture). - pub fn requires_row_capture(self) -> bool { - matches!(self, Self::ZeroOrMore | Self::OneOrMore) - } - - pub fn is_non_empty(self) -> bool { - matches!(self, Self::OneOrMore) - } -}