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
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion crates/plotnik-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -233,5 +233,7 @@ clap = { version = "4.5", features = ["derive"] }
plotnik-core = { version = "0.1.0", path = "../plotnik-core" }
plotnik-langs = { version = "0.1.0", path = "../plotnik-langs", default-features = false }
plotnik-lib = { version = "0.1.0", path = "../plotnik-lib" }
arborium-tree-sitter = "2.3.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
arborium-tree-sitter = "2.3.2"
thiserror = "2.0"
63 changes: 52 additions & 11 deletions crates/plotnik-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,8 @@ NOTE: Use --verbose-nodes to match `exec --verbose-nodes` output shape."#)]
/// Execute a query against source code and output JSON
#[command(after_help = r#"EXAMPLES:
plotnik exec query.ptk app.js
plotnik exec -q '(identifier) @id' -s app.js
plotnik exec query.ptk app.ts --pretty
plotnik exec query.ptk app.ts --verbose-nodes"#)]
plotnik exec -q '(identifier) @id' -s 'let x = 1' -l javascript
plotnik exec query.ptk app.ts --compact"#)]
Exec {
/// Query file or workspace directory
#[arg(value_name = "QUERY")]
Expand All @@ -153,14 +152,10 @@ NOTE: Use --verbose-nodes to match `exec --verbose-nodes` output shape."#)]
#[arg(short = 'q', long = "query", value_name = "TEXT")]
query_text: Option<String>,

/// Source code as inline text
#[arg(long = "source", value_name = "TEXT")]
/// Inline source text
#[arg(short = 's', long = "source", value_name = "TEXT")]
source_text: Option<String>,

/// Source file (alternative to positional)
#[arg(short = 's', long = "source-file", value_name = "FILE")]
source_file: Option<PathBuf>,

/// Language (inferred from source extension if not specified)
#[arg(short = 'l', long, value_name = "LANG")]
lang: Option<String>,
Expand All @@ -172,6 +167,52 @@ NOTE: Use --verbose-nodes to match `exec --verbose-nodes` output shape."#)]
output: OutputArgs,
},

/// Trace query execution for debugging
#[command(after_help = r#"EXAMPLES:
plotnik trace -q '(identifier) @id' -s 'let x = 1' -l javascript
plotnik trace query.ptk app.ts
plotnik trace query.ptk app.ts --no-result"#)]
Trace {
/// Query file or workspace directory
#[arg(value_name = "QUERY")]
query_path: Option<PathBuf>,

/// Source file to execute against
#[arg(value_name = "SOURCE")]
source_path: Option<PathBuf>,

/// Inline query text
#[arg(short = 'q', long = "query", value_name = "TEXT")]
query_text: Option<String>,

/// Inline source text
#[arg(short = 's', long = "source", value_name = "TEXT")]
source_text: Option<String>,

/// Language (inferred from source extension if not specified)
#[arg(short = 'l', long, value_name = "LANG")]
lang: Option<String>,

/// Entry point name (definition to match from)
#[arg(long, value_name = "NAME")]
entry: Option<String>,

/// Verbosity level (-v for verbose, -vv for very verbose)
#[arg(short = 'v', action = clap::ArgAction::Count)]
verbose: u8,

/// Skip materialization, show effects only
#[arg(long)]
no_result: bool,

/// Execution fuel limit
#[arg(long, default_value = "1000000", value_name = "N")]
fuel: u32,

#[command(flatten)]
output: OutputArgs,
},

