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
4 changes: 4 additions & 0 deletions crates/plotnik-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ pub struct InferOutputArgs {
#[arg(long)]
pub no_export: bool,

/// Type for void results: undefined (default) or null
#[arg(long, value_name = "TYPE")]
pub void_type: Option<String>,

/// Write output to file
#[arg(short = 'o', long, value_name = "FILE")]
pub output: Option<PathBuf>,
Expand Down
72 changes: 42 additions & 30 deletions crates/plotnik-cli/src/commands/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct InferArgs {
pub export: bool,
pub output: Option<PathBuf>,
pub color: bool,
pub void_type: Option<String>,
}

pub fn run(args: InferArgs) {
Expand All @@ -45,55 +46,66 @@ pub fn run(args: InferArgs) {
std::process::exit(1);
}

// Resolve language (required for infer)
let lang = args
.lang
.as_deref()
.map(|name| {
resolve_lang_required(name).unwrap_or_else(|msg| {
// Parse and analyze
let query = match QueryBuilder::new(source_map).parse() {
Ok(parsed) => parsed.analyze(),
Err(e) => {
eprintln!("error: {}", e);
std::process::exit(1);
}
};

// Resolve language (optional - enables linking)
let lang = if let Some(lang_name) = &args.lang {
match resolve_lang_required(lang_name) {
Ok(l) => Some(l),
Err(msg) => {
eprintln!("error: {}", msg);
if let Some(suggestion) = suggest_language(name) {
if let Some(suggestion) = suggest_language(lang_name) {
eprintln!();
eprintln!("Did you mean '{}'?", suggestion);
}
eprintln!();
eprintln!("Run 'plotnik langs' for the full list.");
std::process::exit(1);
})
})
.or_else(|| resolve_lang(None, args.query_path.as_deref()));

let Some(lang) = lang else {
eprintln!("error: --lang is required for type generation");
std::process::exit(1);
}
}
} else {
resolve_lang(None, args.query_path.as_deref())
};

// Parse, analyze, and link
let query = match QueryBuilder::new(source_map).parse() {
Ok(parsed) => parsed.analyze().link(&lang),
Err(e) => {
eprintln!("error: {}", e);
let bytecode = if let Some(lang) = lang {
let linked = query.link(&lang);
if !linked.is_valid() {
eprint!(
"{}",
linked.diagnostics().render_colored(linked.source_map(), args.color)
);
std::process::exit(1);
}
linked.emit().expect("bytecode emission failed")
} else {
if !query.is_valid() {
eprint!(
"{}",
query.diagnostics().render_colored(query.source_map(), args.color)
);
std::process::exit(1);
}
query.emit().expect("bytecode emission failed")
};

if !query.is_valid() {
eprint!(
"{}",
query.diagnostics().render_colored(query.source_map(), args.color)
);
std::process::exit(1);
}

// Emit to bytecode
let bytecode = query.emit().expect("bytecode emission failed");
let module = Module::from_bytes(bytecode).expect("module loading failed");

// Emit TypeScript types
let void_type = match args.void_type.as_deref() {
Some("null") => typescript::VoidType::Null,
_ => typescript::VoidType::Undefined,
};
let config = typescript::Config {
export: args.export,
emit_node_type: !args.no_node_type,
verbose_nodes: args.verbose_nodes,
void_type,
};
let output = typescript::emit_with_config(&module, config);

Expand Down
1 change: 1 addition & 0 deletions crates/plotnik-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ fn main() {
export: !infer_output.no_export,
output: infer_output.output,
color: output.color.should_colorize(),
void_type: infer_output.void_type,
});
}
Command::Exec {
Expand Down
40 changes: 38 additions & 2 deletions crates/plotnik-lib/src/typegen/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ use crate::bytecode::{
EntrypointsView, Module, QTypeId, StringsView, TypeDef, TypeKind, TypesView,
};

/// How to represent the void type in TypeScript.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum VoidType {
/// `undefined` - the absence of a value
#[default]
Undefined,
/// `null` - explicit null value
Null,
}

/// Configuration for TypeScript emission.
#[derive(Clone, Debug)]
pub struct Config {
Expand All @@ -21,6 +31,8 @@ pub struct Config {
pub emit_node_type: bool,
/// Use verbose node representation (with kind, text, etc.)
pub verbose_nodes: bool,
/// How to represent the void type
pub void_type: VoidType,
}

impl Default for Config {
Expand All @@ -29,6 +41,7 @@ impl Default for Config {
export: true,
emit_node_type: true,
verbose_nodes: false,
void_type: VoidType::default(),
}
}
}
Expand Down Expand Up @@ -109,6 +122,20 @@ impl<'a> Emitter<'a> {
}
}

// Emit entrypoints with primitive result types (like VOID)
// These are not in to_emit because collect_reachable_types skips primitives
for (&type_id, name) in &primary_names {
if self.emitted.contains(&type_id) {
continue;
}
let Some(type_def) = self.types.get(type_id) else {
continue;
};
if let Some(kind) = type_def.type_kind() && kind.is_primitive() {
self.emit_type_definition(name, type_id);
}
}

// Emit aliases
for (alias_name, type_id) in aliases {
if let Some(primary_name) = primary_names.get(&type_id) {
Expand Down Expand Up @@ -251,7 +278,9 @@ impl<'a> Emitter<'a> {
TypeKind::Node => {
self.node_referenced = true;
}
TypeKind::String | TypeKind::Void => {}
TypeKind::String | TypeKind::Void => {
// No action needed for primitives
}
TypeKind::Struct | TypeKind::Enum => {
let member_types: Vec<_> = self
.types
Expand All @@ -266,6 +295,7 @@ impl<'a> Emitter<'a> {
self.collect_refs_recursive(QTypeId(type_def.data));
}
TypeKind::Alias => {
// Alias to Node
self.node_referenced = true;
}
}
Expand Down Expand Up @@ -361,11 +391,14 @@ impl<'a> Emitter<'a> {
return;
};

// 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) {
for member in self.types.members_of(&type_def) {
self.collect_reachable_types(member.type_id, out);
}
} else {
// For non-struct payloads, fall back to regular collection.
self.collect_reachable_types(type_id, out);
}
}
Expand Down Expand Up @@ -574,7 +607,10 @@ impl<'a> Emitter<'a> {
};

match kind {
TypeKind::Void => "undefined".to_string(),
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 => {
Expand Down