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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions crates/plotnik-lib/src/infer/emit/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ pub struct RustEmitConfig {
pub derive_debug: bool,
pub derive_clone: bool,
pub derive_partial_eq: bool,
/// Name for the default (unnamed) query entry point type.
pub default_query_name: String,
}

/// How to handle cyclic type references.
Expand All @@ -32,6 +34,7 @@ impl Default for RustEmitConfig {
derive_debug: true,
derive_clone: true,
derive_partial_eq: false,
default_query_name: "QueryResult".to_string(),
}
}
}
Expand Down Expand Up @@ -67,7 +70,10 @@ fn emit_type_def(
table: &TypeTable<'_>,
config: &RustEmitConfig,
) -> String {
let name = key.to_pascal_case();
let name = match key {
TypeKey::DefaultQuery => config.default_query_name.clone(),
_ => key.to_pascal_case(),
};

match value {
TypeValue::Node | TypeValue::String | TypeValue::Unit | TypeValue::Invalid => String::new(),
Expand Down Expand Up @@ -148,7 +154,10 @@ pub(crate) fn emit_type_ref(
format!("Vec<{}>", inner_str)
}
// Struct, TaggedUnion, or undefined forward reference - use pascal-cased name
Some(TypeValue::Struct(_)) | Some(TypeValue::TaggedUnion(_)) | None => key.to_pascal_case(),
Some(TypeValue::Struct(_)) | Some(TypeValue::TaggedUnion(_)) | None => match key {
TypeKey::DefaultQuery => config.default_query_name.clone(),
_ => key.to_pascal_case(),
},
};

if is_cyclic {
Expand Down
53 changes: 53 additions & 0 deletions crates/plotnik-lib/src/infer/emit/rust_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,3 +537,56 @@ fn emit_builtin_value_with_named_key() {
"#};
insta::assert_snapshot!(emit(input), @"");
}

// --- DefaultQuery ---

#[test]
fn emit_default_query_struct() {
let input = "#DefaultQuery = { #Node @value }";

insta::assert_snapshot!(emit(input), @r"
#[derive(Debug, Clone)]
pub struct QueryResult {
pub value: Node,
}
");
}

#[test]
fn emit_default_query_custom_name() {
let input = "#DefaultQuery = { #Node @value }";
let config = RustEmitConfig {
default_query_name: "MyResult".to_string(),
..Default::default()
};

insta::assert_snapshot!(emit_with_config(input, &config), @r"
#[derive(Debug, Clone)]
pub struct MyResult {
pub value: Node,
}
");
}

#[test]
fn emit_default_query_referenced() {
let input = indoc! {r#"
Item = { #Node @value }
Items = Item*
#DefaultQuery = { Items @items }
"#};

insta::assert_snapshot!(emit(input), @r"
#[derive(Debug, Clone)]
pub struct Item {
pub value: Node,
}

pub type Items = Vec<Item>;

#[derive(Debug, Clone)]
pub struct QueryResult {
pub items: Vec<Item>,
}
");
}
19 changes: 15 additions & 4 deletions crates/plotnik-lib/src/infer/emit/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ pub struct TypeScriptEmitConfig {
pub node_type_name: String,
/// Whether to emit `type Foo = ...` instead of `interface Foo { ... }`.
pub use_type_alias: bool,
/// Name for the default (unnamed) query entry point.
pub default_query_name: String,
}

/// How to represent optional types.
Expand All @@ -43,6 +45,7 @@ impl Default for TypeScriptEmitConfig {
inline_synthetic: true,
node_type_name: "SyntaxNode".to_string(),
use_type_alias: false,
default_query_name: "QueryResult".to_string(),
}
}
}
Expand Down Expand Up @@ -83,7 +86,7 @@ fn emit_type_def(
table: &TypeTable<'_>,
config: &TypeScriptEmitConfig,
) -> String {
let name = key.to_pascal_case();
let name = type_name(key, config);
let export_prefix = if config.export && !matches!(key, TypeKey::Synthetic(_)) {
"export "
} else {
Expand Down Expand Up @@ -192,13 +195,13 @@ pub(crate) fn emit_field_type(
if config.inline_synthetic && matches!(key, TypeKey::Synthetic(_)) {
(emit_inline_struct(fields, table, config), false)
} else {
(key.to_pascal_case(), false)
(type_name(key, config), false)
}
}

Some(TypeValue::TaggedUnion(_)) => (key.to_pascal_case(), false),
Some(TypeValue::TaggedUnion(_)) => (type_name(key, config), false),

None => (key.to_pascal_case(), false),
None => (type_name(key, config), false),
}
}

Expand Down Expand Up @@ -231,6 +234,14 @@ pub(crate) fn emit_inline_struct(
out
}

fn type_name(key: &TypeKey<'_>, config: &TypeScriptEmitConfig) -> String {
if key.is_default_query() {
config.default_query_name.clone()
} else {
key.to_pascal_case()
}
}

pub(crate) fn wrap_if_union(type_str: &str) -> String {
if type_str.contains('|') {
format!("({})", type_str)
Expand Down
50 changes: 50 additions & 0 deletions crates/plotnik-lib/src/infer/emit/typescript_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,7 @@ fn emit_all_config_options() {
inline_synthetic: true,
node_type_name: "ASTNode".to_string(),
use_type_alias: false,
default_query_name: "QueryResult".to_string(),
};
insta::assert_snapshot!(emit_with_config(input, &config), @r"
export type MaybeNode = ASTNode;
Expand Down Expand Up @@ -741,3 +742,52 @@ fn emit_builtin_value_with_named_key() {
"#};
insta::assert_snapshot!(emit(input), @"");
}

// --- DefaultQuery ---

#[test]
fn emit_default_query_interface() {
let input = "#DefaultQuery = { #Node @value }";

insta::assert_snapshot!(emit(input), @r"
interface QueryResult {
value: SyntaxNode;
}
");
}

#[test]
fn emit_default_query_custom_name() {
let input = "#DefaultQuery = { #Node @value }";
let config = TypeScriptEmitConfig {
default_query_name: "MyResult".to_string(),
..Default::default()
};

insta::assert_snapshot!(emit_with_config(input, &config), @r"
interface MyResult {
value: SyntaxNode;
}
");
}

#[test]
fn emit_default_query_referenced() {
let input = indoc! {r#"
Item = { #Node @value }
Items = Item*
#DefaultQuery = { Items @items }
"#};

insta::assert_snapshot!(emit(input), @r"
interface Item {
value: SyntaxNode;
}

type Items = Item[];

interface QueryResult {
items: Item[];
}
");
}
16 changes: 16 additions & 0 deletions crates/plotnik-lib/src/infer/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@
//! Built-in types (Node, String, Unit, Invalid) have both a key and value variant for
//! consistency—the key is what you reference, the value is what gets stored.
//!
//! ## DefaultQuery Key
//!
//! `TypeKey::DefaultQuery` represents the unnamed entry point query (the last definition
//! without a name). It has no corresponding `TypeValue` variant—it's purely a key that
//! maps to a Struct or other value. The emitted name ("QueryResult" by default) is
//! configurable per code generator.
//!
//! ## Synthetic Keys
//!
//! For nested captures like `(function @fn { (param @p) @params })`, we need unique type
Expand All @@ -55,6 +62,9 @@ pub enum TypeKey<'src> {
/// Invalid type for unresolvable conflicts (built-in)
/// Emitted same as Unit in code generators.
Invalid,
/// The unnamed entry point query (last definition without a name).
/// Default emitted name is "QueryResult", but emitters may override.
DefaultQuery,
/// User-provided type name via `:: TypeName`
Named(&'src str),
/// Path-based synthetic name: ["Foo", "bar"] → FooBar
Expand All @@ -69,6 +79,7 @@ impl TypeKey<'_> {
TypeKey::String => "String".to_string(),
TypeKey::Unit => "Unit".to_string(),
TypeKey::Invalid => "Unit".to_string(), // Invalid emits as Unit
TypeKey::DefaultQuery => "DefaultQuery".to_string(),
TypeKey::Named(name) => (*name).to_string(),
TypeKey::Synthetic(segments) => segments.iter().map(|s| to_pascal(s)).collect(),
}
Expand All @@ -81,6 +92,11 @@ impl TypeKey<'_> {
TypeKey::Node | TypeKey::String | TypeKey::Unit | TypeKey::Invalid
)
}

/// Returns true if this is the default query entry point.
pub fn is_default_query(&self) -> bool {
matches!(self, TypeKey::DefaultQuery)
}
}

/// Convert snake_case or lowercase to PascalCase.
Expand Down
14 changes: 14 additions & 0 deletions crates/plotnik-lib/src/infer/tyton.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
//! - `#Node` — built-in node type
//! - `#string` — built-in string type
//! - `#Invalid` — built-in invalid type
//! - `#DefaultQuery` — unnamed entry point query
//! - `()` — built-in unit type
//! - `PascalName` — named type
//! - `<Foo bar baz>` — synthetic key from path segments
Expand All @@ -40,6 +41,7 @@
//! - `Name = [ ... ]` — define a tagged union
//! - `Name = Other?` — define an optional
//! - `<Foo bar> = { ... }` — define with synthetic key
//! - `#DefaultQuery = { ... }` — define unnamed entry point
//! - `AliasNode = #Node` — alias to builtin
//!
//! # Example
Expand Down Expand Up @@ -70,6 +72,9 @@ enum Token<'src> {
#[token("#Invalid")]
Invalid,

#[token("#DefaultQuery")]
DefaultQuery,

#[token("()")]
Unit,

Expand Down Expand Up @@ -211,6 +216,10 @@ impl<'src> Parser<'src> {
self.advance();
Ok(TypeKey::Invalid)
}
Some(Token::DefaultQuery) => {
self.advance();
Ok(TypeKey::DefaultQuery)
}
Some(Token::Unit) => {
self.advance();
Ok(TypeKey::Unit)
Expand Down Expand Up @@ -410,6 +419,10 @@ impl<'src> Parser<'src> {
self.advance();
TypeKey::Named(name)
}
Some(Token::DefaultQuery) => {
self.advance();
TypeKey::DefaultQuery
}
Some(Token::LAngle) => self.parse_synthetic_key()?,
_ => {
return Err(ParseError {
Expand Down Expand Up @@ -475,6 +488,7 @@ fn emit_key(out: &mut String, key: &TypeKey<'_>) {
TypeKey::String => out.push_str("#string"),
TypeKey::Invalid => out.push_str("#Invalid"),
TypeKey::Unit => out.push_str("()"),
TypeKey::DefaultQuery => out.push_str("#DefaultQuery"),
TypeKey::Named(name) => out.push_str(name),
TypeKey::Synthetic(segments) => {
out.push('<');
Expand Down