/// List supported languages
Langs,
}
Expand Down Expand Up @@ -212,9 +253,9 @@ pub struct InferOutputArgs {

#[derive(Args)]
pub struct ExecOutputArgs {
/// Pretty-print JSON output
/// Output compact JSON (default: pretty when stdout is a TTY)
#[arg(long)]
pub pretty: bool,
pub compact: bool,

/// Include verbose node information (line/column positions)
#[arg(long)]
Expand Down
147 changes: 50 additions & 97 deletions crates/plotnik-cli/src/commands/exec.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
use std::fs;
use std::io::{self, Read};
//! Execute a query and output JSON result.

use std::path::PathBuf;

use plotnik_langs::Lang;
use plotnik_lib::bytecode::Module;
use plotnik_lib::emit::emit_linked;
use plotnik_lib::Colors;
use plotnik_lib::QueryBuilder;

use super::lang_resolver::{resolve_lang_required, suggest_language};
use plotnik_lib::engine::{FuelLimits, Materializer, RuntimeError, ValueMaterializer, VM};

use super::query_loader::load_query_source;
use super::run_common;

pub struct ExecArgs {
pub query_path: Option<PathBuf>,
Expand All @@ -15,38 +19,42 @@ pub struct ExecArgs {
pub source_text: Option<String>,
pub lang: Option<String>,
pub pretty: bool,
pub verbose_nodes: bool,
pub check: bool,
pub entry: Option<String>,
pub color: bool,
}

pub fn run(args: ExecArgs) {
if let Err(msg) = validate(&args) {
if let Err(msg) = run_common::validate(
args.query_text.is_some() || args.query_path.is_some(),
args.source_text.is_some() || args.source_path.is_some(),
args.source_text.is_some(),
args.lang.is_some(),
) {
eprintln!("error: {}", msg);
std::process::exit(1);
}

let source_map = match load_query_source(
args.query_path.as_deref(),
args.query_text.as_deref(),
) {
Ok(map) => map,
Err(msg) => {
eprintln!("error: {}", msg);
std::process::exit(1);
}
};
let source_map =
match load_query_source(args.query_path.as_deref(), args.query_text.as_deref()) {
Ok(map) => map,
Err(msg) => {
eprintln!("error: {}", msg);
std::process::exit(1);
}
};

if source_map.is_empty() {
eprintln!("error: query cannot be empty");
std::process::exit(1);
}

let _source_code = load_source(&args);
let lang = resolve_source_lang(&args);
let source_code = run_common::load_source(
args.source_text.as_deref(),
args.source_path.as_deref(),
args.query_path.as_deref(),
);
let lang = run_common::resolve_lang(args.lang.as_deref(), args.source_path.as_deref());

// Parse and analyze query
let query = match QueryBuilder::new(source_map).parse() {
Ok(parsed) => parsed.analyze().link(&lang),
Err(e) => {
Expand All @@ -58,91 +66,36 @@ pub fn run(args: ExecArgs) {
if !query.is_valid() {
eprint!(
"{}",
query.diagnostics().render_colored(query.source_map(), args.color)
query
.diagnostics()
.render_colored(query.source_map(), args.color)
);
std::process::exit(1);
}

let _ = (args.pretty, args.verbose_nodes, args.check, args.entry);
let bytecode = emit_linked(&query).expect("emit failed");
let module = Module::from_bytes(bytecode).expect("module load failed");

eprintln!("The 'exec' command is under development.");
eprintln!();
eprintln!("For now, use 'plotnik infer' to generate TypeScript types.");
std::process::exit(0);
}
let entrypoint = run_common::resolve_entrypoint(&module, args.entry.as_deref());
let tree = lang.parse(&source_code);
let trivia_types = run_common::build_trivia_types(&module);

fn load_source(args: &ExecArgs) -> String {
if let Some(ref text) = args.source_text {
return text.clone();
}
if let Some(ref path) = args.source_path {
if path.as_os_str() == "-" {
// Check if query is also from stdin
if args.query_path.as_ref().map(|p| p.as_os_str() == "-").unwrap_or(false) {
eprintln!("error: query and source cannot both be from stdin");
std::process::exit(1);
}
let mut buf = String::new();
io::stdin()
.read_to_string(&mut buf)
.expect("failed to read stdin");
return buf;
}
return fs::read_to_string(path).unwrap_or_else(|e| {
eprintln!("error: failed to read '{}': {}", path.display(), e);
let vm = VM::new(&tree, trivia_types, FuelLimits::default());
let effects = match vm.execute(&module, &entrypoint) {
Ok(effects) => effects,
Err(RuntimeError::NoMatch) => {
std::process::exit(1);
});
}
unreachable!("validation ensures source input exists")
}

fn resolve_source_lang(args: &ExecArgs) -> Lang {
if let Some(ref name) = args.lang {
return resolve_lang_required(name).unwrap_or_else(|msg| {
eprintln!("error: {}", msg);
if let Some(suggestion) = suggest_language(name) {
eprintln!();
eprintln!("Did you mean '{}'?", suggestion);
}
eprintln!();
eprintln!("Run 'plotnik langs' for the full list.");
std::process::exit(1);
});
}

if let Some(ref path) = args.source_path
&& path.as_os_str() != "-"
&& let Some(ext) = path.extension().and_then(|e| e.to_str())
{
if let Some(lang) = plotnik_langs::from_ext(ext) {
return lang;
}
eprintln!(
"error: cannot infer language from extension '.{}', use --lang",
ext
);
std::process::exit(1);
}

eprintln!("error: --lang is required (cannot infer from input)");
std::process::exit(1);
}

fn validate(args: &ExecArgs) -> Result<(), &'static str> {
let has_query = args.query_text.is_some() || args.query_path.is_some();
let has_source = args.source_text.is_some() || args.source_path.is_some();

if !has_query {
return Err("query is required: use positional argument, -q/--query, or --query-file");
}

if !has_source {
return Err("source is required: use positional argument, -s/--source-file, or --source");
}
Err(e) => {
eprintln!("runtime error: {}", e);
std::process::exit(2);
}
};

if args.source_text.is_some() && args.lang.is_none() {
return Err("--lang is required when using --source");
}
let materializer = ValueMaterializer::new(&source_code, module.types(), module.strings());
let value = materializer.materialize(effects.as_slice(), entrypoint.result_type);

Ok(())
let colors = Colors::new(args.color);
let output = value.format(args.pretty, colors);
println!("{}", output);
}
4 changes: 4 additions & 0 deletions crates/plotnik-cli/src/commands/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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;
Expand Down Expand Up @@ -101,11 +102,14 @@ pub fn run(args: InferArgs) {
Some("null") => typescript::VoidType::Null,
_ => typescript::VoidType::Undefined,
};
// 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 output = typescript::emit_with_config(&module, config);

Expand Down
2 changes: 2 additions & 0 deletions crates/plotnik-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ pub mod infer;
pub mod lang_resolver;
pub mod langs;
pub mod query_loader;
pub mod run_common;
pub mod trace;
pub mod tree;

#[cfg(test)]
Expand Down
Loading