diff --git a/Cargo.lock b/Cargo.lock index b56f927c..35b341f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1519,7 +1519,9 @@ dependencies = [ "plotnik-core", "plotnik-langs", "plotnik-lib", + "serde", "serde_json", + "thiserror", ] [[package]] diff --git a/crates/plotnik-cli/Cargo.toml b/crates/plotnik-cli/Cargo.toml index 2b3abc18..af9afe1a 100644 --- a/crates/plotnik-cli/Cargo.toml +++ b/crates/plotnik-cli/Cargo.toml @@ -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" \ No newline at end of file +thiserror = "2.0" \ No newline at end of file diff --git a/crates/plotnik-cli/src/cli.rs b/crates/plotnik-cli/src/cli.rs index 2d171f6c..a261d98c 100644 --- a/crates/plotnik-cli/src/cli.rs +++ b/crates/plotnik-cli/src/cli.rs @@ -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")] @@ -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, - /// 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, - /// Source file (alternative to positional) - #[arg(short = 's', long = "source-file", value_name = "FILE")] - source_file: Option, - /// Language (inferred from source extension if not specified) #[arg(short = 'l', long, value_name = "LANG")] lang: Option, @@ -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, + + /// Source file to execute against + #[arg(value_name = "SOURCE")] + source_path: Option, + + /// Inline query text + #[arg(short = 'q', long = "query", value_name = "TEXT")] + query_text: Option, + + /// Inline source text + #[arg(short = 's', long = "source", value_name = "TEXT")] + source_text: Option, + + /// Language (inferred from source extension if not specified) + #[arg(short = 'l', long, value_name = "LANG")] + lang: Option, + + /// Entry point name (definition to match from) + #[arg(long, value_name = "NAME")] + entry: Option, + + /// 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, } @@ -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)] diff --git a/crates/plotnik-cli/src/commands/exec.rs b/crates/plotnik-cli/src/commands/exec.rs index 5ae02188..5ac448f7 100644 --- a/crates/plotnik-cli/src/commands/exec.rs +++ b/crates/plotnik-cli/src/commands/exec.rs @@ -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, @@ -15,38 +19,42 @@ pub struct ExecArgs { pub source_text: Option, pub lang: Option, pub pretty: bool, - pub verbose_nodes: bool, - pub check: bool, pub entry: Option, 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) => { @@ -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); } diff --git a/crates/plotnik-cli/src/commands/mod.rs b/crates/plotnik-cli/src/commands/mod.rs index 7c199d0d..9a93451b 100644 --- a/crates/plotnik-cli/src/commands/mod.rs +++ b/crates/plotnik-cli/src/commands/mod.rs @@ -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)] diff --git a/crates/plotnik-cli/src/commands/run_common.rs b/crates/plotnik-cli/src/commands/run_common.rs new file mode 100644 index 00000000..9dfa08c8 --- /dev/null +++ b/crates/plotnik-cli/src/commands/run_common.rs @@ -0,0 +1,123 @@ +//! Shared logic for exec and trace commands. + +use std::fs; +use std::io::{self, Read}; +use std::path::Path; + +use plotnik_langs::Lang; +use plotnik_lib::bytecode::{Entrypoint, Module}; + +use super::lang_resolver::{resolve_lang_required, suggest_language}; + +/// Load source code from file, stdin, or inline text. +pub fn load_source( + source_text: Option<&str>, + source_path: Option<&Path>, + query_path: Option<&Path>, +) -> String { + if let Some(text) = source_text { + return text.to_owned(); + } + if let Some(path) = source_path { + if path.as_os_str() == "-" { + if query_path.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); + std::process::exit(1); + }); + } + unreachable!("validation ensures source input exists") +} + +/// Resolve source language from --lang flag or file extension. +pub fn resolve_lang(lang_name: Option<&str>, source_path: Option<&Path>) -> Lang { + if let Some(name) = lang_name { + 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(path) = 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) +} + +/// Resolve entrypoint by name or use the single available one. +pub fn resolve_entrypoint(module: &Module, name: Option<&str>) -> Entrypoint { + let entries = module.entrypoints(); + let strings = module.strings(); + + match name { + Some(name) => entries.find_by_name(name, &strings).unwrap_or_else(|| { + eprintln!("error: invalid entrypoint: {}", name); + std::process::exit(1); + }), + None => { + if entries.len() == 1 { + entries.get(0) + } else if entries.is_empty() { + eprintln!("error: no entrypoints in module"); + std::process::exit(1); + } else { + eprintln!("error: multiple entrypoints, specify one with --entry"); + std::process::exit(1); + } + } + } +} + +/// Validate common arguments. +pub fn validate( + has_query: bool, + has_source: bool, + source_is_inline: bool, + has_lang: bool, +) -> Result<(), &'static str> { + 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"); + } + if source_is_inline && !has_lang { + return Err("--lang is required when using --source"); + } + Ok(()) +} + +/// Build trivia type list from module. +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) + .collect() +} diff --git a/crates/plotnik-cli/src/commands/trace.rs b/crates/plotnik-cli/src/commands/trace.rs new file mode 100644 index 00000000..49e23798 --- /dev/null +++ b/crates/plotnik-cli/src/commands/trace.rs @@ -0,0 +1,121 @@ +//! Trace query execution for debugging. + +use std::path::PathBuf; + +use plotnik_lib::bytecode::Module; +use plotnik_lib::emit::emit_linked; +use plotnik_lib::Colors; +use plotnik_lib::QueryBuilder; + +use plotnik_lib::engine::{ + FuelLimits, Materializer, PrintTracer, RuntimeError, ValueMaterializer, Verbosity, VM, +}; + +use super::query_loader::load_query_source; +use super::run_common; + +pub struct TraceArgs { + pub query_path: Option, + pub query_text: Option, + pub source_path: Option, + pub source_text: Option, + pub lang: Option, + pub entry: Option, + pub verbosity: Verbosity, + pub no_result: bool, + pub fuel: u32, + pub color: bool, +} + +pub fn run(args: TraceArgs) { + 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); + } + }; + + if source_map.is_empty() { + eprintln!("error: query cannot be empty"); + std::process::exit(1); + } + + 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()); + + let query = match QueryBuilder::new(source_map).parse() { + Ok(parsed) => parsed.analyze().link(&lang), + Err(e) => { + eprintln!("error: {}", e); + std::process::exit(1); + } + }; + + if !query.is_valid() { + eprint!( + "{}", + query + .diagnostics() + .render_colored(query.source_map(), args.color) + ); + std::process::exit(1); + } + + let bytecode = emit_linked(&query).expect("emit failed"); + let module = Module::from_bytes(bytecode).expect("module load failed"); + + 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); + + let limits = FuelLimits { + exec_fuel: args.fuel, + ..Default::default() + }; + let vm = VM::new(&tree, trivia_types, limits); + let colors = Colors::new(args.color); + let mut tracer = PrintTracer::new(&source_code, &module, args.verbosity, colors); + + let effects = match vm.execute_with(&module, &entrypoint, &mut tracer) { + Ok(effects) => { + tracer.print(); + effects + } + Err(RuntimeError::NoMatch) => { + tracer.print(); + std::process::exit(1); + } + Err(e) => { + tracer.print(); + eprintln!("runtime error: {}", e); + std::process::exit(2); + } + }; + + if args.no_result { + return; + } + + 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 output = value.format(true, colors); + println!("{}", output); +} diff --git a/crates/plotnik-cli/src/main.rs b/crates/plotnik-cli/src/main.rs index 761f24bb..769d39ee 100644 --- a/crates/plotnik-cli/src/main.rs +++ b/crates/plotnik-cli/src/main.rs @@ -6,6 +6,7 @@ use commands::check::CheckArgs; use commands::dump::DumpArgs; use commands::exec::ExecArgs; use commands::infer::InferArgs; +use commands::trace::TraceArgs; use commands::tree::TreeArgs; fn main() { @@ -80,26 +81,56 @@ fn main() { source_path, query_text, source_text, - source_file, lang, exec_output, output, } => { - // Merge source_path and source_file (positional takes precedence) - let resolved_source = source_path.or(source_file); + // Pretty by default when stdout is a TTY, unless --compact is passed + let pretty = + !exec_output.compact && std::io::IsTerminal::is_terminal(&std::io::stdout()); commands::exec::run(ExecArgs { query_path, query_text, - source_path: resolved_source, + source_path, source_text, lang, - pretty: exec_output.pretty, - verbose_nodes: exec_output.verbose_nodes, - check: exec_output.check, + pretty, entry: exec_output.entry, color: output.color.should_colorize(), }); } + Command::Trace { + query_path, + source_path, + query_text, + source_text, + lang, + entry, + verbose, + no_result, + fuel, + output, + } => { + use plotnik_lib::engine::Verbosity; + + let verbosity = match verbose { + 0 => Verbosity::Default, + 1 => Verbosity::Verbose, + _ => Verbosity::VeryVerbose, + }; + commands::trace::run(TraceArgs { + query_path, + query_text, + source_path, + source_text, + lang, + entry, + verbosity, + no_result, + fuel, + color: output.color.should_colorize(), + }); + } Command::Langs => { commands::langs::run(); } diff --git a/docs/README.md b/docs/README.md index 0406ee17..e1d87558 100644 --- a/docs/README.md +++ b/docs/README.md @@ -14,6 +14,7 @@ Plotnik is a strongly-typed pattern matching language for tree-sitter syntax tre - [AGENTS.md](../AGENTS.md) — Project rules, coding standards, testing patterns - [Runtime Engine](runtime-engine.md) — VM execution model +- [Tree Navigation](tree-navigation.md) — Cursor walk implementation - [Binary Format](binary-format/01-overview.md) — Compiled query format ## Document Map @@ -26,13 +27,16 @@ docs/ ├── lang-reference.md # Query language syntax and semantics ├── type-system.md # Type inference rules and output shapes ├── runtime-engine.md # VM state, backtracking, effects +├── tree-navigation.md # Cursor walk, search loop, anchor lowering └── binary-format/ # Compiled bytecode specification ├── 01-overview.md # Header, sections, alignment ├── 02-strings.md # String pool and table ├── 03-symbols.md # Node types, fields, trivia ├── 04-types.md # Type metadata format ├── 05-entrypoints.md # Definition table - └── 06-transitions.md # VM instructions and data blocks + ├── 06-transitions.md # VM instructions and data blocks + ├── 07-dump-format.md # Bytecode dump output format + └── 08-trace-format.md # Execution trace output format ``` ## Reading Order @@ -47,6 +51,9 @@ Building tooling: 1. `binary-format/01-overview.md` → through `06-transitions.md` 2. `runtime-engine.md` +3. `tree-navigation.md` +4. `binary-format/07-dump-format.md` — Understanding bytecode dumps +5. `binary-format/08-trace-format.md` — Debugging with execution traces Contributing: diff --git a/docs/binary-format/07-dump-format.md b/docs/binary-format/07-dump-format.md index f7d4b97e..7bafb3ee 100644 --- a/docs/binary-format/07-dump-format.md +++ b/docs/binary-format/07-dump-format.md @@ -15,8 +15,9 @@ Assignment = (assignment_expression ## Bytecode Dump -**Epsilon transitions** (`𝜀`) succeed unconditionally without cursor interaction. +**Epsilon transitions** (`ε`) succeed unconditionally without cursor interaction. They require all three conditions: + - `nav == Stay` (no cursor movement) - `node_type == None` (no type constraint) - `node_field == None` (no field constraint) @@ -30,7 +31,7 @@ effects (`Obj`, `EndObj`, `Arr`, `EndArr`, `Enum`, `EndEnum`) remain in epsilons ``` [flags] -linked = true +linked = false [strings] S00 "Beauty will save the world" @@ -45,65 +46,72 @@ S08 "Assignment" S09 "identifier" S10 "number" S11 "assignment_expression" -S12 "left" -S13 "right" +S12 "right" +S13 "left" [type_defs] -T00 = void -T01 = Node -T02 = str -T03 = Struct M0[1] ; { name } -T04 = Struct M1[1] ; { value } -T05 = Struct M2[1] ; { name } -T06 = Enum M3[2] ; Literal | Variable -T07 = Struct M5[2] ; { value, target } +T00 = +T01 = +T02 = Struct M0:1 ; { name } +T03 = Struct M1:1 ; { value } +T04 = Struct M2:1 ; { name } +T05 = Enum M3:2 ; Literal | Variable +T06 = Struct M5:2 ; { value, target } +T07 = Struct M7:1 ; { target } +T08 = Struct M8:1 ; { value } [type_members] -M0: S01 → T02 ; name: str -M1: S02 → T01 ; value: Node -M2: S01 → T01 ; name: Node -M3: S03 → T04 ; Literal: T04 -M4: S04 → T05 ; Variable: T05 -M5: S02 → T06 ; value: Expression -M6: S05 → T01 ; target: Node +M0: S01 → T01 ; name: +M1: S02 → T00 ; value: +M2: S01 → T00 ; name: +M3: S03 → T03 ; Literal: T03 +M4: S04 → T04 ; Variable: T04 +M5: S02 → T05 ; value: Expression +M6: S05 → T00 ; target: +M7: S05 → T00 ; target: +M8: S02 → T05 ; value: Expression [type_names] -N0: S06 → T03 ; Ident -N1: S07 → T06 ; Expression -N2: S08 → T07 ; Assignment +N0: S06 → T02 ; Ident +N1: S07 → T05 ; Expression +N2: S08 → T06 ; Assignment [entrypoints] -Assignment = 08 :: T07 -Expression = 05 :: T06 -Ident = 01 :: T03 +Assignment = 12 :: T06 +Expression = 09 :: T05 +Ident = 01 :: T02 [transitions] - 00 𝜀 ◼ + 00 ε ◼ Ident: - 01 𝜀 02 - 02 (identifier) [Text Set(M0)] 04 - 04 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (identifier) [Text Set(M0)] 06 + 06 ε [EndObj] 08 + 08 ▶ Expression: - 05 𝜀 06 - 06 𝜀 22, 28 + 09 ε 10 + 10 ε 30, 36 Assignment: - 08 𝜀 09 - 09 (assignment_expression) 10 - 10 ↓* left: (identifier) [Node Set(M6)] 12 - 12 * ▶ right: (Expression) 13 - 13 𝜀 [Set(M5)] 15 - 15 *↑¹ 16 - 16 ▶ - 17 ▶ - 18 𝜀 [EndEnum] 17 - 20 (number) [Node Set(M1)] 18 - 22 𝜀 [Enum(M3)] 20 - 24 𝜀 [EndEnum] 17 - 26 (identifier) [Node Set(M2)] 24 - 28 𝜀 [Enum(M4)] 26 + 12 ε 13 + 13 ε [Obj] 15 + 15 (assignment_expression) 16 + 16 ▽ left: (identifier) [Node Set(M6)] 18 + 18 ▷ right: (Expression) 19 ⯇ + 19 ε [Set(M5)] 21 + 21 △ 22 + 22 ε [EndObj] 24 + 24 ▶ + 25 ▶ + 26 ε [EndEnum] 25 + 28 (number) [Node Set(M1)] 26 + 30 ε [Enum(M3)] 28 + 32 ε [EndEnum] 25 + 34 (identifier) [Node Set(M2)] 32 + 36 ε [Enum(M4)] 34 ``` ## Files @@ -122,26 +130,54 @@ Future: options for verbosity levels, hiding sections, etc. ## Instruction Format -Each line follows the column layout: `