diff --git a/Cargo.lock b/Cargo.lock index e54cb55e4..643f4033f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2703,9 +2703,11 @@ dependencies = [ "glob", "html-escape", "ignore", + "indexmap", "itertools 0.15.0", "md5", "miette", + "mimalloc", "mq-macros", "mq-markdown", "nom 8.0.0", diff --git a/Cargo.toml b/Cargo.toml index fc784cdd1..095a2ddce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,6 +72,7 @@ flate2 = "1.1" futures = "0.3" glob = "0.3.3" httpmock = "0.8.2" +indexmap = "2.14.0" itertools = "0.15.0" js-sys = "0.3.77" libc = "0.2" diff --git a/crates/mq-check/src/builtin.rs b/crates/mq-check/src/builtin.rs index f1db1e646..95572176b 100644 --- a/crates/mq-check/src/builtin.rs +++ b/crates/mq-check/src/builtin.rs @@ -1341,10 +1341,6 @@ fn register_markdown(ctx: &mut InferenceContext) { /// Variable/symbol management functions fn register_variable(ctx: &mut InferenceContext) { register_nullary(ctx, "all_symbols", Type::array(Type::Symbol)); - // Deprecated: tree-walker only (see `mq-lang`'s `runtime::builtin`), scheduled for - // removal in the next release. - register_unary(ctx, "get_variable", Type::String, Type::String); - register_binary(ctx, "set_variable", Type::String, Type::String, Type::None); register_unary(ctx, "intern", Type::String, Type::Symbol); } @@ -2390,8 +2386,6 @@ mod tests { #[case::error_func("error(\"message\")", true)] #[case::halt_func("halt(1)", true)] #[case::all_symbols("all_symbols()", true)] - #[case::get_variable("get_variable(\"key\")", true)] - #[case::set_variable("set_variable(\"key\", \"value\")", true)] #[case::intern("intern(\"symbol\")", true)] #[case::is_debug_mode("is_debug_mode()", true)] #[case::breakpoint("breakpoint()", true)] diff --git a/crates/mq-crawler/Cargo.toml b/crates/mq-crawler/Cargo.toml index a230080fa..67eca848e 100644 --- a/crates/mq-crawler/Cargo.toml +++ b/crates/mq-crawler/Cargo.toml @@ -11,9 +11,6 @@ readme = "README.md" repository.workspace = true version.workspace = true -[features] -tarn = ["mq-lang/tarn"] - [dependencies] base64 = {workspace = true} chromiumoxide = {workspace = true} diff --git a/crates/mq-dap/Cargo.toml b/crates/mq-dap/Cargo.toml index 3a1d00d76..8d68d7f37 100644 --- a/crates/mq-dap/Cargo.toml +++ b/crates/mq-dap/Cargo.toml @@ -11,9 +11,6 @@ readme = "README.md" repository.workspace = true version.workspace = true -[features] -tarn = ["mq-lang/tarn"] - [dependencies] crossbeam-channel = {workspace = true} dap = {workspace = true} diff --git a/crates/mq-dap/src/adapter.rs b/crates/mq-dap/src/adapter.rs index 7402030f0..a7f41bc99 100644 --- a/crates/mq-dap/src/adapter.rs +++ b/crates/mq-dap/src/adapter.rs @@ -299,14 +299,9 @@ impl MqAdapter { ))) as Box); }; - #[cfg(feature = "tarn")] let result = self .engine .eval_debug_expression(code, context.current_value.clone(), &context.vm_bindings()); - #[cfg(not(feature = "tarn"))] - let result = self - .engine - .eval_debug_expression(code, context.current_value.clone(), &context.env); result.map_err(|e| { let error_msg = format!("Evaluation error: {}", e); @@ -315,7 +310,6 @@ impl MqAdapter { }) } - #[cfg(feature = "tarn")] fn eval_single_value(&mut self, code: &str) -> DynResult { self.eval(code)?.values().first().cloned().ok_or_else(|| { Box::new(MqAdapterError::EvaluationError(Cow::Borrowed( @@ -522,90 +516,55 @@ impl MqAdapter { } Command::SetVariable(args) => { debug!(?args, "Received SetVariables request"); - #[cfg(feature = "tarn")] - { - let name = args.name.clone(); - let response_value = args.value.clone(); - let value = self.eval_single_value(&response_value)?; - let Some(context) = self.current_debug_context.as_ref() else { - return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed( - "Current context not found", - )))); - }; - let prefer_upvalue = args.variables_reference == 1; - if !context.set_vm_variable(&name, value, prefer_upvalue) { - return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!( - "Variable `{}` is not visible in this VM scope", - name - ))))); - } - let rsp = req.success(ResponseBody::SetVariable(SetVariableResponse { - value: response_value, - indexed_variables: None, - named_variables: None, - type_field: None, - variables_reference: None, - })); - server.respond(rsp)?; - } - #[cfg(not(feature = "tarn"))] - { - self.eval(format!("let {} = {}", args.name, args.value).as_str())?; - - let value = args.value.clone(); - let rsp = req.success(ResponseBody::SetVariable(SetVariableResponse { - value, - indexed_variables: None, - named_variables: None, - type_field: None, - variables_reference: None, - })); - server.respond(rsp)?; + let name = args.name.clone(); + let response_value = args.value.clone(); + let value = self.eval_single_value(&response_value)?; + let Some(context) = self.current_debug_context.as_ref() else { + return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed( + "Current context not found", + )))); + }; + let prefer_upvalue = args.variables_reference == 1; + if !context.set_vm_variable(&name, value, prefer_upvalue) { + return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!( + "Variable `{}` is not visible in this VM scope", + name + ))))); } + let rsp = req.success(ResponseBody::SetVariable(SetVariableResponse { + value: response_value, + indexed_variables: None, + named_variables: None, + type_field: None, + variables_reference: None, + })); + server.respond(rsp)?; } Command::SetExpression(args) => { debug!(?args, "Received SetExpression request"); - #[cfg(feature = "tarn")] - { - let expression = args.expression.clone(); - let response_value = args.value.clone(); - let value = self.eval_single_value(&response_value)?; - let Some(context) = self.current_debug_context.as_ref() else { - return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed( - "Current context not found", - )))); - }; - if !context.set_vm_expression(&expression, value) { - return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!( - "Expression `{}` is not a visible VM variable", - expression - ))))); - } - let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse { - value: response_value, - type_field: None, - presentation_hint: None, - variables_reference: None, - named_variables: None, - indexed_variables: None, - })); - server.respond(rsp)?; - } - #[cfg(not(feature = "tarn"))] - { - self.eval(format!("let {} = {}", args.expression, args.value).as_str())?; - - let value = args.value.clone(); - let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse { - value, - type_field: None, - presentation_hint: None, - variables_reference: None, - named_variables: None, - indexed_variables: None, - })); - server.respond(rsp)?; + let expression = args.expression.clone(); + let response_value = args.value.clone(); + let value = self.eval_single_value(&response_value)?; + let Some(context) = self.current_debug_context.as_ref() else { + return Err(Box::new(MqAdapterError::EvaluationError(Cow::Borrowed( + "Current context not found", + )))); + }; + if !context.set_vm_expression(&expression, value) { + return Err(Box::new(MqAdapterError::EvaluationError(Cow::Owned(format!( + "Expression `{}` is not a visible VM variable", + expression + ))))); } + let rsp = req.success(ResponseBody::SetExpression(SetExpressionResponse { + value: response_value, + type_field: None, + presentation_hint: None, + variables_reference: None, + named_variables: None, + indexed_variables: None, + })); + server.respond(rsp)?; } Command::Continue(_) => { debug!("Received Continue request"); @@ -736,7 +695,6 @@ mod tests { use dap::server::Server; use mq_lang::Shared; use std::io::{BufReader, BufWriter, Cursor}; - #[cfg(feature = "tarn")] use std::time::Duration; #[test] @@ -1115,27 +1073,10 @@ mod tests { assert!(result.is_ok()); } - #[cfg(not(feature = "tarn"))] - #[test] - fn test_eval_resolves_debug_context_bindings() { - let mut adapter = MqAdapter::new(); - let context = mq_lang::DebugContext::default(); - context - .env - .write() - .unwrap() - .define("x".into(), mq_lang::RuntimeValue::Number(41.into())); - adapter.current_debug_context = Some(context); - - let result = adapter.eval("x + 1").unwrap(); - assert_eq!(result[0], mq_lang::RuntimeValue::Number(42.into())); - } - /// A real stopped VM frame must expose its bindings to DAP variable and evaluate requests. /// /// This runs through the configured `DapHandlerWrapper`, rather than constructing a /// `DebugContext` by hand, so it protects the VM debugger boundary → DAP adapter path. - /// The same test also runs without the `tarn` feature as the tree-walker reference. #[test] fn test_stopped_frame_exposes_live_bindings_to_dap() { let mut adapter = MqAdapter::new(); @@ -1195,7 +1136,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[rstest::rstest] #[case::top_level_global_set_variable(("let x = 1 |\nx + 1", 2, 1, "x", "1", "41", false, 42))] #[case::top_level_set_expression(("let x = 1 |\nx + 1", 2, 1, "x", "1", "41", true, 42))] @@ -1651,7 +1591,7 @@ mod tests { let mut context = mq_lang::DebugContext::default(); context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: Shared::new(mq_lang::AstExpr::Literal(mq_lang::AstLiteral::Number(42.into()))), + expr: mq_lang::AstExpr::Literal(mq_lang::AstLiteral::Number(42.into())), token_id: 0u32.into(), })); adapter.current_debug_context = Some(context); diff --git a/crates/mq-ffi/src/lib.rs b/crates/mq-ffi/src/lib.rs index 22a0904cd..f4b9a928b 100644 --- a/crates/mq-ffi/src/lib.rs +++ b/crates/mq-ffi/src/lib.rs @@ -1339,7 +1339,9 @@ mod tests { let engine = mq_create(); mq_set_max_call_stack_depth(engine, 2); - let code = make_c_string("def rec(): rec(); rec()"); + // The recursive call must not be in tail position: tail calls deliberately reuse a + // frame, while this test verifies enforcement of the configured stack-depth limit. + let code = make_c_string("def rec(): 1 + rec(); | rec()"); let input = make_c_string("test"); let format = make_c_string("text"); let result = unsafe { mq_eval(engine, code, input, format) }; diff --git a/crates/mq-ffi/test_mq.c b/crates/mq-ffi/test_mq.c index 549e83291..026388454 100644 --- a/crates/mq-ffi/test_mq.c +++ b/crates/mq-ffi/test_mq.c @@ -288,7 +288,7 @@ void test_set_max_call_stack_depth() { mq_context_t *engine = mq_create(); mq_set_max_call_stack_depth(engine, 2); - struct mq_result_t result = mq_eval(engine, "def rec(): rec(); rec()", "test", "text"); + struct mq_result_t result = mq_eval(engine, "def rec(): 1 + rec(); rec()", "test", "text"); assert_not_null(result.error_msg, "Should have error due to stack depth limit"); mq_free_result(result); diff --git a/crates/mq-lang/Cargo.toml b/crates/mq-lang/Cargo.toml index 8c6718d76..85a842c38 100644 --- a/crates/mq-lang/Cargo.toml +++ b/crates/mq-lang/Cargo.toml @@ -25,6 +25,7 @@ chrono = {workspace = true} dirs = {workspace = true} glob = {workspace = true} ignore = {workspace = true, optional = true} +indexmap = {workspace = true} itertools = {workspace = true} miette = {workspace = true} mq-markdown = {workspace = true, features = ["json", "obsidian"]} @@ -66,17 +67,14 @@ web-time = {workspace = true} [features] ast-json = ["smallvec/serde", "smol_str/serde"] -# Routes `Engine::eval`/`eval_compiled` through Tarn, the bytecode VM (`src/tarn.rs`), -# instead of the tree-walking evaluator (`src/eval.rs`). Opt-in for now while Tarn is -# validated against real usage; the tree-walker stays the default until it's dropped. -tarn = [] # Captures VM operand-stack snapshots at debugger statement boundaries. Intended for -# `mq-dbg` diagnosis; disabled in regular Tarn builds to avoid snapshot allocations. -debug-trace = ["tarn"] +# `mq-dbg` diagnosis; disabled by default to avoid snapshot allocations. +debug-trace = [] +# Enables execution-count profiling for Tarn bytecode instructions. This is intended for +# development diagnostics and is deliberately excluded from normal builds. +vm-profile = [] cst = ["dep:ropey"] css-selector = ["dep:ego-tree", "dep:scraper"] -# The tree-walker debugger is independent of Tarn. VM operand-stack tracing remains opt-in -# through `debug-trace`, which enables Tarn for debugger frontends that need it. debugger = ["sync"] default = ["std", "html-to-markdown"] file-io = ["dep:ignore"] @@ -92,6 +90,7 @@ tiktoken = ["dep:tiktoken-rs"] [dev-dependencies] divan = {workspace = true} +mimalloc = {workspace = true, features = ["v3"]} proptest = {workspace = true} rstest = {workspace = true} scopeguard = {workspace = true} diff --git a/crates/mq-lang/benches/benchmark.rs b/crates/mq-lang/benches/benchmark.rs index 35995cf8a..19976667b 100644 --- a/crates/mq-lang/benches/benchmark.rs +++ b/crates/mq-lang/benches/benchmark.rs @@ -1,6 +1,17 @@ +//! Regression benchmarks for the parser and compiled Tarn VM. +//! +//! Each benchmark protects a distinct execution path. Exploratory, cold-start, and overlapping +//! microbenchmarks belong in ad-hoc profiling rather than this always-run suite. + use mq_lang::{Shared, SharedCell}; use std::sync::LazyLock; +// Keep allocator behavior consistent with the `mq` CLI. This is deliberately +// defined in the benchmark binary so library consumers retain control of their +// allocator choice. +#[global_allocator] +static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; + fn main() { divan::main(); } @@ -15,22 +26,6 @@ where bencher.bench_local(|| engine.eval_compiled(&compiled, input().into_iter()).unwrap()); } -#[divan::bench()] -fn eval_fibonacci() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - " - def fibonacci(x): - if (x < 2): - x - else: - fibonacci(x - 1) + fibonacci(x - 2); | fibonacci(20)", - vec![mq_lang::RuntimeValue::Number(20.into())].into_iter(), - ) - .unwrap() -} - #[divan::bench] fn eval_compiled_fibonacci(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); @@ -47,95 +42,19 @@ fn eval_compiled_fibonacci(bencher: divan::Bencher) { ); } -#[divan::bench()] -fn eval_while_speed_test() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - "var i = 10000 | while(i > 0): i -= 1; | i", - vec![mq_lang::RuntimeValue::Number(1.into())].into_iter(), - ) - .unwrap() -} - -#[divan::bench(name = "eval_select_h")] -fn eval_select_h() -> mq_lang::RuntimeValues { - let markdown: mq_markdown::Markdown = - mq_markdown::Markdown::from_markdown_str("# heading\n- item1\n- item2\n## heading2\n- item1\n- item2\n") - .unwrap(); - let input = markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from); - let mut engine = mq_lang::DefaultEngine::default(); - engine.eval(".h1", input.into_iter()).unwrap() -} - -#[divan::bench(name = "eval_string_interpolation")] -fn eval_string_interpolation() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - r#"let world = "world" | s"$$Hello, ${world}$$""#, // Semicolon is correct here before pipe - vec!["".into()].into_iter(), - ) - .unwrap() -} - -/// Isolates steady-state execution for a tiny, non-looping query — where compile overhead -/// (shared with the tree-walker) otherwise swamps the signal in `eval_select_h`. -#[divan::bench] -fn eval_compiled_select_h(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - let input: Vec = - mq_markdown::Markdown::from_markdown_str("# heading\n- item1\n- item2\n## heading2\n- item1\n- item2\n") - .unwrap() - .nodes - .into_iter() - .map(mq_lang::RuntimeValue::from) - .collect(); - bench_compiled(bencher, &mut engine, ".h1", || input.clone()); -} - -/// Measures a fresh Markdown input, as supplied by line-oriented callers. The node is uniquely -/// owned at VM entry, so this catches unnecessary whole-tree clones during tree walking. #[divan::bench] -fn eval_compiled_owned_markdown_tree(bencher: divan::Bencher) { - fn input() -> mq_lang::RuntimeValue { - mq_lang::RuntimeValue::new_markdown(mq_markdown::Node::Fragment(mq_markdown::Fragment { - values: (0..1_000) - .map(|index| { - mq_markdown::Node::Text(mq_markdown::Text { - value: index.to_string(), - position: None, - }) - }) - .collect(), - })) - } - +fn eval_compiled_while(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); - let compiled = engine.compile(".").unwrap(); - engine.eval_compiled(&compiled, std::iter::once(input())).unwrap(); - - bencher.bench_local(|| engine.eval_compiled(&compiled, std::iter::once(input())).unwrap()); + bench_compiled( + bencher, + &mut engine, + "var i = 10000 | while(i > 0): i -= 1; | i", + || vec![mq_lang::RuntimeValue::Number(1.into())], + ); } /// Measures the API pattern used by line-oriented callers: one compiled query evaluated once -/// per input value, rather than one call over a batch of inputs. -#[divan::bench] -fn eval_compiled_reused_single_input(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - let compiled = engine.compile(". * 10").unwrap(); - engine - .eval_compiled(&compiled, std::iter::once(mq_lang::RuntimeValue::Number(1.into()))) - .unwrap(); - - bencher.bench_local(|| { - engine - .eval_compiled(&compiled, std::iter::once(mq_lang::RuntimeValue::Number(1.into()))) - .unwrap() - }); -} - -/// Mirrors line-oriented CLI calls whose per-file globals remain unchanged across rows. +/// per input value while file globals remain stable. #[divan::bench] fn eval_compiled_reused_single_input_with_globals(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); @@ -154,334 +73,6 @@ fn eval_compiled_reused_single_input_with_globals(bencher: divan::Bencher) { }); } -/// Covers a repeated query that includes an external module. Cached Tarn bytecode freezes the -/// module source for its Engine, so this measures the cache-hit path without a file read/hash. -#[cfg(feature = "tarn")] -#[divan::bench] -fn eval_compiled_reused_single_input_with_external_module(bencher: divan::Bencher) { - let directory = tempfile::tempdir().unwrap(); - std::fs::write(directory.path().join("constant.mq"), "def constant(): 42;").unwrap(); - - let mut engine = mq_lang::DefaultEngine::default(); - engine.set_search_paths(vec![directory.path().to_owned()]); - let compiled = engine.compile(r#"include "constant" | constant()"#).unwrap(); - engine - .eval_compiled(&compiled, std::iter::once(mq_lang::RuntimeValue::None)) - .unwrap(); - - bencher.bench_local(|| { - engine - .eval_compiled(&compiled, std::iter::once(mq_lang::RuntimeValue::None)) - .unwrap() - }); -} - -/// See `eval_compiled_select_h`. -#[divan::bench] -fn eval_compiled_string_interpolation(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - bench_compiled( - bencher, - &mut engine, - r#"let world = "world" | s"$$Hello, ${world}$$""#, - || vec!["".into()], - ); -} - -#[divan::bench(name = "eval_nodes")] -fn eval_nodes() -> mq_lang::RuntimeValues { - let markdown: mq_markdown::Markdown = - mq_markdown::Markdown::from_markdown_str("# heading\n- item1\n- item2\n## heading2\n- item1\n- item2\n") - .unwrap(); - let input = markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from); - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine.eval(".h | nodes | map(upcase)", input.into_iter()).unwrap() -} - -#[divan::bench] -fn eval_compiled_nodes(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - bench_compiled(bencher, &mut engine, ".h | nodes | map(upcase)", || { - mq_markdown::Markdown::from_markdown_str("# heading\n- item1\n- item2\n## heading2\n- item1\n- item2\n") - .unwrap() - .nodes - .into_iter() - .map(mq_lang::RuntimeValue::from) - .collect() - }); -} - -#[divan::bench] -fn parse_fibonacci() -> Vec> { - let token_arena = Shared::new(SharedCell::new(mq_lang::Arena::new(100))); - mq_lang::parse( - " - def fibonacci(x): - if (x == 0): - 0 - elif (x == 1): - 1 - else: - fibonacci(sub(x, 1)) + fibonacci(sub(x, 2)); | fibonacci(20)", - Shared::clone(&token_arena), - ) - .unwrap() -} - -/// Exercises byte-string lexing without charging construction of the input to the parser. -#[divan::bench] -fn parse_large_byte_string() -> Vec> { - static CODE: LazyLock = LazyLock::new(|| format!(r#"b"{}""#, "a".repeat(16 * 1024))); - let token_arena = Shared::new(SharedCell::new(mq_lang::Arena::new(4))); - mq_lang::parse(&CODE, token_arena).unwrap() -} - -/// Exercises flattening of a long logical-expression chain during AST construction. -#[divan::bench] -fn parse_long_and_chain() -> Vec> { - static CODE: LazyLock = - LazyLock::new(|| std::iter::repeat_n("true", 4_096).collect::>().join(" && ")); - let token_arena = Shared::new(SharedCell::new(mq_lang::Arena::new(8_192))); - mq_lang::parse(&CODE, token_arena).unwrap() -} - -#[divan::bench(name = "eval_foreach")] -fn eval_foreach() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval(r#"foreach(x, range(0, 1000, 1)): x + 1;"#, vec!["".into()].into_iter()) - .unwrap() -} - -#[divan::bench] -fn eval_compiled_foreach(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - bench_compiled(bencher, &mut engine, r#"foreach(x, range(0, 1000, 1)): x + 1;"#, || { - vec![mq_lang::RuntimeValue::String(Shared::new(String::new()))] - }); -} - -const CSV_PARSE_INPUT: &str = - "a,b,c\n\"1,2\",\"2,3\",\"3,4\"\n4,5,6\n\"multi\nline\",7,8\n9,10,\"quoted,comma\"\n\"\",11,12\n13,14,15\n"; - -/// Includes and compiles the CSV module on every invocation, representing one-shot use. -#[divan::bench()] -fn eval_csv_parse() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"include "csv" | csv_parse(true)"#, - vec![mq_lang::RuntimeValue::String(Shared::new(CSV_PARSE_INPUT.to_string()))].into_iter(), - ) - .unwrap() -} - -/// Reuses the optimized query/module bytecode after one warm-up evaluation. -#[divan::bench] -fn eval_compiled_csv_parse(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine.load_module("csv").unwrap(); - bench_compiled(bencher, &mut engine, "csv_parse(true)", || { - vec![mq_lang::RuntimeValue::String(Shared::new(CSV_PARSE_INPUT.to_string()))] - }); -} - -#[divan::bench()] -fn eval_yaml_parse() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"include "yaml" | yaml_parse()"#, - vec![mq_lang::RuntimeValue::String(Shared::new("---\nstring: hello\nnumber: 42\nfloat: 3.14\nbool_true: true\nbool_false: false\nnull_value: null\narray:\n - item1\n - item2\n - item3\nobject:\n key1: value1\n key2: value2\nnested:\n arr:\n - a\n - b\n obj:\n subkey: subval\nmultiline: |\n This is a\n multiline string\nquoted: \"quoted string\"\nsingle_quoted: 'single quoted string'\ndate: 2024-06-01\ntimestamp: 2024-06-01T12:34:56Z\nempty_array: []\nempty_object: {}\nanchors:\n &anchor_val anchored value\nref: *anchor_val\ncomplex:\n - foo: bar\n baz:\n - qux\n - quux\n - corge: grault\nspecial_chars: \"!@#$%^&*()_+-=[]{}|;:',.<>/?\"\nunicode: \"こんにちは世界\"\nbool_list:\n - true\n - false\nnull_list:\n - null\n - ~".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_json_parse() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"include "json" | json_parse()"#, - vec![mq_lang::RuntimeValue::String(Shared::new("{\"users\":[{\"id\":1,\"name\":\"Alice\",\"email\":\"alice@example.com\",\"roles\":[\"admin\",\"user\"]},{\"id\":2,\"name\":\"Bob\",\"email\":\"bob@example.com\",\"roles\":[\"user\"]},{\"id\":3,\"name\":\"Charlie\",\"email\":\"charlie@example.com\",\"roles\":[\"editor\",\"user\"]}],\"meta\":{\"count\":3,\"generated_at\":\"2024-06-01T12:00:00Z\"}}".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_qualified_access_to_csv_module() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"import "csv" | csv::csv_parse(true)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("a,b,c\n\"1,2\",\"2,3\",\"3,4\"\n4,5,6\n\"multi\nline\",7,8\n9,10,\"quoted,comma\"\n\"\",11,12\n13,14,15\n".to_string()))].into_iter(), - ) - .unwrap() -} - -fn section_markdown_input() -> impl Iterator { - let markdown_content = (0..30) - .map(|i| format!("# Section {i}\n\nIntro paragraph for section {i}.\n\n## Subsection {i}\n\nSome detail text.\n\n- point a\n- point b\n\n")) - .collect::(); - let markdown: mq_markdown::Markdown = mq_markdown::Markdown::from_markdown_str(&markdown_content).unwrap(); - markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from) -} - -#[divan::bench()] -fn eval_section_sections() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"nodes | import "section" | section::sections() | len()"#, - section_markdown_input(), - ) - .unwrap() -} - -#[divan::bench] -fn eval_compiled_section_sections(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine.load_module("section").unwrap(); - bench_compiled(bencher, &mut engine, "nodes | sections() | len()", || { - section_markdown_input().collect() - }); -} - -fn table_markdown_input() -> impl Iterator { - let header = "| Name | Age | City |\n| --- | --- | --- |\n"; - let rows = (0..30) - .map(|i| format!("| Person {i} | {} | City {i} |\n", 20 + i % 50)) - .collect::(); - let markdown_content = format!("{header}{rows}"); - let markdown: mq_markdown::Markdown = mq_markdown::Markdown::from_markdown_str(&markdown_content).unwrap(); - markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from) -} - -#[divan::bench()] -fn eval_table_tables() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval(r#"nodes | import "table" | table::tables()"#, table_markdown_input()) - .unwrap() -} - -#[divan::bench] -fn eval_compiled_table_tables(bencher: divan::Bencher) { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine.load_module("table").unwrap(); - bench_compiled(bencher, &mut engine, "nodes | tables()", || { - table_markdown_input().collect() - }); -} - -#[divan::bench()] -fn eval_string_equality() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#" -let a1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1" -| let a2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa2" -| let a3 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa3" -| let a4 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4" -| let a5 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa5" -| let a6 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa6" -| let a7 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa7" -| let a8 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa8" -| a1 == a1 | a1 == a2 | a1 == a3 | a1 == a4 | a1 == a5 | a1 == a6 | a1 == a7 | a1 == a8 -| a2 == a1 | a2 == a2 | a2 == a3 | a2 == a4 | a2 == a5 | a2 == a6 | a2 == a7 | a2 == a8 -| a3 == a1 | a3 == a2 | a3 == a3 | a3 == a4 | a3 == a5 | a3 == a6 | a3 == a7 | a3 == a8 -| a4 == a1 | a4 == a2 | a4 == a3 | a4 == a4 | a4 == a5 | a4 == a6 | a4 == a7 | a4 == a8 -| a5 == a1 | a5 == a2 | a5 == a3 | a5 == a4 | a5 == a5 | a5 == a6 | a5 == a7 | a5 == a8 -| a6 == a1 | a6 == a2 | a6 == a3 | a6 == a4 | a6 == a5 | a6 == a6 | a6 == a7 | a6 == a8 -| a7 == a1 | a7 == a2 | a7 == a3 | a7 == a4 | a7 == a5 | a7 == a6 | a7 == a7 | a7 == a8 -| a8 == a1 | a8 == a2 | a8 == a3 | a8 == a4 | a8 == a5 | a8 == a6 | a8 == a7 | a8 == a8 -"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_large_program() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - r#" -let a = 1 | let b = 2 | let c = 3 | let d = 4 | let e = 5 -| let f = 6 | let g = 7 | let h = 8 | let i = 9 | let j = 10 -| let k = 11 | let l = 12 | let m = 13 | let n = 14 | let o = 15 -| a + b + c + d + e + f + g + h + i + j + k + l + m + n + o -"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -// Array/Collection Operations Benchmarks - -#[divan::bench()] -fn eval_array_map() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"range(0, 1000, 1) | map(fn(x): x * 2;)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_array_filter() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"range(0, 1000, 1) | filter(fn(x): x % 2 == 0;)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_array_fold() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"def sum(acc, x): add(acc, x); | fold(range(0, 100, 1), 0, sum)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_array_chained_operations() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"range(0, 500, 1) | filter(fn(x): x % 2 == 0;) | map(fn(x): x * 3;) | filter(fn(x): x > 100;)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - #[divan::bench] fn eval_compiled_array_map(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); @@ -530,52 +121,6 @@ fn eval_compiled_array_chained_operations(bencher: divan::Bencher) { ); } -// Object/Hash Access Benchmarks - -#[divan::bench()] -fn eval_object_field_access() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"let obj = dict() - | let obj = set(obj, "a", 1) | let obj = set(obj, "b", 2) | let obj = set(obj, "c", 3) - | let obj = set(obj, "d", 4) | let obj = set(obj, "e", 5) - | foreach(i, range(0, 100, 1)): add(add(add(add(get(obj, "a"), get(obj, "b")), get(obj, "c")), get(obj, "d")), get(obj, "e"));"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -#[divan::bench()] -fn eval_nested_object_access() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"let inner = dict() | let inner = set(inner, "value", 42) - | let middle = dict() | let middle = set(middle, "inner", inner) - | let outer = dict() | let outer = set(outer, "middle", middle) - | let obj = dict() | let obj = set(obj, "outer", outer) - | foreach(i, range(0, 100, 1)): get(get(get(get(obj, "outer"), "middle"), "inner"), "value");"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - -// Function Call Overhead Benchmarks - -#[divan::bench()] -fn eval_function_call_overhead() -> mq_lang::RuntimeValues { - let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - r#"def identity(x): x; | foreach(i, range(0, 1000, 1)): identity(i);"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() -} - /// Isolates repeated non-capturing user-function calls after bytecode compilation. #[divan::bench] fn eval_compiled_function_call_overhead(bencher: divan::Bencher) { @@ -588,16 +133,29 @@ fn eval_compiled_function_call_overhead(bencher: divan::Bencher) { ); } -#[divan::bench()] -fn eval_nested_function_calls() -> mq_lang::RuntimeValues { +/// Measures calls through a local holding a native function, which use the VM's generic call +/// path instead of the fixed-arity closure fast path. +#[divan::bench] +fn eval_compiled_dynamic_builtin_call(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - r#"def add1(x): x + 1; | def add2(x): add1(add1(x)); | def add4(x): add2(add2(x)); - | foreach(i, range(0, 100, 1)): add4(i);"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() + bench_compiled( + bencher, + &mut engine, + r#"let transform = upcase | foreach(i, range(0, 1000, 1)): transform("value");"#, + || vec![mq_lang::RuntimeValue::String(Shared::new(String::new()))], + ); +} + +/// Tracks the direct `CallBuiltin` path for the one- and two-argument forms used in tight loops. +#[divan::bench] +fn eval_compiled_direct_builtin_calls(bencher: divan::Bencher) { + let mut engine = mq_lang::DefaultEngine::default(); + bench_compiled( + bencher, + &mut engine, + r#"foreach(i, range(0, 1000, 1)): contains(upcase("value"), "A");"#, + || vec![mq_lang::RuntimeValue::String(Shared::new(String::new()))], + ); } /// Isolates nested call-frame setup and teardown after bytecode compilation. @@ -613,128 +171,161 @@ fn eval_compiled_nested_function_calls(bencher: divan::Bencher) { ); } -// Pipeline Processing Benchmarks - -#[divan::bench()] -fn eval_long_pipeline() -> mq_lang::RuntimeValues { +/// Isolates repeated `get()` lookups on a small dict, the common `set()`/`get()` shape. +#[divan::bench] +fn eval_compiled_dict_field_access(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine - .eval( - r#"range(0, 100, 1) - | map(fn(x): x + 1;) - | map(fn(x): x * 2;) - | map(fn(x): x - 3;) - | map(fn(x): x + 4;) - | map(fn(x): x * 5;) - | map(fn(x): x - 6;) - | map(fn(x): x + 7;) - | map(fn(x): x * 8;)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() + bench_compiled( + bencher, + &mut engine, + r#"let obj = dict() + | let obj = set(obj, "a", 1) | let obj = set(obj, "b", 2) | let obj = set(obj, "c", 3) + | let obj = set(obj, "d", 4) | let obj = set(obj, "e", 5) + | foreach(i, range(0, 1000, 1)): add(add(add(add(get(obj, "a"), get(obj, "b")), get(obj, "c")), get(obj, "d")), get(obj, "e"));"#, + || vec![mq_lang::RuntimeValue::String(Shared::new(String::new()))], + ); } -#[divan::bench()] -fn eval_pipeline_with_conditionals() -> mq_lang::RuntimeValues { +#[divan::bench] +fn eval_compiled_large_dict_field_access(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); engine.load_builtin_module(); - engine - .eval( - r#"range(0, 100, 1) - | map(fn(x): if (x % 2 == 0): x * 2 else: x + 1;) - | filter(fn(x): x > 50;) - | map(fn(x): if (x % 3 == 0): x / 3 else: x;)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() + bench_compiled( + bencher, + &mut engine, + r#"let d = fold(range(0, 100, 1), dict(), fn(acc, i): set(acc, to_string(i), i);) + | foreach(i, range(0, 2000, 1)): get(d, to_string(i % 100));"#, + || vec![mq_lang::RuntimeValue::String(Shared::new(String::new()))], + ); } -// Real-World Markdown Processing Benchmarks +fn owned_markdown_tree() -> mq_lang::RuntimeValue { + mq_lang::RuntimeValue::new_markdown(mq_markdown::Node::Fragment(mq_markdown::Fragment { + values: (0..1_000) + .map(|index| { + mq_markdown::Node::Text(mq_markdown::Text { + value: index.to_string(), + position: None, + }) + }) + .collect(), + })) +} -#[divan::bench()] -fn eval_large_markdown_filtering() -> mq_lang::RuntimeValues { - let markdown_content = (0..100) - .map(|i| format!("# Heading {}\n\nSome content here.\n\n- Item 1\n- Item 2\n- Item 3\n\n## Subheading {}\n\nMore content.\n\n", i, i)) - .collect::(); - let markdown: mq_markdown::Markdown = mq_markdown::Markdown::from_markdown_str(&markdown_content).unwrap(); - let input = markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from); +/// A matching tree walk catches unnecessary copies when the VM returns each source node. +#[divan::bench] +fn eval_compiled_owned_markdown_tree(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - engine.eval(".h | nodes", input.into_iter()).unwrap() + let compiled = engine.compile(".").unwrap(); + engine + .eval_compiled(&compiled, std::iter::once(owned_markdown_tree())) + .unwrap(); + + bencher.bench_local(|| { + engine + .eval_compiled(&compiled, std::iter::once(owned_markdown_tree())) + .unwrap() + }); } -#[divan::bench()] -fn eval_markdown_complex_query() -> mq_lang::RuntimeValues { - let markdown_content = (0..50) - .map(|i| format!("# Heading {}\n\n**Bold text** and *italic text*.\n\n- Item 1\n- Item 2\n\n```rust\nfn main() {{\n println!(\"Hello\");\n}}\n```\n\n", i)) - .collect::(); - let markdown: mq_markdown::Markdown = mq_markdown::Markdown::from_markdown_str(&markdown_content).unwrap(); - let input = markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from); +/// A rejecting tree walk catches copies in the child-preserving fallback path. +#[divan::bench] +fn eval_compiled_owned_markdown_tree_without_matches(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); + let compiled = engine.compile(".h1").unwrap(); engine - .eval( - ".h1 | nodes | map(upcase) | filter(fn(x): contains(x, \"HEADING\");)", - input.into_iter(), - ) - .unwrap() -} + .eval_compiled(&compiled, std::iter::once(owned_markdown_tree())) + .unwrap(); -// Variable Assignment and Access Benchmarks + bencher.bench_local(|| { + engine + .eval_compiled(&compiled, std::iter::once(owned_markdown_tree())) + .unwrap() + }); +} -#[divan::bench()] -fn eval_variable_assignment_chain() -> mq_lang::RuntimeValues { +/// Covers selector dispatch plus the `nodes` module's tree traversal and a native builtin. +#[divan::bench] +fn eval_compiled_nodes(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); - engine - .eval( - r#"foreach(i, range(0, 100, 1)): - let a = i | let b = a + 1 | let c = b + 2 | let d = c + 3 | let e = d + 4 - | a + b + c + d + e;"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() + engine.load_builtin_module(); + bench_compiled(bencher, &mut engine, ".h | nodes | map(upcase)", || { + mq_markdown::Markdown::from_markdown_str("# heading\n- item1\n- item2\n## heading2\n- item1\n- item2\n") + .unwrap() + .nodes + .into_iter() + .map(mq_lang::RuntimeValue::from) + .collect() + }); } -// Conditional Execution Benchmarks +const CSV_PARSE_INPUT: &str = + "a,b,c\n\"1,2\",\"2,3\",\"3,4\"\n4,5,6\n\"multi\nline\",7,8\n9,10,\"quoted,comma\"\n\"\",11,12\n13,14,15\n"; -#[divan::bench()] -fn eval_if_else_branching() -> mq_lang::RuntimeValues { +/// Reuses optimized bytecode and the standard CSV module for one input record. +#[divan::bench] +fn eval_compiled_csv_parse(bencher: divan::Bencher) { let mut engine = mq_lang::DefaultEngine::default(); engine.load_builtin_module(); - engine - .eval( - r#"def classify(x): - if (x % 5 == 0): - x * 5 - elif (x % 3 == 0): - x * 3 - elif (x % 2 == 0): - x * 2 - else: - x; - | map(range(0, 500, 1), classify)"#, - vec![mq_lang::RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ) - .unwrap() + engine.load_module("csv").unwrap(); + bench_compiled(bencher, &mut engine, "csv_parse(true)", || { + vec![mq_lang::RuntimeValue::String(Shared::new(CSV_PARSE_INPUT.to_string()))] + }); } -/// Measures `expand_wikilinks` post-processing cost when the document contains wikilinks. -#[divan::bench(name = "parse_markdown_with_wikilinks")] -fn parse_markdown_with_wikilinks() -> mq_markdown::Markdown { - let content = (0..100) +fn section_markdown_input() -> impl Iterator { + let markdown_content = (0..30) .map(|i| { - format!("# Heading {i}\n\nSome text with [[target{i}]] and [[another{i}|Display Text {i}]] links.\n\n") + format!( + "# Section {i}\n\nIntro paragraph for section {i}.\n\n## Subsection {i}\n\nSome detail text.\n\n- point a\n- point b\n\n" + ) }) .collect::(); - mq_markdown::Markdown::from_markdown_str(&content).unwrap() + let markdown: mq_markdown::Markdown = mq_markdown::Markdown::from_markdown_str(&markdown_content).unwrap(); + markdown.nodes.into_iter().map(mq_lang::RuntimeValue::from) } -/// Baseline: same document shape but no `[[...]]` patterns — measures pure traversal cost. -#[divan::bench(name = "parse_markdown_without_wikilinks")] -fn parse_markdown_without_wikilinks() -> mq_markdown::Markdown { - let content = (0..100) - .map(|i| format!("# Heading {i}\n\nSome text without any wikilink patterns here.\n\n")) - .collect::(); - mq_markdown::Markdown::from_markdown_str(&content).unwrap() +/// Covers a document-scale Markdown query through the cached standard `section` module. +#[divan::bench] +fn eval_compiled_section_sections(bencher: divan::Bencher) { + let mut engine = mq_lang::DefaultEngine::default(); + engine.load_builtin_module(); + engine.load_module("section").unwrap(); + bench_compiled(bencher, &mut engine, "nodes | sections() | len()", || { + section_markdown_input().collect() + }); +} + +#[divan::bench] +fn parse_fibonacci() -> Vec> { + let token_arena = Shared::new(SharedCell::new(mq_lang::Arena::new(100))); + mq_lang::parse( + " + def fibonacci(x): + if (x == 0): + 0 + elif (x == 1): + 1 + else: + fibonacci(sub(x, 1)) + fibonacci(sub(x, 2)); | fibonacci(20)", + Shared::clone(&token_arena), + ) + .unwrap() +} + +/// Exercises byte-string lexing without charging construction of the input to the parser. +#[divan::bench] +fn parse_large_byte_string() -> Vec> { + static CODE: LazyLock = LazyLock::new(|| format!(r#"b"{}""#, "a".repeat(16 * 1024))); + let token_arena = Shared::new(SharedCell::new(mq_lang::Arena::new(4))); + mq_lang::parse(&CODE, token_arena).unwrap() +} + +/// Exercises flattening of a long logical-expression chain during AST construction. +#[divan::bench] +fn parse_long_and_chain() -> Vec> { + static CODE: LazyLock = + LazyLock::new(|| std::iter::repeat_n("true", 4_096).collect::>().join(" && ")); + let token_arena = Shared::new(SharedCell::new(mq_lang::Arena::new(8_192))); + mq_lang::parse(&CODE, token_arena).unwrap() } diff --git a/crates/mq-lang/modules/__snapshots__/json_test/json_stringify.snap b/crates/mq-lang/modules/__snapshots__/json_test/json_stringify.snap index fc6b6cb1c..f7e003d44 100644 --- a/crates/mq-lang/modules/__snapshots__/json_test/json_stringify.snap +++ b/crates/mq-lang/modules/__snapshots__/json_test/json_stringify.snap @@ -1 +1 @@ -{"users": [{"name": "Alice", "id": 1, "email": "alice@example.com", "roles": ["admin", "user"]}, {"name": "Bob", "id": 2, "email": "bob@example.com", "roles": ["user"]}, {"name": "Charlie", "id": 3, "email": "charlie@example.com", "roles": ["editor", "user"]}], "meta": {"count": 3, "generated_at": "2024-06-01T12:00:00Z"}} \ No newline at end of file +{"users": [{"id": 1, "name": "Alice", "email": "alice@example.com", "roles": ["admin", "user"]}, {"id": 2, "name": "Bob", "email": "bob@example.com", "roles": ["user"]}, {"id": 3, "name": "Charlie", "email": "charlie@example.com", "roles": ["editor", "user"]}], "meta": {"count": 3, "generated_at": "2024-06-01T12:00:00Z"}} \ No newline at end of file diff --git a/crates/mq-lang/modules/__snapshots__/json_test/json_to_markdown_table.snap b/crates/mq-lang/modules/__snapshots__/json_test/json_to_markdown_table.snap index ab975f6fe..58b74dcd0 100644 --- a/crates/mq-lang/modules/__snapshots__/json_test/json_to_markdown_table.snap +++ b/crates/mq-lang/modules/__snapshots__/json_test/json_to_markdown_table.snap @@ -1,5 +1,5 @@ -| name | id | email | roles | +| id | name | email | roles | | --- | --- | --- | --- | -| Alice | 1 | alice@example.com | ["admin", "user"] | -| Bob | 2 | bob@example.com | ["user"] | -| Charlie | 3 | charlie@example.com | ["editor", "user"] | \ No newline at end of file +| 1 | Alice | alice@example.com | ["admin", "user"] | +| 2 | Bob | bob@example.com | ["user"] | +| 3 | Charlie | charlie@example.com | ["editor", "user"] | \ No newline at end of file diff --git a/crates/mq-lang/modules/__snapshots__/toon_test/toon_stringify.snap b/crates/mq-lang/modules/__snapshots__/toon_test/toon_stringify.snap new file mode 100644 index 000000000..0021cd7b2 --- /dev/null +++ b/crates/mq-lang/modules/__snapshots__/toon_test/toon_stringify.snap @@ -0,0 +1,16 @@ +context: + task: Our favorite hikes together + location: Boulder + season: spring_2025 +friends[3]: ana,luis,sam +hikes[3]{id,name,distanceKm,elevationGain,companion,wasSunny}: + 1,Blue Lake Trail,7.5,320,ana,true + 2,Ridge Overlook,9.2,540,luis,false + 3,Wildflower Loop,5.1,180,sam,true +items[3]: + - 1 + - a: 1 + - text +items2[2]{name,price}: + Laptop,999 + Mouse,29 \ No newline at end of file diff --git a/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_json.snap b/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_json.snap index 6a61686b4..55c051e12 100644 --- a/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_json.snap +++ b/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_json.snap @@ -1 +1 @@ -{"array": ["item1", "item2", "item3"], "nested": {"arr": ["a", "b"], "obj": {"subkey": "subval"}}, "number": 42, "float": 3.14, "bool_true": true, "bool_false": false, "null_value": null, "object": {"key1": "value1", "key2": "value2"}} \ No newline at end of file +{"number": 42, "float": 3.14, "bool_true": true, "bool_false": false, "null_value": null, "array": ["item1", "item2", "item3"], "object": {"key1": "value1", "key2": "value2"}, "nested": {"arr": ["a", "b"], "obj": {"subkey": "subval"}}} \ No newline at end of file diff --git a/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_markdown_table.snap b/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_markdown_table.snap index da4b2c902..49b39699b 100644 --- a/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_markdown_table.snap +++ b/crates/mq-lang/modules/__snapshots__/yaml_test/yaml_to_markdown_table.snap @@ -1,10 +1,10 @@ | Key | Value | | --- | --- | -| array | ["item1", "item2", "item3"] | -| nested | {"arr": ["a", "b"], "obj": {"subkey": "subval"}} | | number | 42 | | float | 3.14 | | bool_true | true | | bool_false | false | | null_value | | -| object | {"key1": "value1", "key2": "value2"} | \ No newline at end of file +| array | ["item1", "item2", "item3"] | +| object | {"key1": "value1", "key2": "value2"} | +| nested | {"arr": ["a", "b"], "obj": {"subkey": "subval"}} | \ No newline at end of file diff --git a/crates/mq-lang/modules/html_test.mq b/crates/mq-lang/modules/html_test.mq index 36fae1ab9..996bde280 100644 --- a/crates/mq-lang/modules/html_test.mq +++ b/crates/mq-lang/modules/html_test.mq @@ -90,7 +90,7 @@ end # @parametrize([ # [{"tag": "a"}, "{\"tag\": \"a\"}"], -# [{"tag": "br", "attributes": {}, "children": [], "text": None}, "{\"text\": null, \"tag\": \"br\", \"attributes\": {}, \"children\": []}"], +# [{"tag": "br", "attributes": {}, "children": [], "text": None}, "{\"tag\": \"br\", \"attributes\": {}, \"children\": [], \"text\": null}"], # ]) def test_html_to_json_simple(data, expected): assert_eq(html::html_to_json(data), expected) diff --git a/crates/mq-lang/modules/toon_test.mq b/crates/mq-lang/modules/toon_test.mq index cbcc299d1..8f6fb30ce 100644 --- a/crates/mq-lang/modules/toon_test.mq +++ b/crates/mq-lang/modules/toon_test.mq @@ -11,33 +11,11 @@ def test_toon_parse(): end -# `RuntimeValue::Dict` (a `BTreeMap`, see mq-lang/src/ident.rs) orders keys by -# interned symbol id, not by string content, so a multi-key dict's field order in the -# stringified output depends on which other identifiers this process happened to intern -# first (e.g. which other test files ran earlier) rather than on `toon_input` itself. -# Comparing sorted lines instead of an exact/ordered snapshot keeps this test meaningful -# (it still catches content/format regressions) without being sensitive to that order. +# `RuntimeValue::Dict` preserves insertion order, so TOON stringification has stable +# key ordering. Keep the complete output in a snapshot to catch format regressions. def test_toon_stringify(): let result = toon::toon_stringify(toon::toon_parse(toon_input)) - | let expected_lines = [ - "context:", - " task: Our favorite hikes together", - " location: Boulder", - " season: spring_2025", - "friends[3]: ana,luis,sam", - "hikes[3]{name,id,distanceKm,elevationGain,companion,wasSunny}:", - " Blue Lake Trail,1,7.5,320,ana,true", - " Ridge Overlook,2,9.2,540,luis,false", - " Wildflower Loop,3,5.1,180,sam,true", - "items[3]:", - " - 1", - " - a: 1", - " - text", - "items2[2]{name,price}:", - " Laptop,999", - " Mouse,29", - ] - | assert_eq(sort(lines(result)), sort(expected_lines)) + | assert_snapshot("toon_stringify", result) end def test_toon_round_trip(): diff --git a/crates/mq-lang/src/ast.rs b/crates/mq-lang/src/ast.rs index 20fce9a73..7722ab094 100644 --- a/crates/mq-lang/src/ast.rs +++ b/crates/mq-lang/src/ast.rs @@ -42,7 +42,7 @@ mod tests { let ident = Shared::new(Node { token_id: TokenId::new(1), - expr: Shared::new(AstExpr::Ident(IdentWithToken::new("foo"))), + expr: AstExpr::Ident(IdentWithToken::new("foo")), }); let program = vec![ident.clone()]; @@ -50,7 +50,7 @@ mod tests { let deserialized = ast_from_json(&json).expect("Deserialization should succeed"); assert_eq!(deserialized.len(), 1); - match &*deserialized[0].expr { + match &deserialized[0].expr { AstExpr::Ident(name) => assert_eq!(name.name, "foo".into()), _ => panic!("Expected Ident node"), } diff --git a/crates/mq-lang/src/ast/code.rs b/crates/mq-lang/src/ast/code.rs index 2452f4f32..4c9cb5700 100644 --- a/crates/mq-lang/src/ast/code.rs +++ b/crates/mq-lang/src/ast/code.rs @@ -12,7 +12,7 @@ impl Node { } fn format_to_code(&self, buf: &mut String, indent: usize) { - match &*self.expr { + match &self.expr { Expr::Literal(lit) => { format_literal(lit, buf); } @@ -461,7 +461,7 @@ mod tests { fn create_node(expr: Expr) -> Node { Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, } } diff --git a/crates/mq-lang/src/ast/node.rs b/crates/mq-lang/src/ast/node.rs index a6912aab6..bdffddb5d 100644 --- a/crates/mq-lang/src/ast/node.rs +++ b/crates/mq-lang/src/ast/node.rs @@ -67,7 +67,7 @@ pub struct Node { serde(skip_serializing, skip_deserializing, default = "default_token_id") )] pub token_id: TokenId, - pub expr: Shared, + pub expr: Expr, } #[cfg(feature = "ast-json")] @@ -87,7 +87,7 @@ impl Node { } pub fn range(&self, arena: Shared>>) -> Range { - match &*self.expr { + match &self.expr { Expr::Block(program) | Expr::Def(_, _, program) | Expr::Fn(_, program) @@ -194,7 +194,7 @@ impl Node { } pub fn is_nodes(&self) -> bool { - matches!(*self.expr, Expr::Nodes) + matches!(&self.expr, Expr::Nodes) } } @@ -419,16 +419,16 @@ mod tests { Expr::CallDynamic( Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("callee".to_string()))), + expr: Expr::Literal(Literal::String("callee".to_string())), }), smallvec![ Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("arg1".to_string()))), + expr: Expr::Literal(Literal::String("arg1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("arg2".to_string()))), + expr: Expr::Literal(Literal::String("arg2".to_string())), }), ] ), @@ -442,7 +442,7 @@ mod tests { Expr::Match( Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("val".to_string()))), + expr: Expr::Literal(Literal::String("val".to_string())), }), smallvec![ MatchArm { @@ -450,7 +450,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("body1".to_string()))), + expr: Expr::Literal(Literal::String("body1".to_string())), }), }, MatchArm { @@ -458,7 +458,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: ArenaId::new(2), - expr: Shared::new(Expr::Literal(Literal::String("body2".to_string()))), + expr: Expr::Literal(Literal::String("body2".to_string())), }), }, ] @@ -474,12 +474,12 @@ mod tests { Expr::Try( Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("try".to_string()))), + expr: Expr::Literal(Literal::String("try".to_string())), }), None, Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("catch".to_string()))), + expr: Expr::Literal(Literal::String("catch".to_string())), }) ), vec![ @@ -493,7 +493,7 @@ mod tests { Pattern::Ident(IdentWithToken::new("x")), Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("letval".to_string()))), + expr: Expr::Literal(Literal::String("letval".to_string())), }) ), vec![ @@ -505,7 +505,7 @@ mod tests { Expr::Paren( Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("paren".to_string()))), + expr: Expr::Literal(Literal::String("paren".to_string())), }) ), vec![ @@ -517,11 +517,11 @@ mod tests { Expr::Block(vec![ Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("block1".to_string()))), + expr: Expr::Literal(Literal::String("block1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("block2".to_string()))), + expr: Expr::Literal(Literal::String("block2".to_string())), }), ]), vec![ @@ -537,11 +537,11 @@ mod tests { vec![ Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("def1".to_string()))), + expr: Expr::Literal(Literal::String("def1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("def2".to_string()))), + expr: Expr::Literal(Literal::String("def2".to_string())), }), ] ), @@ -557,11 +557,11 @@ mod tests { vec![ Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("fn1".to_string()))), + expr: Expr::Literal(Literal::String("fn1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("fn2".to_string()))), + expr: Expr::Literal(Literal::String("fn2".to_string())), }), ] ), @@ -575,16 +575,16 @@ mod tests { Expr::While( Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("cond".to_string()))), + expr: Expr::Literal(Literal::String("cond".to_string())), }), vec![ Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("while1".to_string()))), + expr: Expr::Literal(Literal::String("while1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(2), - expr: Shared::new(Expr::Literal(Literal::String("while2".to_string()))), + expr: Expr::Literal(Literal::String("while2".to_string())), }), ] ), @@ -599,16 +599,16 @@ mod tests { Expr::Until( Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("cond".to_string()))), + expr: Expr::Literal(Literal::String("cond".to_string())), }), vec![ Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("until1".to_string()))), + expr: Expr::Literal(Literal::String("until1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(2), - expr: Shared::new(Expr::Literal(Literal::String("until2".to_string()))), + expr: Expr::Literal(Literal::String("until2".to_string())), }), ] ), @@ -624,16 +624,16 @@ mod tests { IdentWithToken::new("item"), Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("iter".to_string()))), + expr: Expr::Literal(Literal::String("iter".to_string())), }), vec![ Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("foreach1".to_string()))), + expr: Expr::Literal(Literal::String("foreach1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(2), - expr: Shared::new(Expr::Literal(Literal::String("foreach2".to_string()))), + expr: Expr::Literal(Literal::String("foreach2".to_string())), }), ] ), @@ -649,21 +649,21 @@ mod tests { ( Some(Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("cond1".to_string()))), + expr: Expr::Literal(Literal::String("cond1".to_string())), })), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("if1".to_string()))), + expr: Expr::Literal(Literal::String("if1".to_string())), }) ), ( Some(Shared::new(Node { token_id: ArenaId::new(2), - expr: Shared::new(Expr::Literal(Literal::String("cond2".to_string()))), + expr: Expr::Literal(Literal::String("cond2".to_string())), })), Shared::new(Node { token_id: ArenaId::new(3), - expr: Shared::new(Expr::Literal(Literal::String("if2".to_string()))), + expr: Expr::Literal(Literal::String("if2".to_string())), }) ), ]), @@ -680,11 +680,11 @@ mod tests { ( Some(Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("cond1".to_string()))), + expr: Expr::Literal(Literal::String("cond1".to_string())), })), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("unless1".to_string()))), + expr: Expr::Literal(Literal::String("unless1".to_string())), }) ), ]), @@ -700,11 +700,11 @@ mod tests { smallvec![ Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::String("arg1".to_string()))), + expr: Expr::Literal(Literal::String("arg1".to_string())), }), Shared::new(Node { token_id: ArenaId::new(1), - expr: Shared::new(Expr::Literal(Literal::String("arg2".to_string()))), + expr: Expr::Literal(Literal::String("arg2".to_string())), }), ] ), @@ -726,7 +726,7 @@ mod tests { } let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(Shared::new(arena)), expected); } @@ -734,7 +734,7 @@ mod tests { fn make_node(token_id: u32) -> Shared { Shared::new(Node { token_id: ArenaId::new(token_id), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }) } @@ -760,7 +760,7 @@ mod tests { let expr = Expr::Loop(vec![make_node(0), make_node(1)]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; let got = node.range(Shared::new(arena)); assert_eq!(got.start, r0.start); @@ -783,7 +783,7 @@ mod tests { let expr = Expr::Module(IdentWithToken::new("m"), vec![make_node(0), make_node(1)]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; let got = node.range(Shared::new(arena)); assert_eq!(got.start, r0.start); @@ -800,7 +800,7 @@ mod tests { let expr = Expr::Block(vec![]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; let got = node.range(arena); assert_eq!(got, Range::default()); @@ -816,7 +816,7 @@ mod tests { let expr = Expr::As(IdentWithToken::new("x"), make_node(0)); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), r0); } @@ -831,7 +831,7 @@ mod tests { let expr = Expr::Var(Pattern::Wildcard, make_node(0)); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), r0); } @@ -846,7 +846,7 @@ mod tests { let expr = Expr::Assign(IdentWithToken::new("v"), make_node(0)); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), r0); } @@ -861,7 +861,7 @@ mod tests { let expr = Expr::And(vec![]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), r0); } @@ -876,7 +876,7 @@ mod tests { let expr = Expr::Or(vec![]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), r0); } @@ -897,7 +897,7 @@ mod tests { let expr = Expr::And(vec![make_node(0), make_node(1)]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; let got = node.range(Shared::new(arena)); assert_eq!(got.start, r0.start); @@ -920,7 +920,7 @@ mod tests { let expr = Expr::Break(Some(make_node(1))); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; let got = node.range(Shared::new(arena)); assert_eq!(got.start, r0.start); @@ -944,7 +944,7 @@ mod tests { let expr = Expr::SelectorCall(Selector::Heading(None), smallvec![make_node(1)]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; let got = node.range(Shared::new(arena)); assert_eq!(got.start, r0.start); @@ -968,7 +968,7 @@ mod tests { let arena = single_token_arena(r0); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), r0, "terminal expr should use token range"); } @@ -984,7 +984,7 @@ mod tests { let expr = Expr::Call(IdentWithToken::new("f"), smallvec![]); let node = Node { token_id: ArenaId::new(0), - expr: Shared::new(expr), + expr, }; assert_eq!(node.range(arena), Range::default()); } @@ -995,12 +995,12 @@ mod tests { let arena = single_token_arena(r); let nodes_node = Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Nodes), + expr: Expr::Nodes, }; assert!(nodes_node.is_nodes()); let other = Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }; assert!(!other.is_nodes()); let _ = arena; @@ -1076,7 +1076,7 @@ mod tests { fn test_expr_display_call_dynamic() { let callee = Shared::new(Node { token_id: ArenaId::new(0), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }); let dynamic = Expr::CallDynamic(callee, smallvec![]); let s = format!("{dynamic}"); diff --git a/crates/mq-lang/src/ast/parser.rs b/crates/mq-lang/src/ast/parser.rs index c7cf34ffe..f81e68c33 100644 --- a/crates/mq-lang/src/ast/parser.rs +++ b/crates/mq-lang/src/ast/parser.rs @@ -7,6 +7,7 @@ use crate::module::ModuleId; use crate::runtime::builtin::io_context; use crate::selector::Selector; use crate::{Ident, Shared, lexer}; +use rustc_hash::FxHashMap; use smallvec::{SmallVec, smallvec}; use smol_str::SmolStr; use std::iter::Peekable; @@ -21,19 +22,21 @@ type IfExpr = (Option>, Shared); static GET_IDENT: LazyLock = LazyLock::new(|| Ident::from(constants::builtins::GET)); pub struct Parser<'a, 'alloc> { - tokens: Peekable>>, + tokens: Peekable>, + token_cache: FxHashMap<*const Token, Shared>, token_arena: &'alloc mut Arena>, module_id: ModuleId, } impl<'a, 'alloc> Parser<'a, 'alloc> { pub fn new( - tokens: core::slice::Iter<'a, Shared>, + tokens: core::slice::Iter<'a, Token>, token_arena: &'alloc mut Arena>, module_id: ModuleId, ) -> Self { Self { tokens: tokens.peekable(), + token_cache: FxHashMap::default(), token_arena, module_id, } @@ -43,6 +46,25 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { self.parse_program(true) } + /// Returns the shared representation for a token retained by the AST. + /// + /// The lexer owns the input token vector for the duration of parsing, so its element address + /// is a stable identity. Caching here avoids eagerly allocating an `Rc`/`Arc` for every token + /// while still sharing a token when multiple AST fields retain it. + fn shared_token(&mut self, token: &Token) -> Shared { + let token_address = token as *const Token; + self.token_cache + .entry(token_address) + .or_insert_with(|| Shared::new(token.clone())) + .clone() + } + + /// Retains a token in the token arena and returns its arena identifier. + fn alloc_token(&mut self, token: &Token) -> TokenId { + let token = self.shared_token(token); + self.token_arena.alloc(token) + } + fn parse_program(&mut self, root: bool) -> Result { let mut asts = Vec::with_capacity(64); @@ -50,10 +72,10 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match self.tokens.peek() { Some(token) => match &token.kind { TokenKind::Pipe | TokenKind::SemiColon => { - return Err(SyntaxError::UnexpectedToken((***token).clone())); + return Err(SyntaxError::UnexpectedToken((**token).clone())); } TokenKind::End => { - return Err(SyntaxError::UnmatchedEnd((***token).clone())); + return Err(SyntaxError::UnmatchedEnd((**token).clone())); } _ => {} }, @@ -72,9 +94,9 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match self.tokens.peek() { Some(next_token) if !matches!(next_token.kind, TokenKind::Eof) => { if matches!(token.kind, TokenKind::End) { - return Err(SyntaxError::UnmatchedEnd((**token).clone())); + return Err(SyntaxError::UnmatchedEnd(token.clone())); } else { - return Err(SyntaxError::UnexpectedToken((***next_token).clone())); + return Err(SyntaxError::UnexpectedToken((**next_token).clone())); } } _ => break, @@ -88,7 +110,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { asts.push(ast); } TokenKind::Nodes => { - return Err(SyntaxError::UnexpectedToken((**token).clone())); + return Err(SyntaxError::UnexpectedToken(token.clone())); } TokenKind::NewLine | TokenKind::Tab(_) | TokenKind::Whitespace(_) => { unreachable!("parse_program should have filtered out whitespace tokens") @@ -108,7 +130,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } #[inline(always)] - fn parse_expr(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_expr(&mut self, token: &Token) -> Result, SyntaxError> { self.parse_equality_expr(token) } @@ -164,19 +186,19 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } fn create_compound_assign( - &self, + &mut self, lhs: &Shared, rhs: Shared, operator_token_id: TokenId, - operator_token: &Shared, + operator_token: &Token, function_name: &'static str, ) -> Result, SyntaxError> { let compound_rhs = Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(function_name, Some(Shared::clone(operator_token))), + expr: Expr::Call( + IdentWithToken::new_with_token(function_name, Some(self.shared_token(operator_token))), smallvec![Shared::clone(lhs), rhs], - )), + ), }); self.create_assign(lhs, compound_rhs, operator_token_id, operator_token) } @@ -186,33 +208,33 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { /// - `x = rhs` → `Assign(x, rhs)` /// - `arr[idx] = rhs` (lhs is `Call("get", [arr, idx])`) → `Assign(arr, set(arr, idx, rhs))` fn create_assign( - &self, + &mut self, lhs: &Shared, rhs: Shared, operator_token_id: TokenId, - operator_token: &Shared, + operator_token: &Token, ) -> Result, SyntaxError> { - match &*lhs.expr { + match &lhs.expr { Expr::Ident(ident) => Ok(Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Assign(ident.clone(), rhs)), + expr: Expr::Assign(ident.clone(), rhs), })), - Expr::Call(func_ident, args) if func_ident.name == *GET_IDENT && args.len() == 2 => match &*args[0].expr { + Expr::Call(func_ident, args) if func_ident.name == *GET_IDENT && args.len() == 2 => match &args[0].expr { Expr::Ident(var_ident) => Ok(Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Assign( + expr: Expr::Assign( var_ident.clone(), Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::SET, - Some(Shared::clone(operator_token)), + Some(self.shared_token(operator_token)), ), smallvec![Shared::clone(&args[0]), Shared::clone(&args[1]), rhs,], - )), + ), }), - )), + ), })), _ => Err(SyntaxError::InvalidAssignmentTarget( (*self.token_arena[args[0].token_id]).clone(), @@ -238,14 +260,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } let operator_token = parser.tokens.next().unwrap(); - let operator_token_id = parser.token_arena.alloc(Shared::clone(operator_token)); + let operator_token_id = parser.alloc_token(operator_token); let rhs_token = match parser.tokens.next() { Some(t) if t.kind == TokenKind::Eof => { - return Err(SyntaxError::UnexpectedEOFAfterToken((**operator_token).clone())); + return Err(SyntaxError::UnexpectedEOFAfterToken(operator_token.clone())); } Some(t) => t, - None => return Err(SyntaxError::UnexpectedEOFAfterToken((**operator_token).clone())), + None => return Err(SyntaxError::UnexpectedEOFAfterToken(operator_token.clone())), }; let mut rhs = parser.parse_primary_expr(rhs_token)?; @@ -269,11 +291,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { lhs = match kind { TokenKind::Equal => parser.create_assign(&lhs, rhs, operator_token_id, operator_token)?, TokenKind::And => { - if matches!(&*lhs.expr, Expr::And(_)) { + if matches!(&lhs.expr, Expr::And(_)) { let mut lhs = lhs; let node = Shared::make_mut(&mut lhs); node.token_id = operator_token_id; - let Expr::And(operands) = Shared::make_mut(&mut node.expr) else { + let Expr::And(operands) = &mut node.expr else { unreachable!("checked the expression before making it mutable"); }; operands.push(rhs); @@ -281,16 +303,16 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } else { Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::And(vec![lhs, rhs])), + expr: Expr::And(vec![lhs, rhs]), }) } } TokenKind::Or => { - if matches!(&*lhs.expr, Expr::Or(_)) { + if matches!(&lhs.expr, Expr::Or(_)) { let mut lhs = lhs; let node = Shared::make_mut(&mut lhs); node.token_id = operator_token_id; - let Expr::Or(operands) = Shared::make_mut(&mut node.expr) else { + let Expr::Or(operands) = &mut node.expr else { unreachable!("checked the expression before making it mutable"); }; operands.push(rhs); @@ -298,7 +320,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } else { Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Or(vec![lhs, rhs])), + expr: Expr::Or(vec![lhs, rhs]), }) } } @@ -340,34 +362,34 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::DoubleSlashEqual => { let floor_div_rhs = Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::FLOOR, - Some(Shared::clone(operator_token)), + Some(parser.shared_token(operator_token)), ), smallvec![Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::DIV, - Some(Shared::clone(operator_token)), + Some(parser.shared_token(operator_token)), ), smallvec![Shared::clone(&lhs), rhs], - )), + ), })], - )), + ), }); parser.create_assign(&lhs, floor_div_rhs, operator_token_id, operator_token)? } _ => Shared::new(Node { token_id: operator_token_id, - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( Self::binary_op_function_name(kind), - Some(Shared::clone(operator_token)), + Some(parser.shared_token(operator_token)), ), smallvec![lhs, rhs], - )), + ), }), }; } @@ -375,7 +397,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(lhs) } - fn parse_equality_expr(&mut self, initial_token: &Shared) -> Result, SyntaxError> { + fn parse_equality_expr(&mut self, initial_token: &Token) -> Result, SyntaxError> { let lhs = self.parse_primary_expr(initial_token)?; let lhs = Self::parse_binary_op(self, 0, lhs)?; @@ -390,7 +412,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { fn parse_as_binding(&mut self, expr: Shared) -> Result, SyntaxError> { let as_token = self.tokens.next().unwrap(); - let as_token_id = self.token_arena.alloc(Shared::clone(as_token)); + let as_token_id = self.alloc_token(as_token); let name_token = match self.tokens.next() { Some(token) => token, @@ -399,17 +421,17 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match &name_token.kind { TokenKind::Ident(name) => { - let ident = IdentWithToken::new_with_token(name, Some(Shared::clone(name_token))); + let ident = IdentWithToken::new_with_token(name, Some(self.shared_token(name_token))); Ok(Shared::new(Node { token_id: as_token_id, - expr: Shared::new(Expr::As(ident, expr)), + expr: Expr::As(ident, expr), })) } - _ => Err(SyntaxError::UnexpectedToken((**name_token).clone())), + _ => Err(SyntaxError::UnexpectedToken(name_token.clone())), } } - fn parse_primary_expr(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_primary_expr(&mut self, token: &Token) -> Result, SyntaxError> { match &token.kind { TokenKind::Selector(_) | TokenKind::DoubleDot => self.parse_selector(token), TokenKind::Let => self.parse_let(token), @@ -446,11 +468,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::None => self.parse_literal(token), TokenKind::Colon => self.parse_symbol(token), TokenKind::Eof => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), - _ => Err(SyntaxError::UnexpectedToken((**token).clone())), + _ => Err(SyntaxError::UnexpectedToken(token.clone())), } } - fn parse_module(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_module(&mut self, token: &Token) -> Result, SyntaxError> { match &token.kind { TokenKind::Module => match self.tokens.peek() { Some(_) => { @@ -465,7 +487,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Only allow 'let', 'def', or 'module' at the top-level of a module block for node in &program { - match &*node.expr { + match &node.expr { Expr::Let(_, _) | Expr::Def(_, _, _) | Expr::Module(_, _) | Expr::Import(_, _) => {} _ => { return Err(SyntaxError::UnexpectedToken((*self.token_arena[node.token_id]).clone())); @@ -474,28 +496,28 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Module( + token_id: self.alloc_token(token), + expr: Expr::Module( IdentWithToken::new_with_token( match &ident_token.kind { TokenKind::Ident(name) => name, _ => { - return Err(SyntaxError::UnexpectedToken((**ident_token).clone())); + return Err(SyntaxError::UnexpectedToken(ident_token.clone())); } }, - Some(Shared::clone(ident_token)), + Some(self.shared_token(ident_token)), ), program, - )), + ), })) } - None => Err(SyntaxError::UnexpectedToken((**token).clone())), + None => Err(SyntaxError::UnexpectedToken(token.clone())), }, - _ => Err(SyntaxError::UnexpectedToken((**token).clone())), + _ => Err(SyntaxError::UnexpectedToken(token.clone())), } } - fn parse_symbol(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_symbol(&mut self, token: &Token) -> Result, SyntaxError> { match &token.kind { TokenKind::Colon => { let next_token = match self.tokens.next() { @@ -504,23 +526,23 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }; match &next_token.kind { TokenKind::Ident(name) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Literal(Literal::Symbol(Ident::new(name)))), + token_id: self.alloc_token(token), + expr: Expr::Literal(Literal::Symbol(Ident::new(name))), })), TokenKind::StringLiteral(s) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Literal(Literal::Symbol(Ident::new(s)))), + token_id: self.alloc_token(token), + expr: Expr::Literal(Literal::Symbol(Ident::new(s))), })), - _ => Err(SyntaxError::UnexpectedToken((**next_token).clone())), + _ => Err(SyntaxError::UnexpectedToken(next_token.clone())), } } - _ => Err(SyntaxError::UnexpectedToken((**token).clone())), + _ => Err(SyntaxError::UnexpectedToken(token.clone())), } } - fn parse_paren(&mut self, lparen_token: &Shared) -> Result, SyntaxError> { - let opening = (**lparen_token).clone(); - let token_id = self.token_arena.alloc(Shared::clone(lparen_token)); + fn parse_paren(&mut self, lparen_token: &Token) -> Result, SyntaxError> { + let opening = lparen_token.clone(); + let token_id = self.alloc_token(lparen_token); let expr_token = match self.tokens.next() { Some(t) => t, None => { @@ -540,12 +562,9 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match self.tokens.next() { Some(t) if t.kind == TokenKind::RParen => {} Some(t) if t.kind == TokenKind::Eof => { - return Err(SyntaxError::ExpectedClosingParen( - (**t).clone(), - Some(Box::new(opening)), - )); + return Err(SyntaxError::ExpectedClosingParen(t.clone(), Some(Box::new(opening)))); } - Some(t) => return Err(SyntaxError::UnexpectedToken((**t).clone())), + Some(t) => return Err(SyntaxError::UnexpectedToken(t.clone())), None => { return Err(SyntaxError::ExpectedClosingParen( Token { @@ -560,22 +579,22 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let paren_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::Paren(expr_node)), + expr: Expr::Paren(expr_node), }); // Handle postfix operations: (expr)(args), (expr)[N], (expr)(args)[N], etc. self.parse_postfix_ops(paren_node, lparen_token) } - fn parse_not(&mut self, not_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(not_token)); + fn parse_not(&mut self, not_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(not_token); let expr_token = match self.tokens.next() { Some(t) if t.kind == TokenKind::Eof => { - return Err(SyntaxError::UnexpectedEOFAfterToken((**not_token).clone())); + return Err(SyntaxError::UnexpectedEOFAfterToken(not_token.clone())); } Some(t) => t, - None => return Err(SyntaxError::UnexpectedEOFAfterToken((**not_token).clone())), + None => return Err(SyntaxError::UnexpectedEOFAfterToken(not_token.clone())), }; if !matches!( @@ -598,30 +617,30 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { | TokenKind::Not | TokenKind::Ident(_) ) { - return Err(SyntaxError::UnexpectedToken((**expr_token).clone())); + return Err(SyntaxError::UnexpectedToken(expr_token.clone())); } let expr_node = self.parse_primary_expr(expr_token)?; // Convert ! to not() function call - let not_ident = IdentWithToken::new_with_token(constants::builtins::NOT, Some(Shared::clone(not_token))); + let not_ident = IdentWithToken::new_with_token(constants::builtins::NOT, Some(self.shared_token(not_token))); let args = smallvec![expr_node]; Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Call(not_ident, args)), + expr: Expr::Call(not_ident, args), })) } - fn parse_negate(&mut self, minus_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(minus_token)); + fn parse_negate(&mut self, minus_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(minus_token); let expr_token = match self.tokens.next() { Some(t) if t.kind == TokenKind::Eof => { - return Err(SyntaxError::UnexpectedEOFAfterToken((**minus_token).clone())); + return Err(SyntaxError::UnexpectedEOFAfterToken(minus_token.clone())); } Some(t) => t, - None => return Err(SyntaxError::UnexpectedEOFAfterToken((**minus_token).clone())), + None => return Err(SyntaxError::UnexpectedEOFAfterToken(minus_token.clone())), }; if !matches!( @@ -638,17 +657,17 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { | TokenKind::Env(_) | TokenKind::Ident(_) ) { - return Err(SyntaxError::UnexpectedToken((**expr_token).clone())); + return Err(SyntaxError::UnexpectedToken(expr_token.clone())); } let expr_node = self.parse_primary_expr(expr_token)?; let negate_ident = - IdentWithToken::new_with_token(constants::builtins::NEGATE, Some(Shared::clone(minus_token))); + IdentWithToken::new_with_token(constants::builtins::NEGATE, Some(self.shared_token(minus_token))); let args = smallvec![expr_node]; Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Call(negate_ident, args)), + expr: Expr::Call(negate_ident, args), })) } @@ -670,7 +689,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(true) } Some(token) => Err(SyntaxError::ExpectedClosingBrace( - (***token).clone(), + (**token).clone(), Some(Box::new(opening.clone())), )), None => Err(SyntaxError::ExpectedClosingBrace( @@ -684,9 +703,9 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } } - fn parse_dict(&mut self, lbrace_token: &Shared) -> Result, SyntaxError> { - let opening = (**lbrace_token).clone(); - let token_id = self.token_arena.alloc(Shared::clone(lbrace_token)); + fn parse_dict(&mut self, lbrace_token: &Token) -> Result, SyntaxError> { + let opening = lbrace_token.clone(); + let token_id = self.alloc_token(lbrace_token); let mut pairs = SmallVec::new(); let eof_closing_err = |opening: &Token, module_id: ModuleId| { @@ -708,7 +727,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Some(token) if token.kind == TokenKind::Eof => { return Err(SyntaxError::ExpectedClosingBrace( - (***token).clone(), + (**token).clone(), Some(Box::new(opening.clone())), )); } @@ -734,22 +753,22 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let key_node = match &key_token.kind { TokenKind::Ident(name) => Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(key_token)), - expr: Shared::new(Expr::Literal(Literal::Symbol(Ident::new(name)))), + token_id: self.alloc_token(key_token), + expr: Expr::Literal(Literal::Symbol(Ident::new(name))), }), TokenKind::StringLiteral(s) => Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(key_token)), - expr: Shared::new(Expr::Literal(Literal::String(s.clone()))), + token_id: self.alloc_token(key_token), + expr: Expr::Literal(Literal::String(s.clone())), }), _ => { - return Err(SyntaxError::UnexpectedToken((**key_token).clone())); + return Err(SyntaxError::UnexpectedToken(key_token.clone())); } }; // Expect Colon match self.tokens.next() { Some(token) if token.kind == TokenKind::Colon => {} - Some(token) => return Err(SyntaxError::UnexpectedToken((**token).clone())), + Some(token) => return Err(SyntaxError::UnexpectedToken(token.clone())), None => return Err(eof_closing_err(&opening, self.module_id)), } @@ -762,10 +781,10 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { pairs.push(Shared::new(Node { token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::clone(key_token))), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(self.shared_token(key_token))), smallvec![key_node, value_node], - )), + ), })); if self.parse_dict_separator(&opening)? { @@ -775,29 +794,29 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::clone(lbrace_token))), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::DICT, Some(self.shared_token(lbrace_token))), pairs, - )), + ), })) } - fn parse_env(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_env(&mut self, token: &Token) -> Result, SyntaxError> { match &token.kind { TokenKind::Env(s) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), + token_id: self.alloc_token(token), expr: io_context::current() .env_var(s) .map_err(|e| match e { crate::io::IoError::PermissionDenied(_) => { - SyntaxError::EnvNotAllowed((**token).clone(), SmolStr::new(s)) + SyntaxError::EnvNotAllowed(token.clone(), SmolStr::new(s)) } - _ => SyntaxError::EnvNotFound((**token).clone(), SmolStr::new(s)), + _ => SyntaxError::EnvNotFound(token.clone(), SmolStr::new(s)), }) - .map(|s| Shared::new(Expr::Literal(Literal::String(s.to_owned()))))?, + .map(|s| Expr::Literal(Literal::String(s.to_owned())))?, })), TokenKind::Eof => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), - _ => Err(SyntaxError::UnexpectedToken((**token).clone())), + _ => Err(SyntaxError::UnexpectedToken(token.clone())), } } @@ -807,14 +826,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { token_id: TokenId, ) -> Result, SyntaxError> { let selector_token = match self.tokens.peek() { - Some(t) => Shared::clone(t), + Some(t) => *t, None => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }; if let TokenKind::Selector(selector) = &selector_token.kind && selector.len() > 1 { - if !Selector::try_from(&*selector_token) + if !Selector::try_from(selector_token) .map_err(SyntaxError::UnknownSelector)? .is_attribute_selector() { @@ -822,10 +841,10 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } let attribute_name = &selector[1..]; // Skip the leading '.' - let attr_literal_token_id = self.token_arena.alloc(Shared::clone(&selector_token)); + let attr_literal_token_id = self.alloc_token(selector_token); let attr_literal = Shared::new(Node { token_id: attr_literal_token_id, - expr: Shared::new(Expr::Literal(Literal::String(attribute_name.to_string()))), + expr: Expr::Literal(Literal::String(attribute_name.to_string())), }); self.tokens.next(); // Consume selector token @@ -837,24 +856,24 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id: attr_literal_token_id, - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::ATTR, Some(Shared::clone(&self.token_arena[token_id])), ), smallvec![base_node, attr_literal], - )), + ), })) } else { Ok(base_node) } } - fn parse_self(&mut self, token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(token)); + fn parse_self(&mut self, token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(token); let self_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }); let node = self.parse_attribute_access(self_node, token_id)?; @@ -864,8 +883,8 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } } - fn parse_break(&mut self, token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(token)); + fn parse_break(&mut self, token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(token); // Check for colon and expression (break: expr) let value = if self.tokens.peek().map(|t| &t.kind) == Some(&TokenKind::Colon) { @@ -881,21 +900,21 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Break(value)), + expr: Expr::Break(value), })) } - fn parse_continue(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_continue(&mut self, token: &Token) -> Result, SyntaxError> { Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Continue), + token_id: self.alloc_token(token), + expr: Expr::Continue, })) } /// Parses a `...expr` spread element, wrapping it in a `SPREAD` marker call that /// `eval_builtin` expands in place when building the enclosing array/dict. - fn parse_spread_element(&mut self, dots_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(dots_token)); + fn parse_spread_element(&mut self, dots_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(dots_token); let next_token = self .tokens .next() @@ -904,16 +923,16 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SPREAD, Some(Shared::clone(dots_token))), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SPREAD, Some(self.shared_token(dots_token))), smallvec![inner], - )), + ), })) } - fn parse_array(&mut self, token: &Shared) -> Result, SyntaxError> { - let opening = (**token).clone(); - let token_id = self.token_arena.alloc(Shared::clone(token)); + fn parse_array(&mut self, token: &Token) -> Result, SyntaxError> { + let opening = token.clone(); + let token_id = self.alloc_token(token); let mut elements: SmallVec<[Shared; 4]> = SmallVec::new(); let mut closed = false; @@ -925,7 +944,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } TokenKind::Eof => { return Err(SyntaxError::ExpectedClosingBracket( - (**elem_token).clone(), + elem_token.clone(), Some(Box::new(opening)), )); } @@ -953,20 +972,20 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let array_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::clone(token))), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(self.shared_token(token))), elements, - )), + ), }); // Handle postfix bracket access: [1,2,3][0], [1,2,3][0:2], etc. self.parse_postfix_ops(array_node, token) } - fn parse_all_nodes(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_all_nodes(&mut self, token: &Token) -> Result, SyntaxError> { Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Nodes), + token_id: self.alloc_token(token), + expr: Expr::Nodes, })) } @@ -1054,30 +1073,30 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { ) } - fn parse_literal(&mut self, literal_token: &Shared) -> Result, SyntaxError> { + fn parse_literal(&mut self, literal_token: &Token) -> Result, SyntaxError> { let literal_node = match &literal_token.kind { TokenKind::BoolLiteral(b) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(literal_token)), - expr: Shared::new(Expr::Literal(Literal::Bool(*b))), + token_id: self.alloc_token(literal_token), + expr: Expr::Literal(Literal::Bool(*b)), })), TokenKind::StringLiteral(s) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(literal_token)), - expr: Shared::new(Expr::Literal(Literal::String(s.to_owned()))), + token_id: self.alloc_token(literal_token), + expr: Expr::Literal(Literal::String(s.to_owned())), })), TokenKind::BytesLiteral(b) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(literal_token)), - expr: Shared::new(Expr::Literal(Literal::Bytes(b.clone()))), + token_id: self.alloc_token(literal_token), + expr: Expr::Literal(Literal::Bytes(b.clone())), })), TokenKind::NumberLiteral(n) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(literal_token)), - expr: Shared::new(Expr::Literal(Literal::Number(*n))), + token_id: self.alloc_token(literal_token), + expr: Expr::Literal(Literal::Number(*n)), })), TokenKind::None => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(literal_token)), - expr: Shared::new(Expr::Literal(Literal::None)), + token_id: self.alloc_token(literal_token), + expr: Expr::Literal(Literal::None), })), TokenKind::Eof => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), - _ => Err(SyntaxError::UnexpectedToken((**literal_token).clone())), + _ => Err(SyntaxError::UnexpectedToken(literal_token.clone())), }?; let token = self.tokens.peek(); @@ -1085,20 +1104,20 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { if Self::is_next_token_allowed(token.as_ref().map(|t| &t.kind)) { Ok(literal_node) } else { - Err(SyntaxError::UnexpectedToken((***token.unwrap()).clone())) + Err(SyntaxError::UnexpectedToken((*token.unwrap()).clone())) } } - fn parse_ident(&mut self, ident: &str, ident_token: &Shared) -> Result, SyntaxError> { + fn parse_ident(&mut self, ident: &str, ident_token: &Token) -> Result, SyntaxError> { match self.tokens.peek().map(|t| &t.kind) { Some(TokenKind::Selector(selector)) if selector.len() > 1 => { - let token_id = self.token_arena.alloc(Shared::clone(ident_token)); + let token_id = self.alloc_token(ident_token); let base_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + expr: Expr::Ident(IdentWithToken::new_with_token( ident, - Some(Shared::clone(ident_token)), - ))), + Some(self.shared_token(ident_token)), + )), }); self.parse_attribute_access(base_node, token_id) @@ -1106,7 +1125,10 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Some(TokenKind::DoubleColon) => { // Parse qualified access: module::function(), module::ident, or module::module2::method // Build the module path by collecting all identifiers separated by '::' - let mut module_path = vec![IdentWithToken::new_with_token(ident, Some(Shared::clone(ident_token)))]; + let mut module_path = vec![IdentWithToken::new_with_token( + ident, + Some(self.shared_token(ident_token)), + )]; // Collect all module path segments while matches!(self.tokens.peek().map(|t| &t.kind), Some(TokenKind::DoubleColon)) { @@ -1119,7 +1141,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let next_ident = match &next_token.kind { TokenKind::Ident(name) => name.clone(), - _ => return Err(SyntaxError::UnexpectedToken((**next_token).clone())), + _ => return Err(SyntaxError::UnexpectedToken(next_token.clone())), }; // Check if this is the last segment (followed by '(' or not '::') @@ -1128,21 +1150,21 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // More segments to come, add to module path module_path.push(IdentWithToken::new_with_token( &next_ident, - Some(Shared::clone(next_token)), + Some(self.shared_token(next_token)), )); } Some(TokenKind::LParen) => { // This is a function call: module::...::function(args) let args = self.parse_args()?; let access_target = AccessTarget::Call( - IdentWithToken::new_with_token(&next_ident, Some(Shared::clone(next_token))), + IdentWithToken::new_with_token(&next_ident, Some(self.shared_token(next_token))), args, ); - let token_id = self.token_arena.alloc(Shared::clone(ident_token)); + let token_id = self.alloc_token(ident_token); let qualified_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::QualifiedAccess(module_path, access_target)), + expr: Expr::QualifiedAccess(module_path, access_target), }); // Check for bracket access after qualified function call (e.g., module::func()[:1]) if matches!(self.tokens.peek().map(|t| &t.kind), Some(TokenKind::LBracket)) { @@ -1154,24 +1176,24 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // This is an identifier: module::...::ident let access_target = AccessTarget::Ident(IdentWithToken::new_with_token( &next_ident, - Some(Shared::clone(next_token)), + Some(self.shared_token(next_token)), )); - let token_id = self.token_arena.alloc(Shared::clone(ident_token)); + let token_id = self.alloc_token(ident_token); return Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::QualifiedAccess(module_path, access_target)), + expr: Expr::QualifiedAccess(module_path, access_target), })); } } } // This should not be reached, but handle it gracefully - Err(SyntaxError::UnexpectedToken((**ident_token).clone())) + Err(SyntaxError::UnexpectedToken(ident_token.clone())) } Some(TokenKind::LParen) => { let mut args = self.parse_args()?; - let token_id = self.token_arena.alloc(Shared::clone(ident_token)); + let token_id = self.alloc_token(ident_token); // Check for a call with a trailing do-block argument (e.g., foo(args) do ...) if matches!(self.tokens.peek().map(|t| &t.kind), Some(TokenKind::Do)) { @@ -1180,36 +1202,36 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { args.push(block); return Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(ident_token)), - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(ident, Some(Shared::clone(ident_token))), + token_id: self.alloc_token(ident_token), + expr: Expr::Call( + IdentWithToken::new_with_token(ident, Some(self.shared_token(ident_token))), args, - )), + ), })); } let call_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(ident, Some(Shared::clone(ident_token))), + expr: Expr::Call( + IdentWithToken::new_with_token(ident, Some(self.shared_token(ident_token))), args, - )), + ), }); if self.is_next_token(|token_kind| matches!(token_kind, TokenKind::Question)) { let question_token = self.tokens.next().unwrap(); - let question_token_id = self.token_arena.alloc(Shared::clone(question_token)); + let question_token_id = self.alloc_token(question_token); return Ok(Shared::new(Node { token_id: question_token_id, - expr: Shared::new(Expr::Try( + expr: Expr::Try( call_node, None, Shared::new(Node { token_id, - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), - )), + ), })); } @@ -1222,29 +1244,29 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } else if Self::is_next_token_allowed(self.tokens.peek().map(|t| &t.kind)) { Ok(call_node) } else { - Err(SyntaxError::UnexpectedToken((***self.tokens.peek().unwrap()).clone())) + Err(SyntaxError::UnexpectedToken((*self.tokens.peek().unwrap()).clone())) } } Some(TokenKind::LBracket) => { let ident_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(ident_token)), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + token_id: self.alloc_token(ident_token), + expr: Expr::Ident(IdentWithToken::new_with_token( ident, - Some(Shared::clone(ident_token)), - ))), + Some(self.shared_token(ident_token)), + )), }); let result = self.parse_bracket_access(ident_node, ident_token)?; self.parse_postfix_ops(result, ident_token) } token if Self::is_next_token_allowed(token) => Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(ident_token)), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + token_id: self.alloc_token(ident_token), + expr: Expr::Ident(IdentWithToken::new_with_token( ident, - Some(Shared::clone(ident_token)), - ))), + Some(self.shared_token(ident_token)), + )), })), - _ => Err(SyntaxError::UnexpectedToken((**ident_token).clone())), + _ => Err(SyntaxError::UnexpectedToken(ident_token.clone())), } } @@ -1252,9 +1274,9 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { fn parse_bracket_access( &mut self, target_node: Shared, - original_token: &Shared, + original_token: &Token, ) -> Result, SyntaxError> { - let lbracket = self.tokens.next().map(|t| (**t).clone()); // consume '[' + let lbracket = self.tokens.next().cloned(); // consume '[' // Check for [:N] or [:] style slice (empty start index). // [:ident] and [:string] are dict key accesses using a symbol, not slices. @@ -1267,32 +1289,32 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { if is_slice_from_start { let _ = self.tokens.next(); // consume ':' let start_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + token_id: self.alloc_token(original_token), + expr: Expr::Literal(Literal::Number(0.into())), }); let result_node = match self.tokens.next() { Some(t) if t.kind == TokenKind::RBracket => { // [:] = slice(arr, 0, len(arr)) Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( + token_id: self.alloc_token(original_token), + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::SLICE, - Some(Shared::clone(original_token)), + Some(self.shared_token(original_token)), ), smallvec![ Shared::clone(&target_node), start_node, Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( + token_id: self.alloc_token(original_token), + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::LEN, None,), smallvec![target_node], - )), + ), }) ], - )), + ), }) } Some(t) => { @@ -1303,7 +1325,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Some(token) => { return Err(SyntaxError::ExpectedClosingBracket( - (***token).clone(), + (**token).clone(), lbracket.clone().map(Box::new), )); } @@ -1319,14 +1341,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } } Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( + token_id: self.alloc_token(original_token), + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::SLICE, - Some(Shared::clone(original_token)), + Some(self.shared_token(original_token)), ), smallvec![target_node, start_node, end_node], - )), + ), }) } None => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), @@ -1357,21 +1379,24 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match self.tokens.next() { Some(t) if t.kind == TokenKind::RBracket => Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::clone(original_token))), + token_id: self.alloc_token(original_token), + expr: Expr::Call( + IdentWithToken::new_with_token( + constants::builtins::SLICE, + Some(self.shared_token(original_token)), + ), smallvec![ Shared::clone(&target_node), first_node, Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( + token_id: self.alloc_token(original_token), + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::LEN, None), smallvec![target_node], - )), + ), }) ], - )), + ), }), Some(t) => { let second_node = self.parse_expr(t)?; @@ -1383,7 +1408,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Some(token) => { return Err(SyntaxError::ExpectedClosingBracket( - (***token).clone(), + (**token).clone(), lbracket.clone().map(Box::new), )); } @@ -1400,14 +1425,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( + token_id: self.alloc_token(original_token), + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::SLICE, - Some(Shared::clone(original_token)), + Some(self.shared_token(original_token)), ), smallvec![target_node, first_node, second_node], - )), + ), }) } None => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), @@ -1420,7 +1445,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Some(token) => { return Err(SyntaxError::ExpectedClosingBracket( - (***token).clone(), + (**token).clone(), lbracket.clone().map(Box::new), )); } @@ -1437,11 +1462,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::clone(original_token))), + token_id: self.alloc_token(original_token), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(self.shared_token(original_token))), smallvec![target_node, first_node], - )), + ), }) }; @@ -1457,8 +1482,8 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { if matches!(self.tokens.peek().map(|t| &t.kind), Some(TokenKind::LParen)) { let args = self.parse_args()?; let call_dynamic = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::CallDynamic(final_result, args)), + token_id: self.alloc_token(original_token), + expr: Expr::CallDynamic(final_result, args), }); self.parse_postfix_ops(call_dynamic, original_token) } else { @@ -1471,14 +1496,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { fn parse_postfix_ops( &mut self, mut current: Shared, - original_token: &Shared, + original_token: &Token, ) -> Result, SyntaxError> { loop { if matches!(self.tokens.peek().map(|t| &t.kind), Some(TokenKind::LParen)) { let args = self.parse_args()?; current = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(original_token)), - expr: Shared::new(Expr::CallDynamic(current, args)), + token_id: self.alloc_token(original_token), + expr: Expr::CallDynamic(current, args), }); } else if matches!(self.tokens.peek().map(|t| &t.kind), Some(TokenKind::LBracket)) { current = self.parse_bracket_access(current, original_token)?; @@ -1489,16 +1514,16 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(current) } - fn parse_def(&mut self, def_token: &Shared) -> Result, SyntaxError> { + fn parse_def(&mut self, def_token: &Token) -> Result, SyntaxError> { let ident_token = self.tokens.next(); let ident = match &ident_token { Some(token) => match &token.kind { TokenKind::Ident(ident) => Ok(ident), - _ => Err(SyntaxError::UnexpectedToken((***token).clone())), + _ => Err(SyntaxError::UnexpectedToken((**token).clone())), }, None => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }?; - let def_token_id = self.token_arena.alloc(Shared::clone(def_token)); + let def_token_id = self.alloc_token(def_token); let params = if self.is_next_token(|token| matches!(token, TokenKind::Colon | TokenKind::Do)) { SmallVec::new() } else { @@ -1511,16 +1536,16 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id: def_token_id, - expr: Shared::new(Expr::Def( - IdentWithToken::new_with_token(ident, ident_token.map(Shared::clone)), + expr: Expr::Def( + IdentWithToken::new_with_token(ident, ident_token.map(|token| self.shared_token(token))), params, program, - )), + ), })) } - fn parse_block(&mut self, do_token: &Shared) -> Result, SyntaxError> { - let do_token_id = self.token_arena.alloc(Shared::clone(do_token)); + fn parse_block(&mut self, do_token: &Token) -> Result, SyntaxError> { + let do_token_id = self.alloc_token(do_token); let program = self.parse_program(false)?; // The End token is already consumed by parse_program when it encounters it @@ -1528,12 +1553,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id: do_token_id, - expr: Shared::new(Expr::Block(program)), + expr: Expr::Block(program), })) } - fn parse_fn(&mut self, fn_token: &Shared) -> Result, SyntaxError> { - let fn_token_id = self.token_arena.alloc(Shared::clone(fn_token)); + fn parse_fn(&mut self, fn_token: &Token) -> Result, SyntaxError> { + let fn_token_id = self.alloc_token(fn_token); let params = self.parse_params()?; self.consume_colon_or_do(); @@ -1542,19 +1567,19 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let fn_node = Shared::new(Node { token_id: fn_token_id, - expr: Shared::new(Expr::Fn(params, program)), + expr: Expr::Fn(params, program), }); // Handle postfix operations: fn(...): ... end(args), fn(...): ... end(args)[N], etc. self.parse_postfix_ops(fn_node, fn_token) } - fn parse_while(&mut self, while_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(while_token)); + fn parse_while(&mut self, while_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(while_token); let args = self.parse_args()?; if args.len() != 1 { - return Err(SyntaxError::UnexpectedToken((**while_token).clone())); + return Err(SyntaxError::UnexpectedToken(while_token.clone())); } self.consume_colon_or_do(); @@ -1566,15 +1591,15 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::While(Shared::clone(cond), body_program)), + expr: Expr::While(Shared::clone(cond), body_program), })) } - None => Err(SyntaxError::UnexpectedToken((**while_token).clone())), + None => Err(SyntaxError::UnexpectedToken(while_token.clone())), } } - fn parse_loop(&mut self, loop_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(loop_token)); + fn parse_loop(&mut self, loop_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(loop_token); self.consume_colon_or_do(); @@ -1584,19 +1609,19 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Loop(body_program)), + expr: Expr::Loop(body_program), })) } - None => Err(SyntaxError::UnexpectedToken((**loop_token).clone())), + None => Err(SyntaxError::UnexpectedToken(loop_token.clone())), } } - fn parse_until(&mut self, until_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(until_token)); + fn parse_until(&mut self, until_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(until_token); let args = self.parse_args()?; if args.len() != 1 { - return Err(SyntaxError::UnexpectedToken((**until_token).clone())); + return Err(SyntaxError::UnexpectedToken(until_token.clone())); } self.consume_colon_or_do(); @@ -1608,15 +1633,15 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Until(Shared::clone(cond), body_program)), + expr: Expr::Until(Shared::clone(cond), body_program), })) } - None => Err(SyntaxError::UnexpectedToken((**until_token).clone())), + None => Err(SyntaxError::UnexpectedToken(until_token.clone())), } } - fn parse_unless(&mut self, unless_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(unless_token)); + fn parse_unless(&mut self, unless_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(unless_token); let args = self.parse_args()?; if args.len() != 1 { @@ -1632,13 +1657,13 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { branches.push((Some(Shared::clone(cond)), then_expr)); Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(unless_token)), - expr: Shared::new(Expr::Unless(branches)), + token_id: self.alloc_token(unless_token), + expr: Expr::Unless(branches), })) } - fn parse_try(&mut self, try_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(try_token)); + fn parse_try(&mut self, try_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(try_token); self.consume_colon_or_do(); @@ -1651,14 +1676,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { if !self.is_next_token(|token_kind| matches!(token_kind, TokenKind::Catch)) { return Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Try( + expr: Expr::Try( try_expr, None, Shared::new(Node { token_id, - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), - )), + ), })); } @@ -1669,7 +1694,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let error_binder = if self.is_next_token(|token_kind| matches!(token_kind, TokenKind::LParen)) { let args = self.parse_args()?; match args.as_slice() { - [arg] => match &*arg.expr { + [arg] => match &arg.expr { Expr::Ident(ident) => Some(ident.clone()), _ => return Err(SyntaxError::UnexpectedToken((*self.token_arena[arg.token_id]).clone())), }, @@ -1693,18 +1718,18 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Try(try_expr, error_binder, catch_expr)), + expr: Expr::Try(try_expr, error_binder, catch_expr), })) } - fn parse_foreach(&mut self, foreach_token: &Shared) -> Result, SyntaxError> { + fn parse_foreach(&mut self, foreach_token: &Token) -> Result, SyntaxError> { let args = self.parse_args()?; if args.len() != 2 { - return Err(SyntaxError::UnexpectedToken((**foreach_token).clone())); + return Err(SyntaxError::UnexpectedToken(foreach_token.clone())); } - let first_arg = &*args.first().unwrap().expr; + let first_arg = &args.first().unwrap().expr; match first_arg { Expr::Ident(IdentWithToken { @@ -1717,23 +1742,23 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let body_program = self.parse_program(false)?; Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(foreach_token)), - expr: Shared::new(Expr::Foreach( + token_id: self.alloc_token(foreach_token), + expr: Expr::Foreach( IdentWithToken { name: *ident, token: ident_token.clone(), }, Shared::clone(&each_values), body_program, - )), + ), })) } - _ => Err(SyntaxError::UnexpectedToken((**foreach_token).clone())), + _ => Err(SyntaxError::UnexpectedToken(foreach_token.clone())), } } - fn parse_if(&mut self, if_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(if_token)); + fn parse_if(&mut self, if_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(if_token); let args = self.parse_args()?; if args.len() != 1 { @@ -1763,13 +1788,13 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(if_token)), - expr: Shared::new(Expr::If(branches)), + token_id: self.alloc_token(if_token), + expr: Expr::If(branches), })) } - fn parse_match(&mut self, match_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(match_token)); + fn parse_match(&mut self, match_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(match_token); // Parse the value expression: match (value): let args = self.parse_args()?; @@ -1796,7 +1821,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Check for guard (if condition) let guard = if let Some(token) = self.tokens.peek() { if matches!(token.kind, TokenKind::If) { - let if_token = Shared::clone(token); + let if_token = Shared::new((*token).clone()); self.tokens.next(); // consume 'if' let guard_args = self.parse_args()?; if guard_args.len() != 1 { @@ -1827,7 +1852,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Match(value, arms)), + expr: Expr::Match(value, arms), })) } @@ -1861,7 +1886,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }; match &type_token.kind { TokenKind::Ident(type_name) => Ok(Pattern::Type(Ident::new(type_name))), - _ => Err(SyntaxError::UnexpectedToken((**type_token).clone())), + _ => Err(SyntaxError::UnexpectedToken(type_token.clone())), } } // Literal patterns @@ -1876,7 +1901,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::LBrace => self.parse_dict_pattern(), // Identifier pattern (binding) TokenKind::Ident(name) => Ok(Pattern::Ident(IdentWithToken::new(name))), - _ => Err(SyntaxError::UnexpectedToken((**token).clone())), + _ => Err(SyntaxError::UnexpectedToken(token.clone())), } } @@ -1901,14 +1926,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { rest_binding = Some(IdentWithToken::new(name)); has_rest = true; } else { - return Err(SyntaxError::UnexpectedToken((**ident_token).clone())); + return Err(SyntaxError::UnexpectedToken(ident_token.clone())); } } // Expect closing bracket after rest if let Some(token) = self.tokens.next() && !matches!(token.kind, TokenKind::RBracket) { - return Err(SyntaxError::UnexpectedToken((**token).clone())); + return Err(SyntaxError::UnexpectedToken(token.clone())); } break; } @@ -1925,7 +1950,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Will be consumed in next iteration continue; } else { - return Err(SyntaxError::UnexpectedToken((***token).clone())); + return Err(SyntaxError::UnexpectedToken((**token).clone())); } } } @@ -1957,7 +1982,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let key = match &key_token.kind { TokenKind::Ident(name) => IdentWithToken::new(name), - _ => return Err(SyntaxError::UnexpectedToken((**key_token).clone())), + _ => return Err(SyntaxError::UnexpectedToken(key_token.clone())), }; // Check if there's a colon (key: pattern) or just key shorthand @@ -1983,7 +2008,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Will be consumed in next iteration continue; } else { - return Err(SyntaxError::UnexpectedToken((***token).clone())); + return Err(SyntaxError::UnexpectedToken((**token).clone())); } } } @@ -2051,18 +2076,18 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let ident_token = self.tokens.next().unwrap(); Ok(Pattern::Ident(IdentWithToken::new_with_token( name, - Some(Shared::clone(ident_token)), + Some(self.shared_token(ident_token)), ))) } _ => { let bad_token = self.tokens.next().unwrap(); - Err(SyntaxError::UnexpectedToken((**bad_token).clone())) + Err(SyntaxError::UnexpectedToken(bad_token.clone())) } } } - fn parse_let(&mut self, let_token: &Shared) -> Result, SyntaxError> { - let let_token_id = self.token_arena.alloc(Shared::clone(let_token)); + fn parse_let(&mut self, let_token: &Token) -> Result, SyntaxError> { + let let_token_id = self.alloc_token(let_token); let pattern = self.parse_let_or_var_pattern()?; self.next_token(|token_kind| matches!(token_kind, TokenKind::Equal))?; @@ -2072,7 +2097,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }?; if matches!(expr_token.kind, TokenKind::Let | TokenKind::Var) { - return Err(SyntaxError::UnexpectedToken((**expr_token).clone())); + return Err(SyntaxError::UnexpectedToken(expr_token.clone())); } let ast = self.parse_expr(expr_token)?; @@ -2083,17 +2108,17 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::Pipe | TokenKind::Eof | TokenKind::SemiColon | TokenKind::End ) { - return Err(SyntaxError::UnexpectedToken((***token).clone())); + return Err(SyntaxError::UnexpectedToken((**token).clone())); } Ok(Shared::new(Node { token_id: let_token_id, - expr: Shared::new(Expr::Let(pattern, ast)), + expr: Expr::Let(pattern, ast), })) } - fn parse_var(&mut self, var_token: &Shared) -> Result, SyntaxError> { - let var_token_id = self.token_arena.alloc(Shared::clone(var_token)); + fn parse_var(&mut self, var_token: &Token) -> Result, SyntaxError> { + let var_token_id = self.alloc_token(var_token); let pattern = self.parse_let_or_var_pattern()?; self.next_token(|token_kind| matches!(token_kind, TokenKind::Equal))?; @@ -2103,7 +2128,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }?; if matches!(expr_token.kind, TokenKind::Let | TokenKind::Var) { - return Err(SyntaxError::UnexpectedToken((**expr_token).clone())); + return Err(SyntaxError::UnexpectedToken(expr_token.clone())); } let ast = self.parse_expr(expr_token)?; @@ -2114,37 +2139,37 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::Pipe | TokenKind::Eof | TokenKind::SemiColon | TokenKind::End ) { - return Err(SyntaxError::UnexpectedToken((***token).clone())); + return Err(SyntaxError::UnexpectedToken((**token).clone())); } Ok(Shared::new(Node { token_id: var_token_id, - expr: Shared::new(Expr::Var(pattern, ast)), + expr: Expr::Var(pattern, ast), })) } #[inline(always)] - fn parse_include(&mut self, include_token: &Shared) -> Result, SyntaxError> { + fn parse_include(&mut self, include_token: &Token) -> Result, SyntaxError> { match self.tokens.peek() { Some(token) => match &token.kind { TokenKind::StringLiteral(module) => { self.tokens.next(); Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(include_token)), - expr: Shared::new(Expr::Include(Literal::String(module.to_owned()))), + token_id: self.alloc_token(include_token), + expr: Expr::Include(Literal::String(module.to_owned())), })) } - _ => Err(SyntaxError::InsufficientTokens((***token).clone())), + _ => Err(SyntaxError::InsufficientTokens((**token).clone())), }, None => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), } } #[inline(always)] - fn parse_import(&mut self, import_token: &Shared) -> Result, SyntaxError> { - let token_id = self.token_arena.alloc(Shared::clone(import_token)); + fn parse_import(&mut self, import_token: &Token) -> Result, SyntaxError> { + let token_id = self.alloc_token(import_token); let token = match self.tokens.next() { - Some(token) => Ok(Shared::clone(token)), + Some(token) => Ok(self.shared_token(token)), None => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }?; @@ -2161,10 +2186,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }; match &name_token.kind { - TokenKind::Ident(name) => { - Some(IdentWithToken::new_with_token(name, Some(Shared::clone(name_token)))) - } - _ => return Err(SyntaxError::UnexpectedToken((**name_token).clone())), + TokenKind::Ident(name) => Some(IdentWithToken::new_with_token( + name, + Some(self.shared_token(name_token)), + )), + _ => return Err(SyntaxError::UnexpectedToken(name_token.clone())), } } else { None @@ -2172,14 +2198,14 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Import(Literal::String(module_name), alias)), + expr: Expr::Import(Literal::String(module_name), alias), })) } _ => Err(SyntaxError::InsufficientTokens((*token).clone())), } } - fn parse_interpolated_string(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_interpolated_string(&mut self, token: &Token) -> Result, SyntaxError> { if let TokenKind::InterpolatedString(segments) = &token.kind { let mut parsed_segments = Vec::new(); @@ -2209,8 +2235,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }) })?; - let shared_tokens: Vec> = tokens.into_iter().map(Shared::new).collect(); - let mut parser = Parser::new(shared_tokens.iter(), self.token_arena, token.module_id); + let mut parser = Parser::new(tokens.iter(), self.token_arena, token.module_id); let expr_node = parser.parse_expr_from_tokens().map_err(|_| { SyntaxError::UnexpectedToken(Token { range: *range, @@ -2226,11 +2251,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::InterpolatedString(parsed_segments)), + token_id: self.alloc_token(token), + expr: Expr::InterpolatedString(parsed_segments), })) } else { - Err(SyntaxError::UnexpectedToken((**token).clone())) + Err(SyntaxError::UnexpectedToken(token.clone())) } } @@ -2248,11 +2273,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let opening_paren = match self.tokens.peek() { Some(token) => match &token.kind { TokenKind::LParen => { - let t = (***token).clone(); + let t = (**token).clone(); self.tokens.next(); Some(t) } - _ => return Err(SyntaxError::UnexpectedToken((***token).clone())), + _ => return Err(SyntaxError::UnexpectedToken((**token).clone())), }, None => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }; @@ -2266,7 +2291,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match &token.kind { TokenKind::RParen => match prev_token { Some(TokenKind::Comma) => { - return Err(SyntaxError::UnexpectedToken((**token).clone())); + return Err(SyntaxError::UnexpectedToken(token.clone())); } _ => break, }, @@ -2274,7 +2299,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Some(TokenKind::RParen) => break, Some(_) | None => { return Err(SyntaxError::ExpectedClosingParen( - (**token).clone(), + token.clone(), opening_paren.clone().map(Box::new), )); } @@ -2282,7 +2307,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::Comma => match prev_token { Some(_) => { let token = match self.tokens.peek() { - Some(token) => Ok(Shared::clone(token)), + Some(token) => Ok(Shared::new((*token).clone())), None => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }?; match &token.kind { @@ -2292,13 +2317,13 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { _ => continue, } } - None => return Err(SyntaxError::UnexpectedToken((**token).clone())), + None => return Err(SyntaxError::UnexpectedToken(token.clone())), }, TokenKind::SemiColon => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), TokenKind::Asterisk => { // Variadic parameter: *name if seen_variadic { - return Err(SyntaxError::MultipleVariadicParameters((**token).clone())); + return Err(SyntaxError::MultipleVariadicParameters(token.clone())); } let ident_token = match self.tokens.next() { Some(t) => t, @@ -2306,23 +2331,23 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { }; match &ident_token.kind { TokenKind::Ident(name) => { - let ident = IdentWithToken::new_with_token(name, Some(Shared::clone(ident_token))); + let ident = IdentWithToken::new_with_token(name, Some(self.shared_token(ident_token))); params.push(Param::variadic(ident)); seen_variadic = true; } _ => { - return Err(SyntaxError::UnexpectedToken((**ident_token).clone())); + return Err(SyntaxError::UnexpectedToken(ident_token.clone())); } } } TokenKind::Ident(name) => { // Non-variadic param after variadic is an error if seen_variadic { - return Err(SyntaxError::VariadicParameterMustBeLast((**token).clone())); + return Err(SyntaxError::VariadicParameterMustBeLast(token.clone())); } // Parse parameter name - let ident = IdentWithToken::new_with_token(name, Some(Shared::clone(token))); + let ident = IdentWithToken::new_with_token(name, Some(self.shared_token(token))); // Check for '=' indicating a default value let default = if let Some(next_token) = self.tokens.peek() @@ -2340,7 +2365,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Some(self.parse_expr(default_token)?) } else { if seen_default { - return Err(SyntaxError::ParameterWithoutDefaultAfterDefault((**token).clone())); + return Err(SyntaxError::ParameterWithoutDefaultAfterDefault(token.clone())); } None }; @@ -2348,7 +2373,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { params.push(Param::with_default(ident, default)); } _ => { - return Err(SyntaxError::UnexpectedToken((**token).clone())); + return Err(SyntaxError::UnexpectedToken(token.clone())); } } @@ -2358,7 +2383,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { && !matches!(token.kind, TokenKind::RParen | TokenKind::Comma) { return Err(SyntaxError::ExpectedClosingParen( - (***token).clone(), + (**token).clone(), opening_paren.clone().map(Box::new), )); } @@ -2371,11 +2396,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let opening_paren = match self.tokens.peek() { Some(token) => match &token.kind { TokenKind::LParen => { - let t = (***token).clone(); + let t = (**token).clone(); self.tokens.next(); Some(t) } - _ => return Err(SyntaxError::UnexpectedToken((***token).clone())), + _ => return Err(SyntaxError::UnexpectedToken((**token).clone())), }, None => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }; @@ -2387,7 +2412,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { match &token.kind { TokenKind::RParen => match prev_token { Some(TokenKind::Comma) => { - return Err(SyntaxError::UnexpectedToken((**token).clone())); + return Err(SyntaxError::UnexpectedToken(token.clone())); } _ => break, }, @@ -2395,7 +2420,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Some(TokenKind::RParen) => break, Some(_) | None => { return Err(SyntaxError::ExpectedClosingParen( - (**token).clone(), + token.clone(), opening_paren.clone().map(Box::new), )); } @@ -2403,7 +2428,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { TokenKind::Comma => match prev_token { Some(_) => { let token = match self.tokens.peek() { - Some(token) => Ok(Shared::clone(token)), + Some(token) => Ok(Shared::new((*token).clone())), None => Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }?; match &token.kind { @@ -2413,7 +2438,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { _ => continue, } } - None => return Err(SyntaxError::UnexpectedToken((**token).clone())), + None => return Err(SyntaxError::UnexpectedToken(token.clone())), }, TokenKind::SemiColon => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), _ => { @@ -2428,7 +2453,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { && !matches!(token.kind, TokenKind::RParen | TokenKind::Comma) { return Err(SyntaxError::ExpectedClosingParen( - (***token).clone(), + (**token).clone(), opening_paren.clone().map(Box::new), )); } @@ -2440,7 +2465,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Helper to parse an argument that is expected to be a general expression. // This typically involves a recursive call to `parse_expr`. #[inline(always)] - fn parse_arg_expr(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_arg_expr(&mut self, token: &Token) -> Result, SyntaxError> { let first = self.parse_expr(token)?; if !self.is_next_token(|kind| matches!(kind, TokenKind::Pipe)) { return Ok(first); @@ -2452,17 +2477,17 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let pipe_token = self.tokens.next().unwrap(); let next_token = match self.tokens.next() { Some(token) if token.kind == TokenKind::Eof => { - return Err(SyntaxError::UnexpectedEOFAfterToken((**pipe_token).clone())); + return Err(SyntaxError::UnexpectedEOFAfterToken(pipe_token.clone())); } Some(token) => token, - None => return Err(SyntaxError::UnexpectedEOFAfterToken((**pipe_token).clone())), + None => return Err(SyntaxError::UnexpectedEOFAfterToken(pipe_token.clone())), }; program.push(self.parse_expr(next_token)?); } Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Block(program)), + expr: Expr::Block(program), })) } @@ -2479,11 +2504,11 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Create the set_attr() function call Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(Shared::clone(token))), + token_id: self.alloc_token(token), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(self.shared_token(token))), smallvec![selector_node, attr_literal, value], - )), + ), })) } @@ -2491,21 +2516,21 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { fn build_attr_call_for_node( &mut self, base_node: Shared, - attr_token: Shared, - token: &Shared, + attr_token: &Token, + token: &Token, ) -> Result, SyntaxError> { if let TokenKind::Selector(attr_selector) = &attr_token.kind { - if !Selector::try_from(&*attr_token) + if !Selector::try_from(attr_token) .map_err(SyntaxError::UnknownSelector)? .is_attribute_selector() { - return Err(SyntaxError::UnexpectedToken((*attr_token).clone())); + return Err(SyntaxError::UnexpectedToken(attr_token.clone())); } let attribute = &attr_selector[1..]; // Skip the dot let attr_literal = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Literal(Literal::String(attribute.to_string()))), + token_id: self.alloc_token(token), + expr: Expr::Literal(Literal::String(attribute.to_string())), }); if self.is_next_token(|kind| matches!(kind, TokenKind::PipeEqual)) { @@ -2514,28 +2539,24 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::clone(token))), + token_id: self.alloc_token(token), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ATTR, Some(self.shared_token(token))), smallvec![base_node, attr_literal], - )), + ), })) } else { - Err(SyntaxError::UnexpectedToken((**token).clone())) + Err(SyntaxError::UnexpectedToken(token.clone())) } } /// Consumes any selector token(s) following an already-parsed `base_node`. - fn parse_selector_tail( - &mut self, - token: &Shared, - base_node: Shared, - ) -> Result, SyntaxError> { + fn parse_selector_tail(&mut self, token: &Token, base_node: Shared) -> Result, SyntaxError> { if !self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_))) { return Ok(base_node); } - let next_token = Shared::clone(self.tokens.next().unwrap()); - let selector = Selector::try_from(&*next_token).map_err(SyntaxError::UnknownSelector)?; + let next_token = self.tokens.next().unwrap(); + let selector = Selector::try_from(next_token).map_err(SyntaxError::UnknownSelector)?; if selector.is_attribute_selector() { return self.build_attr_call_for_node(base_node, next_token, token); @@ -2548,8 +2569,8 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { loop { nodes.push(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Selector(Selector::Recursive)), + token_id: self.alloc_token(token), + expr: Expr::Selector(Selector::Recursive), })); let step_expr = if self.is_next_token(|kind| matches!(kind, TokenKind::LParen)) { @@ -2558,20 +2579,20 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Expr::Selector(step_selector) }; nodes.push(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(&step_token)), - expr: Shared::new(step_expr), + token_id: self.alloc_token(step_token), + expr: step_expr, })); if !self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_))) { break; } - let peeked_token = Shared::clone(self.tokens.next().unwrap()); - let peeked_selector = Selector::try_from(&*peeked_token).map_err(SyntaxError::UnknownSelector)?; + let peeked_token = self.tokens.next().unwrap(); + let peeked_selector = Selector::try_from(peeked_token).map_err(SyntaxError::UnknownSelector)?; if peeked_selector.is_attribute_selector() { let chained = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Block(nodes)), + token_id: self.alloc_token(token), + expr: Expr::Block(nodes), }); return self.build_attr_call_for_node(chained, peeked_token, token); } @@ -2581,36 +2602,36 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Block(nodes)), + token_id: self.alloc_token(token), + expr: Expr::Block(nodes), })) } /// Parse a selector without checking for attributes (to avoid infinite recursion) - fn parse_selector_direct(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_selector_direct(&mut self, token: &Token) -> Result, SyntaxError> { match &token.kind { TokenKind::Selector(selector) => { if selector == "." { if self.is_next_token(|token_kind| matches!(token_kind, TokenKind::LBracket)) { - self.parse_selector_table_args(Shared::clone(token)) + self.parse_selector_table_args(token) } else { Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Self_), + token_id: self.alloc_token(token), + expr: Expr::Self_, })) } } else { - let selector = Selector::try_from(&**token).map_err(SyntaxError::UnknownSelector)?; + let selector = Selector::try_from(token).map_err(SyntaxError::UnknownSelector)?; if selector.is_attribute_selector() { - let token_id = self.token_arena.alloc(Shared::clone(token)); + let token_id = self.alloc_token(token); let self_node = Shared::new(Node { token_id, - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }); let attr_literal = Shared::new(Node { token_id, - expr: Shared::new(Expr::Literal(Literal::String(selector.name()))), + expr: Expr::Literal(Literal::String(selector.name())), }); if self.is_next_token(|kind| matches!(kind, TokenKind::PipeEqual)) { self.tokens.next(); @@ -2618,10 +2639,13 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } return Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::clone(token))), + expr: Expr::Call( + IdentWithToken::new_with_token( + constants::builtins::ATTR, + Some(self.shared_token(token)), + ), smallvec![self_node, attr_literal], - )), + ), })); } @@ -2630,22 +2654,22 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { && self.is_next_token(|kind| matches!(kind, TokenKind::LBracket)) { let prop_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Selector(selector)), + token_id: self.alloc_token(token), + expr: Expr::Selector(selector), }); return self.parse_property_iterator(token, vec![prop_node]); } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Selector(selector)), + token_id: self.alloc_token(token), + expr: Expr::Selector(selector), })) } } TokenKind::DoubleDot => { let recursive_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Selector(Selector::Recursive)), + token_id: self.alloc_token(token), + expr: Expr::Selector(Selector::Recursive), }); if let Some(next) = self.tokens.peek() { match &next.kind { @@ -2654,12 +2678,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let key = key.clone(); let next = self.tokens.next().unwrap(); let prop_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(next)), - expr: Shared::new(Expr::Selector(Selector::Property(Ident::new(key.as_str())))), + token_id: self.alloc_token(next), + expr: Expr::Selector(Selector::Property(Ident::new(key.as_str()))), }); return Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Block(vec![recursive_node, prop_node])), + token_id: self.alloc_token(token), + expr: Expr::Block(vec![recursive_node, prop_node]), })); } // ..text, ..h, ..code, etc. → recursive descent + Markdown node-type selector @@ -2672,13 +2696,13 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { { let next = self.tokens.next().unwrap(); let sel_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(next)), - expr: Shared::new(Expr::Selector(selector)), + token_id: self.alloc_token(next), + expr: Expr::Selector(selector), }); return Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Block(vec![recursive_node, sel_node])), + token_id: self.alloc_token(token), + expr: Expr::Block(vec![recursive_node, sel_node]), })); } } @@ -2687,19 +2711,19 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(recursive_node) } - _ => Err(SyntaxError::InsufficientTokens((**token).clone())), + _ => Err(SyntaxError::InsufficientTokens(token.clone())), } } - fn parse_selector(&mut self, token: &Shared) -> Result, SyntaxError> { + fn parse_selector(&mut self, token: &Token) -> Result, SyntaxError> { // Handle chained property access: .a.b.c → Block([Selector(Property("a")), ...]) if let TokenKind::Selector(_) = &token.kind - && matches!(Selector::try_from(&**token), Ok(Selector::Property(_))) + && matches!(Selector::try_from(token), Ok(Selector::Property(_))) { let next_is_property = self .tokens .peek() - .is_some_and(|t| matches!(Selector::try_from(&***t), Ok(Selector::Property(_)))); + .is_some_and(|t| matches!(Selector::try_from(*t), Ok(Selector::Property(_)))); if next_is_property { return self.parse_chained_property_selectors(token); } @@ -2715,12 +2739,12 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { && let TokenKind::Selector(s) = &token.kind && s != "." { - let selector = Selector::try_from(&**token).map_err(SyntaxError::UnknownSelector)?; + let selector = Selector::try_from(token).map_err(SyntaxError::UnknownSelector)?; if !selector.is_attribute_selector() { let args = self.parse_args()?; let base_node = Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::SelectorCall(selector, args)), + token_id: self.alloc_token(token), + expr: Expr::SelectorCall(selector, args), }); // Check for attribute access or a descendant chain continuation: `.h(1).level`, `.h(1) .code` return self.parse_selector_tail(token, base_node); @@ -2730,26 +2754,26 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { self.parse_selector_direct(token) } - fn parse_chained_property_selectors(&mut self, first_token: &Shared) -> Result, SyntaxError> { - let first_sel = Selector::try_from(&**first_token).map_err(SyntaxError::UnknownSelector)?; + fn parse_chained_property_selectors(&mut self, first_token: &Token) -> Result, SyntaxError> { + let first_sel = Selector::try_from(first_token).map_err(SyntaxError::UnknownSelector)?; let mut nodes: Program = vec![Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(first_token)), - expr: Shared::new(Expr::Selector(first_sel)), + token_id: self.alloc_token(first_token), + expr: Expr::Selector(first_sel), })]; while self.is_next_token(|kind| matches!(kind, TokenKind::Selector(_))) { let next_is_property = self .tokens .peek() - .is_some_and(|t| matches!(Selector::try_from(&***t), Ok(Selector::Property(_)))); + .is_some_and(|t| matches!(Selector::try_from(*t), Ok(Selector::Property(_)))); if !next_is_property { break; } let next_token = self.tokens.next().unwrap(); - let sel = Selector::try_from(&**next_token).map_err(SyntaxError::UnknownSelector)?; + let sel = Selector::try_from(next_token).map_err(SyntaxError::UnknownSelector)?; nodes.push(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(next_token)), - expr: Shared::new(Expr::Selector(sel)), + token_id: self.alloc_token(next_token), + expr: Expr::Selector(sel), })); } @@ -2759,54 +2783,47 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(first_token)), - expr: Shared::new(Expr::Block(nodes)), + token_id: self.alloc_token(first_token), + expr: Expr::Block(nodes), })) } - fn parse_property_iterator( - &mut self, - token: &Shared, - mut nodes: Program, - ) -> Result, SyntaxError> { + fn parse_property_iterator(&mut self, token: &Token, mut nodes: Program) -> Result, SyntaxError> { let index = self.parse_bracket_expr()?; let list_selector = match &index { None => Selector::List(None, None), Some(node) => { - if let Expr::Literal(Literal::Number(num)) = &*node.expr { + if let Expr::Literal(Literal::Number(num)) = &node.expr { Selector::List(Some(num.value() as usize), None) } else { // Dynamic index expression: emit a SelectorCall so the index is evaluated at runtime - let token_id = self.token_arena.alloc(Shared::clone(token)); + let token_id = self.alloc_token(token); nodes.push(Shared::new(Node { token_id, - expr: Shared::new(Expr::SelectorCall( - Selector::List(None, None), - smallvec![Shared::clone(node)], - )), + expr: Expr::SelectorCall(Selector::List(None, None), smallvec![Shared::clone(node)]), })); return Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Block(nodes)), + token_id: self.alloc_token(token), + expr: Expr::Block(nodes), })); } } }; nodes.push(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Selector(list_selector)), + token_id: self.alloc_token(token), + expr: Expr::Selector(list_selector), })); Ok(Shared::new(Node { - token_id: self.token_arena.alloc(Shared::clone(token)), - expr: Shared::new(Expr::Block(nodes)), + token_id: self.alloc_token(token), + expr: Expr::Block(nodes), })) } // Parses arguments for table or list item selectors like `.[index1][index2]` (for tables) or `.[index1]` (for lists). - fn parse_selector_table_args(&mut self, token: Shared) -> Result, SyntaxError> { + fn parse_selector_table_args(&mut self, token: &Token) -> Result, SyntaxError> { let first = self.parse_bracket_expr()?; let has_second = self.is_next_token(|kind| matches!(kind, TokenKind::LBracket)); let second = if has_second { @@ -2817,7 +2834,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { let is_dynamic_node = |opt: &Option>| { opt.as_ref() - .is_some_and(|n| !matches!(&*n.expr, Expr::Literal(Literal::Number(_)))) + .is_some_and(|n| !matches!(&n.expr, Expr::Literal(Literal::Number(_)))) }; let has_dynamic = is_dynamic_node(&first) || second.as_ref().is_some_and(is_dynamic_node); let has_explicit_args = self.is_next_token(|kind| matches!(kind, TokenKind::LParen)); @@ -2834,10 +2851,10 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // .[][v]: insert None as row placeholder so args[0]=row, args[1]=col positional encoding holds. if is_table && first.is_none() && second.as_ref().is_some_and(|s| s.is_some()) { - let placeholder_token_id = self.token_arena.alloc(Shared::clone(&token)); + let placeholder_token_id = self.alloc_token(token); args.push(Shared::new(Node { token_id: placeholder_token_id, - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), })); } else if let Some(node) = first { args.push(node); @@ -2850,16 +2867,16 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { args.extend(self.parse_args()?); } - let token_id = self.token_arena.alloc(Shared::clone(&token)); + let token_id = self.alloc_token(token); return Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::SelectorCall(selector, args)), + expr: Expr::SelectorCall(selector, args), })); } let static_index = |opt: Option>| -> Option { opt.and_then(|n| { - if let Expr::Literal(Literal::Number(num)) = &*n.expr { + if let Expr::Literal(Literal::Number(num)) = &n.expr { Some(num.value() as usize) } else { None @@ -2872,16 +2889,16 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { Some(opt) => Selector::Table(i1, static_index(opt)), }; - let token_id = self.token_arena.alloc(Shared::clone(&token)); + let token_id = self.alloc_token(token); Ok(Shared::new(Node { token_id, - expr: Shared::new(Expr::Selector(selector)), + expr: Expr::Selector(selector), })) } fn parse_bracket_expr(&mut self) -> Result>, SyntaxError> { let bracket_token = match self.tokens.peek() { - Some(t) => Shared::clone(t), + Some(t) => (*t).clone(), None => return Err(SyntaxError::UnexpectedEOFDetected(self.module_id)), }; self.next_token(|kind| matches!(kind, TokenKind::LBracket))?; @@ -2892,10 +2909,10 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } let expr_token = match self.tokens.next() { - Some(t) => Shared::clone(t), - None => return Err(SyntaxError::InsufficientTokens((*bracket_token).clone())), + Some(t) => t, + None => return Err(SyntaxError::InsufficientTokens(bracket_token.clone())), }; - let node = self.parse_expr(&expr_token)?; + let node = self.parse_expr(expr_token)?; self.next_token(|kind| matches!(kind, TokenKind::RBracket))?; Ok(Some(node)) } @@ -2905,7 +2922,7 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { // Token found and matches one of the expected kinds. Some(token) if expected_kinds(&token.kind) => { let token = self.tokens.next().unwrap(); - Ok(self.token_arena.alloc(Shared::clone(token))) + Ok(self.alloc_token(token)) } // Consume and return. // Token found but does not match expected kinds. Some(token) => Err(SyntaxError::UnexpectedToken(Token { @@ -2977,31 +2994,31 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("and", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("and")))))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("contains", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("contains")))))), smallvec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("test".to_owned()))) + expr: Expr::Literal(Literal::String("test".to_owned())) })], - )) + ) }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("startswith", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("startswith")))))), smallvec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("test2".to_owned()))) + expr: Expr::Literal(Literal::String("test2".to_owned())) })], - )) + ) }) ], - )) + ) }) ]))] #[case::ident2( @@ -3022,19 +3039,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("and", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("and")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(Some(1)))), + expr: Expr::Selector(Selector::Heading(Some(1))), }), Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Selector(Selector::Table(Some(2), None))), + expr: Expr::Selector(Selector::Table(Some(2), None)), }), ], - )) + ) }) ]))] #[case::ident3( @@ -3057,7 +3074,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("filter", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("filter")))))), smallvec![ Param::new(IdentWithToken::new_with_token("arg1", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arg1"))))))), @@ -3065,21 +3082,21 @@ mod tests { ], vec![Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("contains", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("contains")))))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg1".to_owned()))), + expr: Expr::Literal(Literal::String("arg1".to_owned())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg2".to_owned()))), + expr: Expr::Literal(Literal::String("arg2".to_owned())), }), ], - )), + ), })], - )), + ), }), ]))] #[case::ident4( @@ -3095,19 +3112,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("and", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("and")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }), ], - )) + ) }) ]))] #[case::ident5( @@ -3147,19 +3164,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("lang".to_owned()))), + expr: Expr::Literal(Literal::String("lang".to_owned())), }), ], - )), + ), }) ]))] #[case::error( @@ -3183,14 +3200,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("name", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("name")))))), SmallVec::new(), vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), })], - )), + ), }), ]))] #[case::def_with_end( @@ -3206,14 +3223,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("name", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("name")))))), SmallVec::new(), vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), })], - )), + ), }), ]))] #[case::def2( @@ -3298,14 +3315,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("name", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("name")))))), SmallVec::new(), vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), })], - )), + ), }), ]))] #[case::def_without_colon2( @@ -3320,14 +3337,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("name", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("name")))))), SmallVec::new(), vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), })], - )), + ), }), ]))] #[case::def_without_colon_with_args( @@ -3343,18 +3360,18 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("name", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("name")))))), smallvec![ Param::new(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), ], vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident( + expr: Expr::Ident( IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))), - )), + ), })], - )), + ), }), ]))] #[case::unmatched_end_at_root( @@ -3408,13 +3425,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Let( + expr: Expr::Let( Pattern::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), - )), + ), }) ]))] #[case::let_2( @@ -3428,13 +3445,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Let( + expr: Expr::Let( Pattern::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("hello".to_owned()))), + expr: Expr::Literal(Literal::String("hello".to_owned())), }), - )), + ), }) ]))] #[case::let_3( @@ -3448,13 +3465,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Let( + expr: Expr::Let( Pattern::Ident(IdentWithToken::new_with_token("flag", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("flag"))))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), - )), + ), }) ]))] #[case::let_4( @@ -3468,15 +3485,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Let( + expr: Expr::Let( Pattern::Ident(IdentWithToken::new_with_token("z", Some(Shared::new(token(TokenKind::Ident("z".into())))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new( + expr: Expr::Ident(IdentWithToken::new_with_token("some_var", - Some(Shared::new(token(TokenKind::Ident(SmolStr::new("some_var")))))))) + Some(Shared::new(token(TokenKind::Ident(SmolStr::new("some_var"))))))) }), - )), + ), }) ]))] #[case::let_5( @@ -3490,14 +3507,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Let( + expr: Expr::Let( Pattern::Ident(IdentWithToken::new_with_token("z", Some(Shared::new(token(TokenKind::Ident("z".into())))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new( - Expr::Ident(IdentWithToken::new_with_token("some_var", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("some_var")))))))), + expr: + Expr::Ident(IdentWithToken::new_with_token("some_var", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("some_var"))))))), }), - )), + ), }) ]))] #[case::let_6( @@ -3510,14 +3527,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Let( + expr: Expr::Let( Pattern::Ident(IdentWithToken::new_with_token("z", Some(Shared::new(token(TokenKind::Ident("z".into())))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new( - Expr::Ident(IdentWithToken::new_with_token("some_var", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("some_var")))))))), + expr: + Expr::Ident(IdentWithToken::new_with_token("some_var", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("some_var"))))))), }), - )), + ), }) ]))] #[case::var_1( @@ -3531,13 +3548,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Var( + expr: Expr::Var( Pattern::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), - )), + ), }) ]))] #[case::var_2( @@ -3551,13 +3568,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Var( + expr: Expr::Var( Pattern::Ident(IdentWithToken::new_with_token("count", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("count"))))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), - )), + ), }) ]))] #[case::assign_1( @@ -3570,13 +3587,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Assign( + expr: Expr::Assign( IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(100.into()))), + expr: Expr::Literal(Literal::Number(100.into())), }), - )), + ), }) ]))] #[case::assign_2( @@ -3589,13 +3606,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Assign( + expr: Expr::Assign( IdentWithToken::new_with_token("name", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("name")))))), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("Alice".to_owned()))), + expr: Expr::Literal(Literal::String("Alice".to_owned())), }), - )), + ), }) ]))] #[case::index_assign( @@ -3611,29 +3628,29 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Assign( + expr: Expr::Assign( IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SET, Some(Shared::new(token(TokenKind::Equal)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(10.into()))), + expr: Expr::Literal(Literal::Number(10.into())), }), ], - )), + ), }), - )), + ), }) ]))] #[case::index_compound_assign( @@ -3649,53 +3666,53 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Assign( + expr: Expr::Assign( IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SET, Some(Shared::new(token(TokenKind::PlusEqual)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ADD, Some(Shared::new(token(TokenKind::PlusEqual)))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }), ], - )), + ), }), - )), + ), }) ]))] #[case::index_double_slash_equal( @@ -3712,59 +3729,59 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Assign( + expr: Expr::Assign( IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SET, Some(Shared::new(token(TokenKind::DoubleSlashEqual)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::FLOOR, Some(Shared::new(token(TokenKind::DoubleSlashEqual)))), smallvec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::DIV, Some(Shared::new(token(TokenKind::DoubleSlashEqual)))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), })], - )), + ), }), ], - )), + ), }), - )), + ), }) ]))] #[case::root_semicolon_error( @@ -3791,25 +3808,25 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 7.into(), - expr: Shared::new(Expr::If(smallvec![ + expr: Expr::If(smallvec![ ( Some(Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), })), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("true branch".to_owned()))), + expr: Expr::Literal(Literal::String("true branch".to_owned())), }) ), ( None, Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Literal(Literal::String("false branch".to_owned()))), + expr: Expr::Literal(Literal::String("false branch".to_owned())), }) ) - ])), + ]), }) ]))] #[case::if_elif_else( @@ -3834,35 +3851,35 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 11.into(), - expr: Shared::new(Expr::If(smallvec![ + expr: Expr::If(smallvec![ ( Some(Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), })), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("true branch".to_owned()))), + expr: Expr::Literal(Literal::String("true branch".to_owned())), }) ), ( Some(Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(false))), + expr: Expr::Literal(Literal::Bool(false)), })), Shared::new(Node { token_id: 7.into(), - expr: Shared::new(Expr::Literal(Literal::String("elif branch".to_owned()))), + expr: Expr::Literal(Literal::String("elif branch".to_owned())), }) ), ( None, Shared::new(Node { token_id: 10.into(), - expr: Shared::new(Expr::Literal(Literal::String("else branch".to_owned()))), + expr: Expr::Literal(Literal::String("else branch".to_owned())), }) ) - ])), + ]), }) ]))] #[case::if_only( @@ -3878,18 +3895,18 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::If(smallvec![ + expr: Expr::If(smallvec![ ( Some(Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), })), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("true branch".to_owned()))), + expr: Expr::Literal(Literal::String("true branch".to_owned())), }) ), - ])), + ]), }) ]))] #[case::if_elif( @@ -3911,28 +3928,28 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::If(smallvec![ + expr: Expr::If(smallvec![ ( Some(Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), })), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("true branch".to_owned()))), + expr: Expr::Literal(Literal::String("true branch".to_owned())), }) ), ( Some(Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), })), Shared::new(Node { token_id: 7.into(), - expr: Shared::new(Expr::Literal(Literal::String("true branch".to_owned()))), + expr: Expr::Literal(Literal::String("true branch".to_owned())), }) ), - ])), + ]), }) ]))] #[case::if_error( @@ -3998,7 +4015,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(None))), + expr: Expr::Selector(Selector::Heading(None)), }) ]))] #[case::h_selector_without_number( @@ -4009,7 +4026,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(None))), + expr: Expr::Selector(Selector::Heading(None)), }) ]))] #[case::while_( @@ -4024,16 +4041,16 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::While( + expr: Expr::While( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), vec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("loop body".to_owned()))), + expr: Expr::Literal(Literal::String("loop body".to_owned())), })], - )), + ), })]))] #[case::while_error( vec![ @@ -4066,16 +4083,16 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::While( + expr: Expr::While( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), vec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("loop body".to_owned()))), + expr: Expr::Literal(Literal::String("loop body".to_owned())), })], - )), + ), })]))] #[case::loop_( vec![ @@ -4086,12 +4103,12 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Loop( + expr: Expr::Loop( vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("loop body".to_owned()))), + expr: Expr::Literal(Literal::String("loop body".to_owned())), })], - )), + ), })]))] #[case::loop_error_no_body( vec![ @@ -4111,16 +4128,16 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Until( + expr: Expr::Until( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), vec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("loop body".to_owned()))), + expr: Expr::Literal(Literal::String("loop body".to_owned())), })], - )), + ), })]))] #[case::until_error( vec![ @@ -4145,18 +4162,18 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Unless(smallvec![ + expr: Expr::Unless(smallvec![ ( Some(Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(false))), + expr: Expr::Literal(Literal::Bool(false)), })), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("branch".to_owned()))), + expr: Expr::Literal(Literal::String("branch".to_owned())), }) ), - ])), + ]), }) ]))] #[case::unless_error( @@ -4181,17 +4198,17 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Try( + expr: Expr::Try( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("error_expr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("error_expr"))))))), }), None, Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("fallback".to_owned()))), + expr: Expr::Literal(Literal::String("fallback".to_owned())), }), - )), + ), })]))] #[case::try_catch_with_binder( vec![ @@ -4208,17 +4225,17 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Try( + expr: Expr::Try( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("error_expr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("error_expr"))))))), }), Some(IdentWithToken::new_with_token("e", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("e"))))))), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("e", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("e")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("e", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("e"))))))), }), - )), + ), })]))] #[case::foreach( vec![ @@ -4237,18 +4254,18 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Foreach( + expr: Expr::Foreach( IdentWithToken::new_with_token( "item", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("item"))))), ), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("array".to_owned()))), + expr: Expr::Literal(Literal::String("array".to_owned())), }), vec![Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( "print", Some(Shared::new(token(TokenKind::Ident(SmolStr::new( @@ -4257,14 +4274,14 @@ mod tests { ), smallvec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + expr: Expr::Ident(IdentWithToken::new_with_token( "item", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("item"))))), - ))), + )), })], - )), + ), })], - )), + ), })]))] #[case::foreach( vec![ @@ -4296,18 +4313,18 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Foreach( + expr: Expr::Foreach( IdentWithToken::new_with_token( "item", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("item"))))), ), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("array".to_owned()))), + expr: Expr::Literal(Literal::String("array".to_owned())), }), vec![Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( "print", Some(Shared::new(token(TokenKind::Ident(SmolStr::new( @@ -4316,20 +4333,20 @@ mod tests { ), smallvec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + expr: Expr::Ident(IdentWithToken::new_with_token( "item", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("item"))))), - ))), + )), })], - )), + ), })], - )), + ), })]))] #[case::self_( vec![token(TokenKind::Self_), token(TokenKind::Eof)], Ok(vec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, })]))] #[case::include( vec![ @@ -4339,7 +4356,7 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Include(Literal::String("module_name".to_owned()))), + expr: Expr::Include(Literal::String("module_name".to_owned())), })]))] #[case::code_selector_with_language( vec![ @@ -4348,7 +4365,7 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Selector(Selector::Code)), + expr: Expr::Selector(Selector::Code), })]))] #[case::selector_call_heading_single_arg( vec![ @@ -4360,14 +4377,14 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Heading(None), // arg literal is allocated first (id=0), then the selector token (id=1) smallvec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), })], - )), + ), })]))] #[case::selector_call_heading_multi_arg( vec![ @@ -4381,20 +4398,20 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Heading(None), // args are allocated first (id=0, id=1), then the selector token (id=2) smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), })]))] #[case::selector_call_code_lang( vec![ @@ -4406,14 +4423,14 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Code, // arg literal is allocated first (id=0), then the selector token (id=1) smallvec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("rust".to_owned()))), + expr: Expr::Literal(Literal::String("rust".to_owned())), })], - )), + ), })]))] #[case::selector_call_with_attribute( vec![ @@ -4427,7 +4444,7 @@ mod tests { Ok(vec![Shared::new(Node { // attr() Call: arg literal id=0, SelectorCall id=1, attr_literal id=2, Call id=3 token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Selector(SmolStr::new(".h"))))), @@ -4435,20 +4452,20 @@ mod tests { smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Heading(None), smallvec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), })], - )), + ), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("level".to_owned()))), + expr: Expr::Literal(Literal::String("level".to_owned())), }), ], - )), + ), })]))] #[case::selector_call_code_with_lang_attribute( vec![ @@ -4462,7 +4479,7 @@ mod tests { Ok(vec![Shared::new(Node { // attr() Call: arg literal id=0, SelectorCall id=1, attr_literal id=2, Call id=3 token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token( constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Selector(SmolStr::new(".code"))))), @@ -4470,20 +4487,20 @@ mod tests { smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Code, smallvec![Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("rust".to_owned()))), + expr: Expr::Literal(Literal::String("rust".to_owned())), })], - )), + ), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("lang".to_owned()))), + expr: Expr::Literal(Literal::String("lang".to_owned())), }), ], - )), + ), })]))] #[case::table_selector( vec![ @@ -4498,7 +4515,7 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Selector(Selector::Table(Some(1), Some(2)))), + expr: Expr::Selector(Selector::Table(Some(1), Some(2))), })]))] #[case::selector_call_list_bracket_single_arg( vec![ @@ -4512,13 +4529,13 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::List(None, None), smallvec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), })], - )), + ), })]))] #[case::selector_call_table_bracket_single_arg( vec![ @@ -4534,13 +4551,13 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Table(None, None), smallvec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), })], - )), + ), })]))] #[case::selector_call_list_bracket_variable( vec![ @@ -4552,16 +4569,16 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::List(None, None), smallvec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + expr: Expr::Ident(IdentWithToken::new_with_token( "v", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("v"))))), - ))), + )), })], - )), + ), })]))] #[case::selector_call_table_bracket_column_variable( vec![ @@ -4575,22 +4592,22 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Table(None, None), smallvec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + expr: Expr::Ident(IdentWithToken::new_with_token( "v", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("v"))))), - ))), + )), }), ], - )), + ), })]))] #[case::selector_call_table_bracket_variable( vec![ @@ -4604,16 +4621,16 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Table(None, None), smallvec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token( + expr: Expr::Ident(IdentWithToken::new_with_token( "v", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("v"))))), - ))), + )), })], - )), + ), })]))] #[case::selector_call_table_bracket_row_col_args( vec![ @@ -4631,19 +4648,19 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::SelectorCall( + expr: Expr::SelectorCall( Selector::Table(None, None), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), })]))] #[case::foreach_error( vec![ @@ -4711,7 +4728,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Nodes), + expr: Expr::Nodes, }) ]))] #[case::nodes_error_in_subprogram( @@ -4735,11 +4752,11 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Nodes), + expr: Expr::Nodes, }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(Some(1)))), + expr: Expr::Selector(Selector::Heading(Some(1))), }) ]))] #[case::root_level_with_multiple_pipes( @@ -4756,19 +4773,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Nodes), + expr: Expr::Nodes, }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Nodes), + expr: Expr::Nodes, }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(Some(1)))), + expr: Expr::Selector(Selector::Heading(Some(1))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Selector(Selector::Text)), + expr: Expr::Selector(Selector::Text), }) ]))] #[case::fn_simple( @@ -4783,15 +4800,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( SmallVec::new(), vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("result".to_owned()))), + expr: Expr::Literal(Literal::String("result".to_owned())), }) ], - )), + ), }) ]))] #[case::fn_with_args( @@ -4814,7 +4831,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( smallvec![ Param::new(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), Param::new(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), @@ -4822,22 +4839,22 @@ mod tests { vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("contains", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("contains")))))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), ], - )), + ), }) ], - )), + ), }) ]))] #[case::fn_with_multiple_statements( @@ -4855,21 +4872,21 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( smallvec![ Param::new(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), ], vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("first".to_owned()))), + expr: Expr::Literal(Literal::String("first".to_owned())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("second".to_owned()))), + expr: Expr::Literal(Literal::String("second".to_owned())), }) ], - )), + ), }) ]))] #[case::fn_with_invalid_args( @@ -4909,25 +4926,25 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("apply", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("apply")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( smallvec![ Param::new(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), ], vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("processed".to_owned()))), + expr: Expr::Literal(Literal::String("processed".to_owned())), }) ], - )), + ), }) ], - )), + ), }) ]))] #[case::empty_array( @@ -4939,10 +4956,10 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), SmallVec::new(), - )), + ), }) ]))] #[case::array_with_elements( @@ -4957,19 +4974,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("first".to_owned()))), + expr: Expr::Literal(Literal::String("first".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), ], - )), + ), }) ]))] #[case::array_with_mixed_elements( @@ -4986,23 +5003,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("text".to_owned()))), + expr: Expr::Literal(Literal::String("text".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), ], - )), + ), }) ]))] #[case::array_with_nested_array( @@ -5021,35 +5038,35 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }), ], - )), + ), }) ]))] #[case::array_with_trailing_comma( @@ -5063,15 +5080,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), ], - )), + ), }) ]))] #[case::array_unclosed( @@ -5110,19 +5127,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("bar", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("bar")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("bar", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("bar"))))))), }), ], - )), + ), }) ]))] #[case::equality_simple( @@ -5135,19 +5152,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("hello".to_owned()))), + expr: Expr::Literal(Literal::String("hello".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("world".to_owned()))), + expr: Expr::Literal(Literal::String("world".to_owned())), }), ], - )), + ), }) ]))] #[case::equality_numbers( @@ -5160,19 +5177,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), ], - )), + ), }) ]))] #[case::equality_booleans( @@ -5185,19 +5202,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(false))), + expr: Expr::Literal(Literal::Bool(false)), }), ], - )), + ), }) ]))] #[case::equality_with_identifiers( @@ -5210,19 +5227,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), ], - )), + ), }) ]))] #[case::equality_with_function_call( @@ -5238,27 +5255,27 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg".to_owned()))), + expr: Expr::Literal(Literal::String("arg".to_owned())), }), ], - )), + ), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("result".to_owned()))), + expr: Expr::Literal(Literal::String("result".to_owned())), }), ], - )), + ), }) ]))] #[case::equality_with_selectors( @@ -5271,19 +5288,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(Some(1)))), + expr: Expr::Selector(Selector::Heading(Some(1))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Selector(Selector::Text)), + expr: Expr::Selector(Selector::Text), }), ], - )), + ), }) ]))] #[case::equality_with_none( @@ -5296,19 +5313,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), ], - )), + ), }) ]))] #[case::equality_error_missing_rhs( @@ -5333,30 +5350,30 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::If(smallvec![ + expr: Expr::If(smallvec![ ( Some(Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(5.into()))), + expr: Expr::Literal(Literal::Number(5.into())), }), ], - )), + ), })), Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("equal".to_owned()))), + expr: Expr::Literal(Literal::String("equal".to_owned())), }) ), - ])), + ]), }) ]))] #[case::not_equality_simple( @@ -5369,19 +5386,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("hello".to_owned()))), + expr: Expr::Literal(Literal::String("hello".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("world".to_owned()))), + expr: Expr::Literal(Literal::String("world".to_owned())), }), ], - )), + ), }) ]))] #[case::not_equality_numbers( @@ -5394,19 +5411,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(24.into()))), + expr: Expr::Literal(Literal::Number(24.into())), }), ], - )), + ), }) ]))] #[case::not_equality_booleans( @@ -5419,19 +5436,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(false))), + expr: Expr::Literal(Literal::Bool(false)), }), ], - )), + ), }) ]))] #[case::not_equality_with_identifiers( @@ -5444,19 +5461,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), ], - )), + ), }) ]))] #[case::not_equality_with_function_call( @@ -5472,27 +5489,27 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg".to_owned()))), + expr: Expr::Literal(Literal::String("arg".to_owned())), }), ], - )), + ), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("result".to_owned()))), + expr: Expr::Literal(Literal::String("result".to_owned())), }), ], - )), + ), }) ]))] #[case::not_equality_with_selectors( @@ -5505,19 +5522,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(Some(1)))), + expr: Expr::Selector(Selector::Heading(Some(1))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Selector(Selector::Text)), + expr: Expr::Selector(Selector::Text), }), ], - )), + ), }) ]))] #[case::not_equality_with_none( @@ -5530,19 +5547,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("something".to_owned()))), + expr: Expr::Literal(Literal::String("something".to_owned())), }), ], - )), + ), }) ]))] #[case::not_equality_error_missing_rhs( @@ -5567,30 +5584,30 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::If(smallvec![ + expr: Expr::If(smallvec![ ( Some(Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NE, Some(Shared::new(token(TokenKind::NeEq)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(5.into()))), + expr: Expr::Literal(Literal::Number(5.into())), }), ], - )), + ), })), Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("not equal".to_owned()))), + expr: Expr::Literal(Literal::String("not equal".to_owned())), }) ), - ])), + ]), }) ]))] #[case::plus_simple( @@ -5603,19 +5620,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ADD, Some(Shared::new(token(TokenKind::Plus)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::plus_with_identifiers( @@ -5628,19 +5645,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ADD, Some(Shared::new(token(TokenKind::Plus)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), ], - )), + ), }) ]))] #[case::plus_error_missing_rhs( @@ -5660,19 +5677,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::LT, Some(Shared::new(token(TokenKind::Lt)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::lte_simple( @@ -5685,19 +5702,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::LTE, Some(Shared::new(token(TokenKind::Lte)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::gt_simple( @@ -5710,19 +5727,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GT, Some(Shared::new(token(TokenKind::Gt)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::gte_simple( @@ -5735,19 +5752,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GTE, Some(Shared::new(token(TokenKind::Gte)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::dict_empty( @@ -5759,10 +5776,10 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::new(token(TokenKind::LBrace)))), SmallVec::new(), - )), + ), }) ]))] #[case::dict_single_pair( @@ -5777,27 +5794,27 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::new(token(TokenKind::LBrace)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("key")))))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Symbol(Ident::new("key")))), + expr: Expr::Literal(Literal::Symbol(Ident::new("key"))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), ], - )), + ), }), ], - )), + ), }) ]))] #[case::dict_multiple_pairs( @@ -5816,43 +5833,43 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::new(token(TokenKind::LBrace)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Symbol(Ident::new("a")))), + expr: Expr::Literal(Literal::Symbol(Ident::new("a"))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::StringLiteral("b".to_owned()))))), smallvec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("b".to_owned()))), + expr: Expr::Literal(Literal::String("b".to_owned())), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(true))), + expr: Expr::Literal(Literal::Bool(true)), }), ], - )), + ), }), ], - )), + ), }) ]))] #[case::dict_trailing_comma( @@ -5868,27 +5885,27 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::new(token(TokenKind::LBrace)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Symbol(Ident::new("x")))), + expr: Expr::Literal(Literal::Symbol(Ident::new("x"))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(10.into()))), + expr: Expr::Literal(Literal::Number(10.into())), }), ], - )), + ), }), ], - )), + ), }) ]))] #[case::dict_unclosed( @@ -5927,19 +5944,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Selector(".h".into()))))), + expr: Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Selector(".h".into()))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Selector(Selector::Heading(None))), + expr: Expr::Selector(Selector::Heading(None)), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), ], - ))})]))] + )})]))] #[case::attr( vec![ token(TokenKind::Selector(".list".into())), @@ -5948,19 +5965,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Selector(".list".into()))))), + expr: Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Selector(".list".into()))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Selector(Selector::List(None, None))), + expr: Expr::Selector(Selector::List(None, None)), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("checked".to_owned()))), + expr: Expr::Literal(Literal::String("checked".to_owned())), }), ], - ))})]))] + )})]))] #[case::paren( vec![ token(TokenKind::LParen), @@ -5972,24 +5989,24 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Paren( + expr: Expr::Paren( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ADD, Some(Shared::new(token(TokenKind::Plus)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) - )), + ), }) ]))] #[case::minus_simple( @@ -6002,19 +6019,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SUB, Some(Shared::new(token(TokenKind::Minus)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(5.into()))), + expr: Expr::Literal(Literal::Number(5.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), ], - )), + ), }) ]))] #[case::minus_with_identifiers( @@ -6027,19 +6044,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SUB, Some(Shared::new(token(TokenKind::Minus)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), ], - )), + ), }) ]))] #[case::slash_simple( @@ -6052,19 +6069,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::DIV, Some(Shared::new(token(TokenKind::Slash)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(6.into()))), + expr: Expr::Literal(Literal::Number(6.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::percent_simple( @@ -6077,19 +6094,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::MOD, Some(Shared::new(token(TokenKind::Percent)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(10.into()))), + expr: Expr::Literal(Literal::Number(10.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), ], - )), + ), }) ]))] #[case::percent_with_identifiers( @@ -6102,19 +6119,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::MOD, Some(Shared::new(token(TokenKind::Percent)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), ], - )), + ), }) ]))] #[case::percent_error_missing_rhs( @@ -6134,19 +6151,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::MUL, Some(Shared::new(token(TokenKind::Asterisk)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(4.into()))), + expr: Expr::Literal(Literal::Number(4.into())), }), ], - )), + ), }) ]))] #[case::mul_with_identifiers( @@ -6159,19 +6176,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::MUL, Some(Shared::new(token(TokenKind::Asterisk)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), ], - )), + ), }) ]))] #[case::mul_error_missing_rhs( @@ -6191,19 +6208,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::CONVERT, Some(Shared::new(token(TokenKind::Convert)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), ], - )), + ), }) ]))] #[case::convert_error_missing_rhs( @@ -6225,31 +6242,31 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::MUL, Some(Shared::new(token(TokenKind::Asterisk)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::MUL, Some(Shared::new(token(TokenKind::Asterisk)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), ], - )), + ), }) ]))] #[case::multiple_binary_operators_eq( @@ -6264,31 +6281,31 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::EQ, Some(Shared::new(token(TokenKind::EqEq)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ADD, Some(Shared::new(token(TokenKind::Plus)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), ], - )), + ), }) ]))] #[case::multiple_and_operators( @@ -6303,20 +6320,20 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), - ])), + ]), }) ]))] #[case::multiple_or_operators( @@ -6331,20 +6348,20 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("z", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("z")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("z", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("z"))))))), }), - ])), + ]), }) ]))] #[case::and_or_mixed( @@ -6359,25 +6376,25 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), - ])), + ]), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), - ])), + ]), }) ]))] #[case::four_and_operators( @@ -6394,24 +6411,24 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d"))))))), }), - ])), + ]), }) ]))] #[case::four_or_operators( @@ -6428,24 +6445,24 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d"))))))), }), - ])), + ]), }) ]))] #[case::or_with_and_in_middle( @@ -6462,29 +6479,29 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), - ])), + ]), }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d"))))))), }), - ])), + ]), }) ]))] #[case::and_or_and_mixed( @@ -6502,34 +6519,34 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), - ])), + ]), }), Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d"))))))), }), - ])), + ]), }), - ])), + ]), }) ]))] #[case::or_and_or_mixed( @@ -6547,29 +6564,29 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), - ])), + ]), }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d"))))))), }), - ])), + ]), }) ]))] #[case::and_and_or_and_and_mixed( @@ -6591,42 +6608,42 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("b"))))))), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("c"))))))), }), - ])), + ]), }), Shared::new(Node { token_id: 9.into(), - expr: Shared::new(Expr::And(vec![ + expr: Expr::And(vec![ Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("d"))))))), }), Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("e", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("e")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("e", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("e"))))))), }), Shared::new(Node { token_id: 10.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f"))))))), }), - ])), + ]), }), - ])), + ]), }) ]))] #[case::range_simple( @@ -6639,19 +6656,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::RANGE, Some(Shared::new(token(TokenKind::DoubleDot)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(5.into()))), + expr: Expr::Literal(Literal::Number(5.into())), }), ], - )), + ), }) ]))] #[case::range_with_identifiers( @@ -6664,19 +6681,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::RANGE, Some(Shared::new(token(TokenKind::DoubleDot)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("start", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("start")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("start", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("start"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("end", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("end")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("end", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("end"))))))), }), ], - )), + ), }) ]))] #[case::range_error_missing_rhs( @@ -6751,40 +6768,40 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Or(vec![ + expr: Expr::Or(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GT, Some(Shared::new(token(TokenKind::Gt)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GT, Some(Shared::new(token(TokenKind::Gt)))), smallvec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }), - ])), + ]), }) ]))] #[case::not_simple( @@ -6796,15 +6813,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NOT, Some(Shared::new(token(TokenKind::Not)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(false))), + expr: Expr::Literal(Literal::Bool(false)), }), ], - )), + ), }) ]))] #[case::not_with_expr( @@ -6816,15 +6833,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NOT, Some(Shared::new(token(TokenKind::Not)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), ], - )), + ), }) ]))] #[case::bracket_access_with_number( @@ -6838,19 +6855,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(5.into()))), + expr: Expr::Literal(Literal::Number(5.into())), }), ], - )), + ), }) ]))] #[case::bracket_access_with_string( @@ -6864,19 +6881,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("dict")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("dict")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token(constants::builtins::DICT, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("dict"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("key".to_owned()))), + expr: Expr::Literal(Literal::String("key".to_owned())), }), ], - )), + ), }) ]))] #[case::bracket_access_error_missing_rbracket( @@ -6903,23 +6920,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(3.into()))), + expr: Expr::Literal(Literal::Number(3.into())), }), ], - )), + ), }) ]))] #[case::slice_access_with_variables( @@ -6935,23 +6952,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("items")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("items", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("items")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("items", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("items"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("start", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("start")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("start", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("start"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("end", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("end")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("end", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("end"))))))), }), ], - )), + ), }) ]))] #[case::not_with_paren_expr( @@ -6965,20 +6982,20 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NOT, Some(Shared::new(token(TokenKind::Not)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Paren( + expr: Expr::Paren( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Bool(false))), + expr: Expr::Literal(Literal::Bool(false)), }) - )), + ), }), ], - )), + ), }) ]))] #[case::not_error_missing_rhs( @@ -6995,7 +7012,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Break(None)), + expr: Expr::Break(None), }) ]))] #[case::continue_( @@ -7006,7 +7023,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Continue), + expr: Expr::Continue, }) ]))] #[case::self_bracket_access_with_number( @@ -7020,19 +7037,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Self_)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(5.into()))), + expr: Expr::Literal(Literal::Number(5.into())), }), ], - )), + ), }) ]))] #[case::self_bracket_access_with_string( @@ -7046,19 +7063,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Self_)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("key".to_owned()))), + expr: Expr::Literal(Literal::String("key".to_owned())), }), ], - )), + ), }) ]))] // Test function call followed by index access (e.g., foo()[0]) @@ -7075,22 +7092,22 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), SmallVec::new(), - )), + ), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }) ]))] // Test function call with arguments followed by index access @@ -7108,27 +7125,27 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("bar")))))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("bar", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("bar")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg".to_owned()))), + expr: Expr::Literal(Literal::String("arg".to_owned())), }) ], - )), + ), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("key".to_owned()))), + expr: Expr::Literal(Literal::String("key".to_owned())), }), ], - )), + ), }) ]))] // Test chained index access on function call result @@ -7148,34 +7165,34 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("baz")))))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("baz")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("baz", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("baz")))))), SmallVec::new(), - )), + ), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }) ]))] #[case::try_without_catch( @@ -7187,17 +7204,17 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Try( + expr: Expr::Try( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("error_expr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("error_expr"))))))), }), None, Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), - )), + ), })]) )] // Test index access followed by function call (e.g., arr[0]()) @@ -7214,25 +7231,25 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::CallDynamic( + expr: Expr::CallDynamic( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }), SmallVec::new(), - )), + ), }) ]))] // Test index access with args followed by function call (e.g., arr[0](arg)) @@ -7250,30 +7267,30 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::CallDynamic( + expr: Expr::CallDynamic( Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }), smallvec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::String("test".to_owned()))), + expr: Expr::Literal(Literal::String("test".to_owned())), }) ], - )), + ), }) ]))] // Test group expr with index access: (x)[0] → get(Paren(x), 0) @@ -7290,24 +7307,24 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::LParen)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Paren( + expr: Expr::Paren( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), - )), + ), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }) ]))] // Test group expr with dynamic call: (x)("test") → CallDynamic(Paren(x), ["test"]) @@ -7324,23 +7341,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::CallDynamic( + expr: Expr::CallDynamic( Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Paren( + expr: Expr::Paren( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), - )), + ), }), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("test".to_owned()))), + expr: Expr::Literal(Literal::String("test".to_owned())), }), ], - )), + ), }) ]))] // Test group expr with chained call then index: (f)("a")[0] → get(CallDynamic(Paren(f), ["a"]), 0) @@ -7360,35 +7377,35 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::LParen)))), smallvec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::CallDynamic( + expr: Expr::CallDynamic( Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Paren( + expr: Expr::Paren( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f"))))))), }), - )), + ), }), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("a".to_owned()))), + expr: Expr::Literal(Literal::String("a".to_owned())), }), ], - )), + ), }), Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }) ]))] // Test array literal with index access: [1,2][0] → get(array([1,2]), 0) @@ -7407,31 +7424,31 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ARRAY, Some(Shared::new(token(TokenKind::LBracket)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }) ]))] // Test chained index access followed by function call (e.g., arr[0][1]()) @@ -7451,37 +7468,37 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::CallDynamic( + expr: Expr::CallDynamic( Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GET, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), ], - )), + ), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }), SmallVec::new(), - )), + ), }) ]))] #[case::function_call_with_question_mark( @@ -7495,25 +7512,25 @@ mod tests { ], Ok(vec![Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Try( + expr: Expr::Try( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg".to_owned()))), + expr: Expr::Literal(Literal::String("arg".to_owned())), }), ], - )), + ), }), None, Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), - )), + ), })]) )] #[case::question_mark_after_call( @@ -7527,20 +7544,20 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Try( + expr: Expr::Try( Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), SmallVec::new(), - )), + ), }), None, Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), - )), + ), }) ]))] #[case::question_mark_after_call_with_args( @@ -7555,25 +7572,25 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Try( + expr: Expr::Try( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("bar", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("bar")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg".to_owned()))), + expr: Expr::Literal(Literal::String("arg".to_owned())), }), ], - )), + ), }), None, Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), - )), + ), }) ]))] #[case::question_mark_after_call_error( @@ -7593,19 +7610,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(Shared::new(token(TokenKind::Coalesce)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("foo".to_owned()))), + expr: Expr::Literal(Literal::String("foo".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("bar".to_owned()))), + expr: Expr::Literal(Literal::String("bar".to_owned())), }), ], - )), + ), }) ]))] #[case::coalesce_with_none( @@ -7618,19 +7635,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(Shared::new(token(TokenKind::Coalesce)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::None)), + expr: Expr::Literal(Literal::None), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("default".to_owned()))), + expr: Expr::Literal(Literal::String("default".to_owned())), }), ], - )), + ), }) ]))] #[case::coalesce_with_identifiers( @@ -7643,19 +7660,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(Shared::new(token(TokenKind::Coalesce)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), ], - )), + ), }) ]))] #[case::coalesce_error_missing_rhs( @@ -7674,15 +7691,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NEGATE, Some(Shared::new(token(TokenKind::Minus)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), ], - )), + ), }) ]))] #[case::negate_with_identifier( @@ -7694,15 +7711,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::NEGATE, Some(Shared::new(token(TokenKind::Minus)))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), ], - )), + ), }) ]))] #[case::negate_error_missing_rhs( @@ -7720,10 +7737,10 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Import( + expr: Expr::Import( Literal::String("name".to_owned()), None, - )), + ), }) ]))] #[case::import_as( @@ -7737,13 +7754,13 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Import( + expr: Expr::Import( Literal::String("name".to_owned()), Some(IdentWithToken::new_with_token( "alias", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("alias"))))), )), - )), + ), }) ]))] #[case::import_as_missing_ident( @@ -7764,10 +7781,10 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::QualifiedAccess( + expr: Expr::QualifiedAccess( vec![IdentWithToken::new_with_token("test", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("test"))))))], AccessTarget::Ident(IdentWithToken::new_with_token("foo", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("foo")))))), - ))), + )), }) ]))] #[case::qualified_access_with_call( @@ -7783,18 +7800,18 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::QualifiedAccess( + expr: Expr::QualifiedAccess( vec![IdentWithToken::new_with_token("mod", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod"))))))], AccessTarget::Call( IdentWithToken::new_with_token("func", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("func")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Literal(Literal::String("arg".to_owned()))), + expr: Expr::Literal(Literal::String("arg".to_owned())), }), ], ), - )), + ), }) ]))] #[case::qualified_access_multi_level( @@ -7811,7 +7828,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::QualifiedAccess( + expr: Expr::QualifiedAccess( vec![ IdentWithToken::new_with_token("mod1", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod1")))))), IdentWithToken::new_with_token("mod2", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod2")))))), @@ -7820,7 +7837,7 @@ mod tests { IdentWithToken::new_with_token("func", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("func")))))), smallvec![], ), - )), + ), }) ]))] #[case::slice_access_with_start_only( @@ -7835,31 +7852,31 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("len", None), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), ], - )), + ), }), ], - )), + ), }) ]))] #[case::slice_access_with_end_only( @@ -7874,23 +7891,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("arr"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::qualified_access_with_call_and_slice( @@ -7910,29 +7927,29 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::QualifiedAccess( + expr: Expr::QualifiedAccess( vec![IdentWithToken::new_with_token("mod", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod"))))))], AccessTarget::Call( IdentWithToken::new_with_token("func", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("func")))))), smallvec![], ), - )), + ), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))), + expr: Expr::Literal(Literal::Number(2.into())), }), ], - )), + ), }) ]))] #[case::qualified_access_with_call_and_end_only_slice( @@ -7951,29 +7968,29 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SLICE, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::QualifiedAccess( + expr: Expr::QualifiedAccess( vec![IdentWithToken::new_with_token("mod", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("mod"))))))], AccessTarget::Call( IdentWithToken::new_with_token("func", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("func")))))), smallvec![], ), - )), + ), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))), + expr: Expr::Literal(Literal::Number(0.into())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(1.into()))), + expr: Expr::Literal(Literal::Number(1.into())), }), ], - )), + ), }) ]))] #[case::selector_dot_is_self( @@ -7984,7 +8001,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }) ]))] #[case::ident_with_single_attr( @@ -7996,19 +8013,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("name".to_owned()))), + expr: Expr::Literal(Literal::String("name".to_owned())), }), ], - )), + ), }) ]))] #[case::function_call_result_with_attr( @@ -8022,22 +8039,22 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("get_user")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("get_user", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("get_user")))))), SmallVec::new(), - )), + ), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("name".to_owned()))), + expr: Expr::Literal(Literal::String("name".to_owned())), }), ], - )), + ), }) ]))] #[case::ident_with_attr_in_pipe( @@ -8051,23 +8068,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("data", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("data")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("data", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("data"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj")))))), smallvec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj"))))))), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), ], - )), + ), }) ]))] #[case::self_with_attr( @@ -8079,19 +8096,19 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::ATTR, Some(Shared::new(token(TokenKind::Self_)))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), ], - )), + ), }) ]))] #[case::pipe_equal_with_selector( @@ -8105,23 +8122,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(Shared::new(token(TokenKind::StringLiteral("new_id".to_owned()))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Selector(selector::Selector::Heading(Some(1)))), + expr: Expr::Selector(selector::Selector::Heading(Some(1))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("new_id".to_owned()))), + expr: Expr::Literal(Literal::String("new_id".to_owned())), }), ], - )), + ), }) ]))] #[case::pipe_equal_with_ident_and_attr( @@ -8135,23 +8152,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(Shared::new(token(TokenKind::StringLiteral("John".to_owned()))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("obj"))))))), }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("John".to_owned()))), + expr: Expr::Literal(Literal::String("John".to_owned())), }), ], - )), + ), }) ]))] #[case::pipe_equal_with_self_and_attr( @@ -8165,23 +8182,23 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(Shared::new(token(TokenKind::NumberLiteral(42.into()))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Self_), + expr: Expr::Self_, }), Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::String("value".to_owned()))), + expr: Expr::Literal(Literal::String("value".to_owned())), }), Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::Number(42.into()))), + expr: Expr::Literal(Literal::Number(42.into())), }), ], - )), + ), }) ]))] #[case::let_with_reserved_keyword_as_value( @@ -8241,16 +8258,16 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f")))))), smallvec![ Param::variadic(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args"))))))), ], vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args"))))))), })], - )), + ), }), ]))] #[case::def_with_regular_and_variadic_param( @@ -8270,7 +8287,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f")))))), smallvec![ Param::new(IdentWithToken::new_with_token("a", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("a"))))))), @@ -8278,9 +8295,9 @@ mod tests { ], vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("rest", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("rest")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("rest", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("rest"))))))), })], - )), + ), }), ]))] #[case::def_variadic_param_not_last( @@ -8319,14 +8336,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f")))))), smallvec![], vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args"))))))), })], - )), + ), }), ]))] #[case::def_without_params_with_do( @@ -8340,14 +8357,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Def( + expr: Expr::Def( IdentWithToken::new_with_token("f", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("f")))))), smallvec![], vec![Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("args"))))))), })], - )), + ), }), ]))] #[case::arrow_simple( @@ -8362,15 +8379,15 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( SmallVec::new(), vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("result".to_owned()))), + expr: Expr::Literal(Literal::String("result".to_owned())), }) ], - )), + ), }) ]))] #[case::arrow_with_args( @@ -8393,7 +8410,7 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( smallvec![ Param::new(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), Param::new(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), @@ -8401,22 +8418,22 @@ mod tests { vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("contains", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("contains")))))), smallvec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), }), Shared::new(Node { token_id: 3.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y")))))))), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("y"))))))), }), ], - )), + ), }) ], - )), + ), }) ]))] #[case::arrow_nested_in_call( @@ -8436,37 +8453,36 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token("apply", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("apply")))))), smallvec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Fn( + expr: Expr::Fn( smallvec![ Param::new(IdentWithToken::new_with_token("x", Some(Shared::new(token(TokenKind::Ident(SmolStr::new("x"))))))), ], vec![ Shared::new(Node { token_id: 2.into(), - expr: Shared::new(Expr::Literal(Literal::String("processed".to_owned()))), + expr: Expr::Literal(Literal::String("processed".to_owned())), }) ], - )), + ), }) ], - )), + ), }) ]))] fn test_parse(#[case] input: Vec, #[case] expected: Result) { let mut arena = Arena::new(10); - let tokens: Vec> = input.into_iter().map(Shared::new).collect(); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let result = Parser::new(input.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match (&result, &expected) { (Ok(actual), Ok(expected)) => { assert_eq!(actual.len(), expected.len()); - let actual_exprs: Vec<_> = actual.iter().map(|a| &*a.expr).collect(); - let expected_exprs: Vec<_> = expected.iter().map(|e| &*e.expr).collect(); + let actual_exprs: Vec<_> = actual.iter().map(|a| &a.expr).collect(); + let expected_exprs: Vec<_> = expected.iter().map(|e| &e.expr).collect(); assert_eq!(actual_exprs, expected_exprs); } (Err(actual), Err(expected)) => { @@ -8535,12 +8551,13 @@ mod tests { }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Selector(selector) = &*program[0].expr { + if let Expr::Selector(selector) = &program[0].expr { assert_eq!(*selector, expected_selector); } else { panic!("Expected Selector expression, got {:?}", program[0].expr); @@ -8615,12 +8632,13 @@ mod tests { module_id: 1.into(), })); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Selector(selector) = &*program[0].expr { + if let Expr::Selector(selector) = &program[0].expr { assert_eq!(*selector, expected_selector); } else { panic!("Expected Selector expression, got {:?}", program[0].expr); @@ -8671,17 +8689,18 @@ mod tests { module_id: 1.into(), })); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID) + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID) .parse() .expect("parse error"); assert_eq!(result.len(), 1); - let Expr::Block(nodes) = &*result[0].expr else { + let Expr::Block(nodes) = &result[0].expr else { panic!("expected Block, got {:?}", result[0].expr); }; assert_eq!(nodes.len(), expected.len()); for (node, sel) in nodes.iter().zip(expected.iter()) { - let Expr::Selector(actual) = &*node.expr else { + let Expr::Selector(actual) = &node.expr else { panic!("expected Selector, got {:?}", node.expr); }; assert_eq!(actual, sel); @@ -8735,17 +8754,18 @@ mod tests { module_id: 1.into(), })); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID) + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID) .parse() .expect("parse error"); assert_eq!(result.len(), 1); - let Expr::Block(nodes) = &*result[0].expr else { + let Expr::Block(nodes) = &result[0].expr else { panic!("expected Block, got {:?}", result[0].expr); }; assert_eq!(nodes.len(), expected.len()); for (node, sel) in nodes.iter().zip(expected.iter()) { - let Expr::Selector(actual) = &*node.expr else { + let Expr::Selector(actual) = &node.expr else { panic!("expected Selector, got {:?}", node.expr); }; assert_eq!(actual, sel); @@ -8777,15 +8797,16 @@ mod tests { module_id: 1.into(), }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); assert!(result.is_ok()); let program = result.unwrap(); assert_eq!(program.len(), 1); - let Expr::As(ident, inner) = &*program[0].expr else { + let Expr::As(ident, inner) = &program[0].expr else { panic!("expected Expr::As, got {:?}", program[0].expr); }; assert_eq!(ident.name.as_str(), "x"); - assert!(matches!(*inner.expr, Expr::Literal(Literal::Number(_)))); + assert!(matches!(&inner.expr, Expr::Literal(Literal::Number(_)))); } #[test] @@ -8809,12 +8830,13 @@ mod tests { }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Literal(Literal::String(value)) = &*program[0].expr { + if let Expr::Literal(Literal::String(value)) = &program[0].expr { assert_eq!(value, "test_value"); } else { panic!("Expected String literal, got {:?}", program[0].expr); @@ -8846,7 +8868,8 @@ mod tests { }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); assert!(matches!( result, @@ -8890,15 +8913,16 @@ mod tests { }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Call(ident, args) = &*program[0].expr { + if let Expr::Call(ident, args) = &program[0].expr { assert_eq!(ident.name, "function".into()); assert_eq!(args.len(), 1); - if let Expr::Literal(Literal::String(value)) = &*args[0].expr { + if let Expr::Literal(Literal::String(value)) = &args[0].expr { assert_eq!(value, "env_arg_value"); } else { panic!("Expected String literal in argument, got {:?}", args[0].expr); @@ -8930,16 +8954,17 @@ mod tests { }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Call(ident, args) = &*program[0].expr { + if let Expr::Call(ident, args) = &program[0].expr { assert_eq!(ident.name, "attr".into()); assert_eq!(args.len(), 2); - assert!(matches!(&*args[0].expr, Expr::Self_)); - if let Expr::Literal(Literal::String(attr_str)) = &*args[1].expr { + assert!(matches!(&args[0].expr, Expr::Self_)); + if let Expr::Literal(Literal::String(attr_str)) = &args[1].expr { assert_eq!(attr_str, attribute); } else { panic!("Expected String literal in second argument, got {:?}", args[1].expr); @@ -8980,18 +9005,19 @@ mod tests { module_id: 1.into(), })); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Call(ident, args) = &*program[0].expr { + if let Expr::Call(ident, args) = &program[0].expr { // Should be transformed to attr(base_selector, "attribute") assert_eq!(ident.name, "attr".into()); assert_eq!(args.len(), 2); // First argument should be the base selector - if let Expr::Selector(selector) = &*args[0].expr { + if let Expr::Selector(selector) = &args[0].expr { match base_selector { "h" => assert_eq!(*selector, Selector::Heading(None)), "h1" => assert_eq!(*selector, Selector::Heading(Some(1))), @@ -9004,7 +9030,7 @@ mod tests { } // Second argument should be the attribute string - if let Expr::Literal(Literal::String(attr_str)) = &*args[1].expr { + if let Expr::Literal(Literal::String(attr_str)) = &args[1].expr { assert_eq!(attr_str, attribute); } else { panic!("Expected String literal in second argument, got {:?}", args[1].expr); @@ -9043,12 +9069,13 @@ mod tests { module_id: 1.into(), })); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Block(nodes) = &*program[0].expr { + if let Expr::Block(nodes) = &program[0].expr { // Desugars to `base | .. | step1 | .. | step2 | ...`, i.e. a plain // selector interleaved with a `Recursive` selector between each hop. let expected: Vec = expected_selectors @@ -9064,7 +9091,7 @@ mod tests { .collect(); assert_eq!(nodes.len(), expected.len()); for (node, expected_sel) in nodes.iter().zip(expected.iter()) { - if let Expr::Selector(sel) = &*node.expr { + if let Expr::Selector(sel) = &node.expr { assert_eq!(sel, expected_sel); } else { panic!("Expected Selector expression, got {:?}", node.expr); @@ -9099,16 +9126,17 @@ mod tests { module_id: 1.into(), })); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::Call(ident, args) = &*program[0].expr { + if let Expr::Call(ident, args) = &program[0].expr { assert_eq!(ident.name, "attr".into()); assert_eq!(args.len(), 2); - assert!(matches!(&*args[0].expr, Expr::Block(nodes) if nodes.len() == 3)); - assert!(matches!(&*args[1].expr, Expr::Literal(Literal::String(s)) if s == "lang")); + assert!(matches!(&args[0].expr, Expr::Block(nodes) if nodes.len() == 3)); + assert!(matches!(&args[1].expr, Expr::Literal(Literal::String(s)) if s == "lang")); } else { panic!("Expected Call expression, got {:?}", program[0].expr); } @@ -9143,14 +9171,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9158,7 +9186,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("one".to_owned()))) + expr: Expr::Literal(Literal::String("one".to_owned())) }) }, MatchArm { @@ -9166,7 +9194,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("two".to_owned()))) + expr: Expr::Literal(Literal::String("two".to_owned())) }) }, MatchArm { @@ -9174,11 +9202,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 11.into(), - expr: Shared::new(Expr::Literal(Literal::String("other".to_owned()))) + expr: Expr::Literal(Literal::String("other".to_owned())) }) } ] - )) + ) }) ]))] #[case::match_type_pattern( @@ -9204,14 +9232,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("value", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("value", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("value")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9219,7 +9247,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("is string".to_owned()))) + expr: Expr::Literal(Literal::String("is string".to_owned())) }) }, MatchArm { @@ -9227,11 +9255,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("is number".to_owned()))) + expr: Expr::Literal(Literal::String("is number".to_owned())) }) } ] - )) + ) }) ]))] #[case::match_array_pattern( @@ -9255,14 +9283,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("arr")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9273,11 +9301,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("two elements".to_owned()))) + expr: Expr::Literal(Literal::String("two elements".to_owned())) }) } ] - )) + ) }) ]))] #[case::match_array_rest_pattern( @@ -9302,14 +9330,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("arr")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9320,15 +9348,15 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("first", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("first", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("first")), module_id: 1.into() - }))))) + })))) }) } ] - )) + ) }) ]))] #[case::match_dict_pattern( @@ -9352,14 +9380,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("obj")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9370,15 +9398,15 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("name", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("name", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("name")), module_id: 1.into() - }))))) + })))) }) } ] - )) + ) }) ]))] #[case::match_with_guard( @@ -9418,68 +9446,68 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("n", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("n", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("n")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { pattern: Pattern::Ident(IdentWithToken::new("x")), guard: Some(Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::GT, Some(Shared::new(token(TokenKind::Gt)))), smallvec![ Shared::new(Node { token_id: 4.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), Shared::new(Node { token_id: 6.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))) + expr: Expr::Literal(Literal::Number(0.into())) }) ] - )) + ) })), body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("positive".to_owned()))) + expr: Expr::Literal(Literal::String("positive".to_owned())) }) }, MatchArm { pattern: Pattern::Ident(IdentWithToken::new("x")), guard: Some(Shared::new(Node { token_id: 11.into(), - expr: Shared::new(Expr::Call( + expr: Expr::Call( IdentWithToken::new_with_token(constants::builtins::LT, Some(Shared::new(token(TokenKind::Lt)))), smallvec![ Shared::new(Node { token_id: 10.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), Shared::new(Node { token_id: 12.into(), - expr: Shared::new(Expr::Literal(Literal::Number(0.into()))) + expr: Expr::Literal(Literal::Number(0.into())) }) ] - )) + ) })), body: Shared::new(Node { token_id: 14.into(), - expr: Shared::new(Expr::Literal(Literal::String("negative".to_owned()))) + expr: Expr::Literal(Literal::String("negative".to_owned())) }) }, MatchArm { @@ -9487,11 +9515,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 17.into(), - expr: Shared::new(Expr::Literal(Literal::String("zero".to_owned()))) + expr: Expr::Literal(Literal::String("zero".to_owned())) }) } ] - )) + ) }) ]))] #[case::match_do_end( @@ -9519,10 +9547,10 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Literal(Literal::Number(2.into()))) + expr: Expr::Literal(Literal::Number(2.into())) }), smallvec![ MatchArm { @@ -9530,7 +9558,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("one".to_owned()))) + expr: Expr::Literal(Literal::String("one".to_owned())) }) }, MatchArm { @@ -9538,7 +9566,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("two".to_owned()))) + expr: Expr::Literal(Literal::String("two".to_owned())) }) }, MatchArm { @@ -9546,11 +9574,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 11.into(), - expr: Shared::new(Expr::Literal(Literal::String("other".to_owned()))) + expr: Expr::Literal(Literal::String("other".to_owned())) }) } ] - )) + ) }) ]))] // --- or-pattern parser tests --- @@ -9580,14 +9608,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9598,7 +9626,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("matched".to_owned()))) + expr: Expr::Literal(Literal::String("matched".to_owned())) }) }, MatchArm { @@ -9606,11 +9634,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("other".to_owned()))) + expr: Expr::Literal(Literal::String("other".to_owned())) }) }, ] - )) + ) }) ]))] #[case::match_or_string_literals( @@ -9636,14 +9664,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9654,7 +9682,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("matched".to_owned()))) + expr: Expr::Literal(Literal::String("matched".to_owned())) }) }, MatchArm { @@ -9662,11 +9690,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("other".to_owned()))) + expr: Expr::Literal(Literal::String("other".to_owned())) }) }, ] - )) + ) }) ]))] #[case::match_or_type_patterns( @@ -9690,14 +9718,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9708,11 +9736,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("str or num".to_owned()))) + expr: Expr::Literal(Literal::String("str or num".to_owned())) }) }, ] - )) + ) }) ]))] #[case::match_or_three_alternatives( @@ -9740,14 +9768,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9759,7 +9787,7 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("matched".to_owned()))) + expr: Expr::Literal(Literal::String("matched".to_owned())) }) }, MatchArm { @@ -9767,11 +9795,11 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 8.into(), - expr: Shared::new(Expr::Literal(Literal::String("other".to_owned()))) + expr: Expr::Literal(Literal::String("other".to_owned())) }) }, ] - )) + ) }) ]))] #[case::match_or_bool_literals( @@ -9793,14 +9821,14 @@ mod tests { Ok(vec![ Shared::new(Node { token_id: 0.into(), - expr: Shared::new(Expr::Match( + expr: Expr::Match( Shared::new(Node { token_id: 1.into(), - expr: Shared::new(Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(Shared::new(Token { range: Range::default(), kind: TokenKind::Ident(SmolStr::new("x")), module_id: 1.into() - }))))) + })))) }), smallvec![ MatchArm { @@ -9811,17 +9839,16 @@ mod tests { guard: None, body: Shared::new(Node { token_id: 5.into(), - expr: Shared::new(Expr::Literal(Literal::String("bool".to_owned()))) + expr: Expr::Literal(Literal::String("bool".to_owned())) }) }, ] - )) + ) }) ]))] fn test_parse_match(#[case] input: Vec, #[case] expected: Result) { let mut arena = Arena::new(10); - let tokens: Vec> = input.into_iter().map(Shared::new).collect(); - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let result = Parser::new(input.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); assert_eq!(result, expected); } @@ -9834,7 +9861,7 @@ mod tests { 2, |segments: &[StringSegment]| { matches!(&segments[0], StringSegment::Text(s) if s == "Value: ") && - matches!(&segments[1], StringSegment::Expr(node) if matches!(&*node.expr, Expr::Literal(Literal::Number(_)))) + matches!(&segments[1], StringSegment::Expr(node) if matches!(&node.expr, Expr::Literal(Literal::Number(_)))) } )] #[case::expr_string( @@ -9845,7 +9872,7 @@ mod tests { 2, |segments: &[StringSegment]| { matches!(&segments[0], StringSegment::Text(s) if s == "Result: ") && - matches!(&segments[1], StringSegment::Expr(node) if matches!(&*node.expr, Expr::Literal(Literal::String(s)) if s == "hello")) + matches!(&segments[1], StringSegment::Expr(node) if matches!(&node.expr, Expr::Literal(Literal::String(s)) if s == "hello")) } )] #[case::expr_call( @@ -9857,7 +9884,7 @@ mod tests { |segments: &[StringSegment]| { matches!(&segments[0], StringSegment::Text(s) if s == "Result: ") && if let StringSegment::Expr(node) = &segments[1] { - if let Expr::Call(ident, args) = &*node.expr { + if let Expr::Call(ident, args) = &node.expr { ident.name == "add".into() && args.len() == 2 } else { false @@ -9933,7 +9960,7 @@ mod tests { 2, |segments: &[StringSegment]| { matches!(&segments[0], StringSegment::Text(s) if s == "Bool: ") && - matches!(&segments[1], StringSegment::Expr(node) if matches!(&*node.expr, Expr::Literal(Literal::Bool(true)))) + matches!(&segments[1], StringSegment::Expr(node) if matches!(&node.expr, Expr::Literal(Literal::Bool(true)))) } )] fn test_parse_interpolated_string_with_expr( @@ -9955,12 +9982,13 @@ mod tests { }), ]; - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + let raw_tokens: Vec = tokens.iter().map(|token| (**token).clone()).collect(); + let result = Parser::new(raw_tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); match result { Ok(program) => { assert_eq!(program.len(), 1); - if let Expr::InterpolatedString(segments) = &*program[0].expr { + if let Expr::InterpolatedString(segments) = &program[0].expr { assert_eq!(segments.len(), expected_len); assert!(validator(segments), "Validator failed for segments"); } else { diff --git a/crates/mq-lang/src/engine.rs b/crates/mq-lang/src/engine.rs index 976e1d9f9..7811485bd 100644 --- a/crates/mq-lang/src/engine.rs +++ b/crates/mq-lang/src/engine.rs @@ -2,16 +2,11 @@ use std::borrow::Cow; use std::path::PathBuf; -#[cfg(all(feature = "debugger", feature = "tarn"))] +#[cfg(feature = "debugger")] use crate::Source; -#[cfg(not(feature = "tarn"))] -use crate::eval::Evaluator; use crate::io::{Io, NativeIo, SandboxedIo}; #[cfg(feature = "debugger")] use crate::module::ModuleId; -#[cfg(all(feature = "debugger", not(feature = "tarn")))] -use crate::runtime::env::Env; -#[cfg(feature = "tarn")] use crate::tarn; use crate::{ ArenaId, ModuleResolver, MqResult, Range, RuntimeValue, Shared, SharedCell, TokenKind, @@ -33,7 +28,7 @@ use crate::{ pub struct CompiledProgram { pub(crate) source: String, pub(crate) program: crate::ast::Program, - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] vm_cache: Option>>>>, } @@ -48,7 +43,7 @@ impl CompiledProgram { &self.program } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] pub(crate) fn cached_vm_program(&self) -> Option>> { let cache = self.vm_cache.as_ref()?; #[cfg(feature = "sync")] @@ -61,7 +56,7 @@ impl CompiledProgram { } } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] pub(crate) fn cache_vm_program(&self, program: Shared) { let Some(cache) = &self.vm_cache else { return; @@ -83,7 +78,7 @@ impl From for CompiledProgram { Self { source: String::new(), program, - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] vm_cache: Some(Shared::new(SharedCell::new(None))), } } @@ -108,21 +103,15 @@ impl From for CompiledProgram { /// ``` #[derive(Debug, Clone)] pub struct Engine> { - #[cfg(not(feature = "tarn"))] - pub(crate) evaluator: Evaluator, - /// VM-only state — see [`tarn::VmState`]. - #[cfg(feature = "tarn")] + /// VM state — see [`tarn::VmState`]. pub(crate) vm: tarn::VmState, token_arena: Shared>>>, optimization_level: OptimizationLevel, - #[cfg(feature = "tarn")] vm_module_prelude: Vec, } /// A module explicitly prepared through the Engine API, replayed before VM compilation. -/// The tree walker stores these in its dynamic environment; the VM instead needs their AST -/// declarations present while it statically resolves the user's query. -#[cfg(feature = "tarn")] +/// The VM needs their AST declarations present while it statically resolves the user's query. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) enum VmModulePrelude { Include(String), @@ -160,43 +149,16 @@ impl Engine> { let token_arena = create_default_token_arena(); let module_loader = ModuleLoader::new(module_resolver); Self { - #[cfg(not(feature = "tarn"))] - evaluator: Evaluator::new(module_loader.clone(), Shared::clone(&token_arena)), - #[cfg(feature = "tarn")] vm: tarn::VmState::with_module_loader(module_loader), token_arena, optimization_level: OptimizationLevel::default(), - #[cfg(feature = "tarn")] vm_module_prelude: Vec::new(), } } - /// Returns a reference to the underlying evaluator. - /// - /// This is primarily intended for advanced use cases such as debugging, - /// where direct access to the evaluator internals is required. - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn switch_env(&self, env: Shared>) -> Self { - #[cfg(not(feature = "sync"))] - let token_arena = Shared::new(SharedCell::new(self.token_arena.borrow().clone())); - #[cfg(feature = "sync")] - let token_arena = Shared::new(SharedCell::new(self.token_arena.read().unwrap().clone())); - - Self { - #[cfg(not(feature = "tarn"))] - evaluator: Evaluator::with_env(Shared::clone(&token_arena), Shared::clone(&env)), - #[cfg(feature = "tarn")] - vm: self.vm.clone(), - token_arena: Shared::clone(&token_arena), - optimization_level: self.optimization_level, - #[cfg(feature = "tarn")] - vm_module_prelude: self.vm_module_prelude.clone(), - } - } - /// Evaluates `code` against a paused Tarn VM frame's bindings, with `input` bound to /// `.`/`self`. - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] pub fn eval_debug_expression( &mut self, code: &str, @@ -227,18 +189,6 @@ impl Engine> { )) }) } - - /// Evaluates `code` against a paused tree-walker environment, with `input` bound to - /// `.`/`self`. - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn eval_debug_expression( - &mut self, - code: &str, - input: RuntimeValue, - env: &Shared>, - ) -> MqResult { - self.switch_env(Shared::clone(env)).eval(code, vec![input].into_iter()) - } } impl Engine { @@ -254,13 +204,9 @@ impl Engine { let token_arena = create_default_token_arena(); let module_loader = ModuleLoader::new(module_resolver); Self { - #[cfg(not(feature = "tarn"))] - evaluator: Evaluator::with_io(module_loader.clone(), Shared::clone(&token_arena), Shared::clone(&io)), - #[cfg(feature = "tarn")] vm: tarn::VmState::with_module_loader_and_io(module_loader, io), token_arena, optimization_level: OptimizationLevel::default(), - #[cfg(feature = "tarn")] vm_module_prelude: Vec::new(), } } @@ -273,16 +219,10 @@ impl Engine { /// Set the maximum call stack depth for function calls. /// /// This prevents infinite recursion by limiting how deep function - /// calls can be nested. Useful for controlling resource usage. + /// calls can be nested. Useful for controlling resource usage. The default is 10,000 in + /// release builds (256 in debug builds). pub fn set_max_call_stack_depth(&mut self, max_call_stack_depth: u32) { - #[cfg(not(feature = "tarn"))] - { - self.evaluator.options.max_call_stack_depth = max_call_stack_depth; - } - #[cfg(feature = "tarn")] - { - self.vm.options.max_call_stack_depth = max_call_stack_depth; - } + self.vm.options.max_call_stack_depth = max_call_stack_depth; } /// Set the maximum wall-clock duration allowed for a single `eval` call. @@ -291,20 +231,11 @@ impl Engine { /// `RuntimeError::Timeout`; the deadline is checked periodically inside loops and /// function calls, so it may be exceeded slightly before evaluation actually stops. pub fn set_timeout(&mut self, timeout: std::time::Duration) { - #[cfg(not(feature = "tarn"))] - { - self.evaluator.options.timeout = Some(timeout); - } - #[cfg(feature = "tarn")] - { - self.vm.options.timeout = Some(timeout); - } + self.vm.options.timeout = Some(timeout); } /// Makes top-level `let`/`var`/`def` bindings from one `eval()` call visible to the next - /// (e.g. a REPL). Opt-in since it costs a capture/reseed pass per call; the tree-walking - /// evaluator does this for free via its persistent `Env`. - #[cfg(feature = "tarn")] + /// (e.g. a REPL). Opt-in since it costs a capture/reseed pass per call. pub fn enable_query_session(&mut self) { self.vm.session = Some(Shared::new(SharedCell::new(Vec::new()))); } @@ -319,12 +250,7 @@ impl Engine { /// [`DefaultModuleResolver::with_io`] when constructing the resolver so /// local-filesystem module resolution is gated consistently. pub fn set_io(&mut self, io: Shared) { - #[cfg(feature = "tarn")] - { - self.vm.io = Shared::clone(&io); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.set_io(io); + self.vm.io = Shared::clone(&io); } /// Set search paths for module loading. @@ -332,14 +258,9 @@ impl Engine { /// These paths will be searched when loading external modules /// via the `include` statement in mq code. pub fn set_search_paths(&mut self, paths: Vec) { - #[cfg(feature = "tarn")] - { - self.vm.module_loader.set_search_paths(paths.clone()); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.set_search_paths(paths); + self.vm.module_loader.set_search_paths(paths); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); } /// Define a string variable that can be used in mq code. @@ -352,10 +273,7 @@ impl Engine { /// Defines an arbitrary runtime value in the current environment. pub fn define_value(&self, name: &str, value: RuntimeValue) { - #[cfg(feature = "tarn")] - self.vm.define(crate::Ident::new(name), value.clone()); - #[cfg(not(feature = "tarn"))] - self.evaluator.define_value(name, value); + self.vm.define(crate::Ident::new(name), value); } /// Registers a native Rust function under `name`, callable from mq code as `name(...)`. @@ -409,22 +327,10 @@ impl Engine { { let name = name.into(); let f = f.into_host_fn(); - #[cfg(feature = "tarn")] - { - #[cfg(not(feature = "sync"))] - self.vm - .host_functions - .borrow_mut() - .insert_shared(name, Shared::clone(&f)); - #[cfg(feature = "sync")] - self.vm - .host_functions - .write() - .unwrap() - .insert_shared(name, Shared::clone(&f)); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.register_fn(name, f); + #[cfg(not(feature = "sync"))] + self.vm.host_functions.borrow_mut().insert_shared(name, f); + #[cfg(feature = "sync")] + self.vm.host_functions.write().unwrap().insert_shared(name, f); } /// Load the built-in function modules. @@ -432,13 +338,7 @@ impl Engine { /// This must be called to enable access to standard functions /// like `add`, `sub`, `map`, `filter`, etc. pub fn load_builtin_module(&mut self) { - // The VM's module loader is independent of `Evaluator`'s. - #[cfg(feature = "tarn")] self.vm.load_builtin_module(Shared::clone(&self.token_arena)); - #[cfg(not(feature = "tarn"))] - self.evaluator - .load_builtin_module() - .expect("Failed to load builtin module"); } /// Import an external module by name. @@ -446,35 +346,13 @@ impl Engine { /// The module will be searched for in the configured search paths /// and made available for use in mq code. pub fn import_module(&mut self, module_name: &str) -> Result<(), Box> { - #[cfg(feature = "tarn")] - { - self.vm - .module_loader - .load_from_file(module_name, Shared::clone(&self.token_arena)) - .map_err(|e| Box::new(error::Error::from_error("", e.into(), self.vm.module_loader.clone())))?; - self.vm_module_prelude - .push(VmModulePrelude::Import(module_name.to_string())); - Ok(()) - } - - #[cfg(not(feature = "tarn"))] - { - let module = self - .evaluator - .module_loader - .load_from_file(module_name, Shared::clone(&self.token_arena)); - let module = - module.map_err(|e| error::Error::from_error("", e.into(), self.evaluator.module_loader.clone()))?; - - let _ = self.evaluator.import_module(module).map_err(|e| { - Box::new(error::Error::from_error( - "", - e.into(), - self.evaluator.module_loader.clone(), - )) - })?; - Ok(()) - } + self.vm + .module_loader + .load_from_file(module_name, Shared::clone(&self.token_arena)) + .map_err(|e| Box::new(error::Error::from_error("", e.into(), self.vm.module_loader.clone())))?; + self.vm_module_prelude + .push(VmModulePrelude::Import(module_name.to_string())); + Ok(()) } /// Load an external module by name. @@ -482,35 +360,13 @@ impl Engine { /// The module will be searched for in the configured search paths /// and made available for use in mq code. pub fn load_module(&mut self, module_name: &str) -> Result<(), Box> { - #[cfg(feature = "tarn")] - { - self.vm - .module_loader - .load_from_file(module_name, Shared::clone(&self.token_arena)) - .map_err(|e| Box::new(error::Error::from_error("", e.into(), self.vm.module_loader.clone())))?; - self.vm_module_prelude - .push(VmModulePrelude::Include(module_name.to_string())); - Ok(()) - } - - #[cfg(not(feature = "tarn"))] - { - let module = self - .evaluator - .module_loader - .load_from_file(module_name, Shared::clone(&self.token_arena)); - let module = - module.map_err(|e| error::Error::from_error("", e.into(), self.evaluator.module_loader.clone()))?; - - self.evaluator.load_module(module).map_err(|e| { - Box::new(error::Error::from_error( - "", - e.into(), - self.evaluator.module_loader.clone(), - )) - })?; - Ok(()) - } + self.vm + .module_loader + .load_from_file(module_name, Shared::clone(&self.token_arena)) + .map_err(|e| Box::new(error::Error::from_error("", e.into(), self.vm.module_loader.clone())))?; + self.vm_module_prelude + .push(VmModulePrelude::Include(module_name.to_string())); + Ok(()) } /// The main engine for evaluating mq code. @@ -534,36 +390,21 @@ impl Engine { return Ok(vec![].into()); } - // Scoped before `parse`, not just `evaluator.eval`, so bare `$VAR` resolution sees this engine's `Io`. - #[cfg(feature = "tarn")] + // Scoped before `parse`, not just `eval_compiled_vm`, so bare `$VAR` resolution sees this engine's `Io`. let _io_guard = io_context::scoped(Shared::clone(&self.vm.io) as Shared); - #[cfg(not(feature = "tarn"))] - let _io_guard = io_context::scoped(Shared::clone(&self.evaluator.io) as Shared); let program = parse(code, Shared::clone(&self.token_arena))?; let program = Optimizer::with_level(self.optimization_level).optimize(program); - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] self.vm.module_loader.set_source_code(code.to_string()); - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - self.evaluator.module_loader.set_source_code(code.to_string()); - #[cfg(feature = "tarn")] - { - let compiled = CompiledProgram { - source: code.to_string(), - program, - #[cfg(all(feature = "tarn", not(feature = "debugger")))] - vm_cache: None, - }; - self.eval_compiled_vm(&compiled, input.into_iter()) - } - #[cfg(not(feature = "tarn"))] - { - self.evaluator - .eval(&program, input.into_iter()) - .map(|values| values.into()) - .map_err(|e| Box::new(error::Error::from_error(code, e, self.evaluator.module_loader.clone()))) - } + let compiled = CompiledProgram { + source: code.to_string(), + program, + #[cfg(not(feature = "debugger"))] + vm_cache: None, + }; + self.eval_compiled_vm(&compiled, input.into_iter()) } /// Compiles mq code into a [`CompiledProgram`] that can be evaluated multiple times. @@ -574,20 +415,17 @@ impl Engine { return Ok(CompiledProgram { source: String::new(), program: vec![], - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] vm_cache: Some(Shared::new(SharedCell::new(None))), }); } - #[cfg(feature = "tarn")] let _io_guard = io_context::scoped(Shared::clone(&self.vm.io) as Shared); - #[cfg(not(feature = "tarn"))] - let _io_guard = io_context::scoped(Shared::clone(&self.evaluator.io) as Shared); let program = parse(code, Shared::clone(&self.token_arena))?; let program = Optimizer::with_level(self.optimization_level).optimize(program); Ok(CompiledProgram { source: code.to_string(), program, - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] vm_cache: Some(Shared::new(SharedCell::new(None))), }) } @@ -613,28 +451,10 @@ impl Engine { compiled: &CompiledProgram, input: I, ) -> MqResult { - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] self.vm.module_loader.set_source_code(compiled.source.clone()); - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - self.evaluator.module_loader.set_source_code(compiled.source.clone()); - #[cfg(feature = "tarn")] - { - self.eval_compiled_vm(compiled, input) - } - #[cfg(not(feature = "tarn"))] - { - self.evaluator - .eval(&compiled.program, input) - .map(|values| values.into()) - .map_err(|e| { - Box::new(error::Error::from_error( - &compiled.source, - e, - self.evaluator.module_loader.clone(), - )) - }) - } + self.eval_compiled_vm(compiled, input) } /// Renders the Tarn bytecode that would be executed for `compiled`. @@ -667,9 +487,7 @@ impl Engine { }) } - /// Evaluates one input through the bytecode VM (`bytecode-vm` feature). Same `MqResult` - /// shape as `eval_compiled`. - #[cfg(feature = "tarn")] + /// Evaluates one input through the bytecode VM. Same `MqResult` shape as `eval_compiled`. pub(crate) fn eval_compiled_vm(&mut self, compiled: &CompiledProgram, input: I) -> MqResult where I: Iterator, @@ -752,47 +570,29 @@ impl Engine { /// This allows interactive debugging of mq code execution when the /// `debugger` feature is enabled. Use this to inspect or control /// the execution state for advanced debugging scenarios. - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] pub fn debugger(&self) -> Shared> { Shared::clone(&self.vm.debugger) } - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn debugger(&self) -> Shared> { - Shared::clone(&self.evaluator.debugger) - } - - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] pub fn set_debugger_handler(&mut self, handler: Box) { self.vm.debugger_handler = Shared::new(SharedCell::new(handler)); } - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn set_debugger_handler(&mut self, handler: Box) { - self.evaluator.debugger_handler = Shared::new(SharedCell::new(handler)); - } - #[cfg(feature = "debugger")] pub fn token_arena(&self) -> Shared>>> { Shared::clone(&self.token_arena) } - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] pub fn get_module_name(&self, module_id: ModuleId) -> Cow<'static, str> { self.vm.module_loader.module_name(module_id) } - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn get_module_name(&self, module_id: ModuleId) -> Cow<'static, str> { - self.evaluator.module_loader.module_name(module_id) - } - #[cfg(feature = "debugger")] pub fn get_source_code_for_debug(&self, module_id: ModuleId) -> Result> { - #[cfg(feature = "tarn")] let module_loader = &self.vm.module_loader; - #[cfg(not(feature = "tarn"))] - let module_loader = &self.evaluator.module_loader; let source_code = module_loader.get_source_code_for_debug(module_id); source_code.map_err(|e| Box::new(error::Error::from_error("", e.into(), module_loader.clone()))) @@ -800,10 +600,7 @@ impl Engine { /// Resolves `module_name` to the path its resolver loaded it from. pub fn get_module_path(&self, module_name: &str) -> Result> { - #[cfg(feature = "tarn")] let module_loader = &self.vm.module_loader; - #[cfg(not(feature = "tarn"))] - let module_loader = &self.evaluator.module_loader; module_loader .get_module_path(module_name) .map_err(|e| Box::new(error::Error::from_error("", e.into(), module_loader.clone()))) @@ -821,14 +618,9 @@ impl Engine { /// An empty list restricts access to the built-in default domain /// (`raw.githubusercontent.com/harehare`) only; it does not open up all URLs. pub fn set_http_allowed_domains(&mut self, domains: Vec) { - #[cfg(feature = "tarn")] - { - self.vm.module_loader.set_http_allowed_domains(domains.clone()); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.set_http_allowed_domains(domains); + self.vm.module_loader.set_http_allowed_domains(domains); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); } /// Enables or disables HTTP module imports outright, independent of the domain allowlist. @@ -836,14 +628,9 @@ impl Engine { /// The `mq` CLI calls this with `false` unless `--allow-http-import` is passed, so /// imports are opt-in there; disabled regardless of `--allowed-domain`. pub fn set_http_import_enabled(&mut self, enabled: bool) { - #[cfg(feature = "tarn")] - { - self.vm.module_loader.set_http_import_enabled(enabled); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.set_http_import_enabled(enabled); + self.vm.module_loader.set_http_import_enabled(enabled); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); } /// Clears all locally-cached HTTP module files. @@ -851,42 +638,27 @@ impl Engine { /// Call this once before processing to force a re-fetch of all cached modules /// on the next resolve (e.g. when `--refresh-modules` is passed on the CLI). pub fn clear_http_cache(&mut self) -> Result<(), crate::module::error::ModuleError> { - #[cfg(feature = "tarn")] - { - let result = self.vm.module_loader.clear_http_cache(); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - result - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.clear_http_cache() + let result = self.vm.module_loader.clear_http_cache(); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); + result } /// Clears all HTTP module cache including versioned modules and lock files. /// /// Use this when `--clear-cache` is passed on the CLI to wipe everything. pub fn clear_http_cache_all(&mut self) -> Result<(), crate::module::error::ModuleError> { - #[cfg(feature = "tarn")] - { - let result = self.vm.module_loader.clear_http_cache_all(); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - result - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.clear_http_cache_all() + let result = self.vm.module_loader.clear_http_cache_all(); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); + result } /// Enables or disables the `mq.lock` integrity check for HTTP imports (on by default). pub fn set_lockfile_enabled(&mut self, enabled: bool) { - #[cfg(feature = "tarn")] - { - self.vm.module_loader.set_lockfile_enabled(enabled); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.set_lockfile_enabled(enabled); + self.vm.module_loader.set_lockfile_enabled(enabled); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); } /// When `true`, a URL with no existing `mq.lock` entry is a hard error instead of being @@ -894,26 +666,16 @@ impl Engine { /// pass `--frozen` on the CLI so trusting a module's content for the first time /// only ever happens in a reviewable local run, not silently in CI. pub fn set_lockfile_frozen(&mut self, frozen: bool) { - #[cfg(feature = "tarn")] - { - self.vm.module_loader.set_lockfile_frozen(frozen); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.set_lockfile_frozen(frozen); + self.vm.module_loader.set_lockfile_frozen(frozen); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); } /// Sets the path used for `mq.lock`. pub fn set_lockfile_path(&mut self, path: std::path::PathBuf) { - #[cfg(feature = "tarn")] - { - self.vm.module_loader.set_lockfile_path(path.clone()); - #[cfg(not(feature = "debugger"))] - self.vm.invalidate_module_cache(); - } - #[cfg(not(feature = "tarn"))] - self.evaluator.module_loader.set_lockfile_path(path); + self.vm.module_loader.set_lockfile_path(path); + #[cfg(not(feature = "debugger"))] + self.vm.invalidate_module_cache(); } } @@ -944,42 +706,27 @@ mod tests { let mut engine = DefaultEngine::default(); let paths = vec![PathBuf::from("/test/path")]; engine.set_search_paths(paths.clone()); - #[cfg(feature = "tarn")] assert_eq!(engine.vm.module_loader.search_paths(), paths); - #[cfg(not(feature = "tarn"))] - assert_eq!(engine.evaluator.module_loader.search_paths(), paths); } #[test] fn test_set_max_call_stack_depth() { let mut engine = DefaultEngine::default(); - #[cfg(feature = "tarn")] let default_depth = engine.vm.options.max_call_stack_depth; - #[cfg(not(feature = "tarn"))] - let default_depth = engine.evaluator.options.max_call_stack_depth; let new_depth = default_depth + 10; engine.set_max_call_stack_depth(new_depth); - #[cfg(feature = "tarn")] assert_eq!(engine.vm.options.max_call_stack_depth, new_depth); - #[cfg(not(feature = "tarn"))] - assert_eq!(engine.evaluator.options.max_call_stack_depth, new_depth); } #[test] fn test_set_timeout() { let mut engine = DefaultEngine::default(); - #[cfg(feature = "tarn")] assert_eq!(engine.vm.options.timeout, None); - #[cfg(not(feature = "tarn"))] - assert_eq!(engine.evaluator.options.timeout, None); let timeout = std::time::Duration::from_secs(1); engine.set_timeout(timeout); - #[cfg(feature = "tarn")] assert_eq!(engine.vm.options.timeout, Some(timeout)); - #[cfg(not(feature = "tarn"))] - assert_eq!(engine.evaluator.options.timeout, Some(timeout)); } #[rstest] @@ -1050,11 +797,7 @@ mod tests { #[test] fn test_default_value_recursion_is_call_depth_limited() { let mut engine = DefaultEngine::default(); - // Tree-walker uses more native stack per depth than Tarn. - #[cfg(feature = "tarn")] engine.set_max_call_stack_depth(50); - #[cfg(not(feature = "tarn"))] - engine.set_max_call_stack_depth(32); let result = engine.eval("def f(x = f()): x; | f()", vec!["".to_string().into()].into_iter()); @@ -1064,7 +807,6 @@ mod tests { )); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_persists_let_across_eval_calls() { let mut engine = DefaultEngine::default(); @@ -1078,7 +820,6 @@ mod tests { assert_eq!(result.unwrap(), vec![42.into()].into()); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_persists_var_mutation_across_eval_calls() { let mut engine = DefaultEngine::default(); @@ -1093,7 +834,6 @@ mod tests { assert_eq!(result.unwrap(), vec![2.into()].into()); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_var_redeclaration_updates_value() { let mut engine = DefaultEngine::default(); @@ -1110,7 +850,6 @@ mod tests { assert_eq!(result.unwrap(), vec![2.into()].into()); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_persists_def_across_eval_calls() { let mut engine = DefaultEngine::default(); @@ -1124,7 +863,6 @@ mod tests { assert_eq!(result.unwrap(), vec![6.into()].into()); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_bindings_are_available_and_updated_across_nodes() { let mut engine = DefaultEngine::default(); @@ -1145,7 +883,6 @@ mod tests { assert_eq!(persisted.values(), &[RuntimeValue::Number(42.into())]); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_var_mutation_carries_forward_across_inputs_in_one_eval() { let mut engine = DefaultEngine::default(); @@ -1165,7 +902,6 @@ mod tests { assert_eq!(persisted.unwrap(), vec![3.into()].into()); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_empty_input_iterator_preserves_bindings() { let mut engine = DefaultEngine::default(); @@ -1181,7 +917,6 @@ mod tests { assert_eq!(persisted.unwrap(), vec![1.into()].into()); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_nodes_over_empty_input_preserves_let_binding() { let mut engine = DefaultEngine::default(); @@ -1202,7 +937,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_nodes_over_empty_input_preserves_var_binding() { let mut engine = DefaultEngine::default(); @@ -1222,7 +956,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_uncached_nodes_split_preserves_let_immutability() { let mut engine = DefaultEngine::default(); @@ -1236,7 +969,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_nodes_split_captures_as_bindings() { let mut engine = DefaultEngine::default(); @@ -1250,7 +982,6 @@ mod tests { assert_eq!(values.values(), &[RuntimeValue::Number(2.into())]); } - #[cfg(feature = "tarn")] #[test] fn test_query_session_is_opt_in() { let mut engine = DefaultEngine::default(); @@ -1564,7 +1295,7 @@ mod tests { let program = vec![Shared::new(AstNode { token_id: crate::arena::ArenaId::new(1), - expr: Shared::new(AstExpr::Literal(AstLiteral::String("hello".to_string()))), + expr: AstExpr::Literal(AstLiteral::String("hello".to_string())), })]; let compiled = CompiledProgram::from(program); @@ -1597,49 +1328,7 @@ mod tests { handle.join().expect("Threaded engine usage failed"); } - // `switch_env` evaluates an ad-hoc expression against a paused frame's *live*, dynamic - // `Env` — inherently tree-walker-specific (the VM resolves names to slots statically at - // compile time, so it has no equivalent for "a name defined into a live scope at - // runtime"). Replacing it is tracked as still-open work before `tarn` is safe to make - // the default; not attempted here. - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - #[test] - fn test_switch_env() { - use crate::runtime::env::Env; - use crate::{RuntimeValue, Shared, SharedCell, null_input}; - - let engine = DefaultEngine::default(); - let env = Shared::new(SharedCell::new(Env::default())); - - env.write().unwrap().define("runtime".into(), RuntimeValue::NONE); - - let mut new_engine = engine.switch_env(env); - - assert_eq!( - new_engine.eval("runtime", null_input().into_iter()).unwrap()[0], - RuntimeValue::NONE - ); - } - - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - #[test] - fn test_eval_debug_expression_tree_walker() { - use crate::runtime::env::Env; - use crate::{RuntimeValue, Shared, SharedCell}; - - let mut engine = DefaultEngine::default(); - let env = Shared::new(SharedCell::new(Env::default())); - env.write().unwrap().define("runtime".into(), RuntimeValue::NONE); - - assert_eq!( - engine - .eval_debug_expression("runtime", RuntimeValue::NONE, &env) - .unwrap()[0], - RuntimeValue::NONE - ); - } - - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] #[test] fn test_eval_debug_expression_vm() { use crate::RuntimeValue; @@ -1655,7 +1344,7 @@ mod tests { ); } - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] #[test] fn test_eval_debug_expression_vm_sees_current_value_as_self() { use crate::RuntimeValue; @@ -1670,7 +1359,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_eval_compiled_vm_error_is_a_real_miette_diagnostic() { use crate::RuntimeValue; @@ -1678,13 +1366,8 @@ mod tests { // `eval_compiled_vm` used to return a bare `Result<_, String>` — this checks it now // produces the same public `error::Error` shape `eval_compiled` does: a real cause // (not just a `Display` string) with a non-trivial source span pointing at the - // failing expression, matching the tree-walker's own error for the same program. + // failing expression. let code = "1 | 1 / 0"; - let mut tree_walk_engine = DefaultEngine::default(); - let tree_walk_err = tree_walk_engine - .eval(code, std::iter::once(RuntimeValue::None)) - .unwrap_err(); - let mut engine = DefaultEngine::default(); let compiled = engine.compile(code).unwrap(); let vm_err = engine @@ -1695,15 +1378,12 @@ mod tests { vm_err.cause, error::InnerError::Runtime(error::runtime::RuntimeError::ZeroDivision(_)) )); - assert_eq!(vm_err.to_string(), tree_walk_err.to_string()); - assert_eq!(vm_err.location, tree_walk_err.location); assert!( !vm_err.location.is_empty(), "span should cover the failing expression, not be empty" ); } - #[cfg(feature = "tarn")] #[test] fn test_eval_compiled_vm_uses_engine_host_functions() { use crate::RuntimeValue; @@ -1729,7 +1409,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_vm_caches_module_free_bytecode() { use crate::RuntimeValue; @@ -1757,7 +1437,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_cache_miss_shares_deadline_between_compile_and_run() { use crate::RuntimeValue; @@ -1793,7 +1473,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_vm_cached_bytecode_preserves_markdown_input_handling() { let mut engine = DefaultEngine::default(); @@ -1806,7 +1486,7 @@ mod tests { assert_eq!(second, first); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_vm_keeps_external_module_bytecode_frozen() { use crate::RuntimeValue; @@ -1841,11 +1521,11 @@ mod tests { assert_eq!( second.values(), &[RuntimeValue::String(Shared::new("first".to_string()))], - "a cached query keeps the module source it compiled, matching the tree-walker" + "a cached query keeps the module source it compiled" ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_cached_vm_does_not_share_frozen_modules_between_engines() { use crate::RuntimeValue; @@ -1877,7 +1557,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_vm_caches_engine_loaded_module_bytecode() { use crate::RuntimeValue; @@ -1895,7 +1575,7 @@ mod tests { assert_eq!(second.values(), first.values()); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_vm_caches_a_program_with_nodes() { use crate::RuntimeValue; @@ -1919,7 +1599,7 @@ mod tests { assert_eq!(second.values(), first.values()); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_eval_compiled_vm_recompiles_after_search_paths_change_on_same_engine() { use crate::RuntimeValue; @@ -1952,7 +1632,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_module_resolution_setters_invalidate_module_cache_key() { let mut engine = DefaultEngine::default(); @@ -2017,7 +1697,6 @@ mod tests { } } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_runs_once_per_eval_across_nodes_split() { use std::sync::Arc; @@ -2068,7 +1747,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_runs_once_per_eval_without_nodes() { use std::sync::Arc; @@ -2120,7 +1798,6 @@ mod tests { assert_eq!(result.values(), &[expected.clone(), expected.clone(), expected]); } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_runs_once_per_eval_for_import() { use std::sync::Arc; @@ -2170,7 +1847,7 @@ mod tests { assert_eq!(call_count.load(Ordering::SeqCst) - baseline, 1); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_module_var_initializer_value_is_pinned_across_cached_eval_calls() { use std::sync::Arc; @@ -2221,7 +1898,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_runs_once_per_eval_with_query_session() { use std::sync::Arc; @@ -2267,7 +1943,6 @@ mod tests { assert_eq!(call_count.load(Ordering::SeqCst) - baseline, 1); } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_runs_once_per_eval_for_aliased_import() { use std::sync::Arc; @@ -2313,7 +1988,6 @@ mod tests { assert_eq!(call_count.load(Ordering::SeqCst), 1); } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_error_propagates() { let (temp_dir, temp_file_path) = create_file( @@ -2334,7 +2008,7 @@ mod tests { }); // Reference the module inline (not via `Engine::load_module`, which would eagerly - // prepare it through the tree-walker and fail before Tarn's own resolution runs). + // prepare it and fail before Tarn's own resolution runs). let compiled = engine .compile(r#"include "erroring_var_module" | get_value()"#) .unwrap(); @@ -2345,7 +2019,6 @@ mod tests { assert!(err.to_string().contains("something went wrong"), "{err}"); } - #[cfg(feature = "tarn")] #[test] fn test_inline_module_var_initializer_referencing_enclosing_def_runs_once_per_eval() { use std::sync::Arc; @@ -2382,7 +2055,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_inline_module_var_initializer_runtime_error_is_not_silently_retried() { use std::sync::Arc; @@ -2417,7 +2089,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_inline_module_var_initializer_runs_once_per_eval_across_nodes_split() { use std::sync::Arc; @@ -2454,7 +2125,6 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_module_var_initializer_runs_once_per_eval_when_imported_inside_an_inline_module() { use std::sync::Arc; @@ -2499,7 +2169,6 @@ mod tests { assert_eq!(call_count.load(Ordering::SeqCst), 1); } - #[cfg(feature = "tarn")] #[test] fn test_nested_external_module_var_initializer_runs_once_per_eval() { use std::sync::Arc; @@ -2546,7 +2215,6 @@ mod tests { assert_eq!(result.values().len(), 3); } - #[cfg(feature = "tarn")] #[test] fn test_nested_inline_module_var_initializers_run_once_each_per_eval() { use std::sync::Arc; @@ -2581,7 +2249,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_cached_nodes_split_preserves_let_immutability() { let mut engine = DefaultEngine::default(); @@ -2596,7 +2264,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_cached_nodes_split_keeps_a_shadowing_var_mutable() { let mut engine = DefaultEngine::default(); @@ -2610,7 +2278,6 @@ mod tests { // `eval_compiled_vm` reads `define_value`/`define_string_value` bindings from `self.vm`, // which only exists under `tarn`. - #[cfg(feature = "tarn")] #[test] fn test_eval_compiled_vm_resolves_names_defined_via_define_value() { use crate::RuntimeValue; @@ -2651,7 +2318,7 @@ mod tests { assert!(compiled.cached_vm_program().is_some_and(|cache| cache.is_some())); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_cached_vm_reuses_current_globals_for_every_input() { use crate::RuntimeValue; @@ -2680,7 +2347,7 @@ mod tests { assert_eq!(values.values(), &[RuntimeValue::Number(101.into())]); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_cached_vm_keeps_global_environments_separate_between_engines() { use crate::RuntimeValue; @@ -2717,7 +2384,7 @@ mod tests { ); } - #[cfg(all(feature = "tarn", not(feature = "debugger")))] + #[cfg(not(feature = "debugger"))] #[test] fn test_cached_vm_does_not_reuse_an_environment_after_its_engine_drops() { use crate::RuntimeValue; @@ -2747,16 +2414,14 @@ mod tests { ); } - #[cfg(feature = "tarn")] #[test] fn test_eval_compiled_vm_resolves_a_local_file_import() { use crate::RuntimeValue; - // Mirrors `test_eval_import_as_alias` (the tree-walker's own local-file import - // test), but through `eval_compiled_vm` — this only works because `eval_compiled_vm` - // threads `self.vm.module_loader.clone()` into the VM compiler instead of - // it hardcoding an in-memory-only `StdModuleResolver` (`STANDARD_MODULES` only, no - // filesystem access). + // Mirrors `test_eval_import_as_alias`, but through `eval_compiled_vm` — this only works + // because `eval_compiled_vm` threads `self.vm.module_loader.clone()` into the VM + // compiler instead of it hardcoding an in-memory-only `StdModuleResolver` + // (`STANDARD_MODULES` only, no filesystem access). let (temp_dir, temp_file_path) = create_file( "greeter_vm_engine_test.mq", r#"def greet(name): "Hello, " + name + "!";"#, @@ -2784,7 +2449,7 @@ mod tests { ); } - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] #[test] fn test_eval_compiled_vm_notifies_debugger_of_uncaught_error() { use crate::{DebugContext, DebuggerHandler, RuntimeValue}; diff --git a/crates/mq-lang/src/eval.rs b/crates/mq-lang/src/eval.rs deleted file mode 100644 index f5e005d9c..000000000 --- a/crates/mq-lang/src/eval.rs +++ /dev/null @@ -1,8625 +0,0 @@ -use std::borrow::Cow; -use std::collections::BTreeMap; -use std::sync::LazyLock; -use std::time::Duration; -#[cfg(not(target_arch = "wasm32"))] -use std::time::Instant; -#[cfg(target_arch = "wasm32")] -use web_time::Instant; - -use crate::Module; -use crate::ast::constants; -use crate::io::{Io, NativeIo, SandboxedIo}; -use crate::module::resolver::DefaultModuleResolver; -#[cfg(feature = "debugger")] -use crate::parse; -use crate::runtime::builtin::{self, io_context}; -#[cfg(feature = "debugger")] -use crate::runtime::debugger::DefaultDebuggerHandler; -#[cfg(feature = "debugger")] -use crate::runtime::debugger::Source; -use crate::runtime::host::{self, HostFunctions}; -#[cfg(feature = "debugger")] -use crate::{Debugger, DebuggerHandler}; -use crate::{ - Ident, Program, Shared, SharedCell, Token, TokenKind, - arena::Arena, - ast::{ - TokenId, - node::{self as ast, Branches, MatchArms, Pattern}, - }, - error::InnerError, - get_token, -}; -use crate::{ - IdentWithToken, ModuleResolver, - error::runtime::RuntimeError, - module::{self, error::ModuleError}, - runtime::{env::EnvError, runtime_value::ModuleEnv}, - selector::Selector, -}; - -#[cfg(feature = "debugger")] -use crate::runtime::debugger::{Breakpoint, DebugContext}; - -use crate::runtime::env::Env; -use crate::runtime::runtime_value::{self, RuntimeValue}; - -/// Number of loop iterations / function calls between wall-clock deadline checks. -/// Must be a power of two so the check is a cheap bitmask instead of a modulo. -const TIMEOUT_CHECK_INTERVAL: u32 = 1024; - -static TYPE_IDENT: LazyLock = LazyLock::new(|| Ident::new("type")); -static DYNAMIC_IDENT: LazyLock = LazyLock::new(|| Ident::new("")); -static SPREAD_IDENT: LazyLock = LazyLock::new(|| Ident::new(constants::builtins::SPREAD)); -static ARRAY_IDENT: LazyLock = LazyLock::new(|| Ident::new(constants::builtins::ARRAY)); -static DICT_IDENT: LazyLock = LazyLock::new(|| Ident::new(constants::builtins::DICT)); -static ERROR_MESSAGE_IDENT: LazyLock = LazyLock::new(|| Ident::new("message")); - -/// Control flow signals for internal evaluation. -/// -/// These are not errors, but signals used within the evaluator -/// to handle `break` and `continue` statements in loops. -#[derive(Debug, Clone, PartialEq)] -pub(crate) enum ControlFlow { - /// Signal to break out of a loop, with the token for error reporting and an optional value. - /// The value is boxed to keep the size of the enum small. - Break(Token, Option>), - /// Signal to continue to the next iteration, with the token for error reporting. - Continue(Token), -} - -/// Internal evaluation error that combines control flow and runtime errors. -/// -/// This type is used internally by the evaluator and should not be exposed -/// to external consumers. External APIs convert this to `RuntimeError`. -#[derive(Debug)] -pub(crate) enum EvalError { - /// Control flow signal (break/continue). - Flow(ControlFlow), - /// Runtime error. - Runtime(RuntimeError), -} - -impl From for EvalError { - fn from(err: RuntimeError) -> Self { - EvalError::Runtime(err) - } -} - -impl EvalError { - /// Converts to RuntimeError, treating control flow as unexpected. - pub(crate) fn into_runtime_error(self) -> RuntimeError { - match self { - EvalError::Flow(ControlFlow::Break(token, _)) => RuntimeError::UnexpectedBreak(token), - EvalError::Flow(ControlFlow::Continue(token)) => RuntimeError::UnexpectedContinue(token), - EvalError::Runtime(err) => err, - } - } - - /// Converts to InnerError for external API. - #[cfg_attr(feature = "tarn", allow(dead_code))] - pub(crate) fn into_inner_error(self) -> InnerError { - InnerError::from(self.into_runtime_error()) - } -} - -/// Result type for internal evaluation functions. -pub(crate) type EvalResult = Result; - -/// Outcome of evaluating a matched breakpoint's `condition`/`hit_condition`/`log_message`. -#[cfg(feature = "debugger")] -enum BreakpointDecision { - /// The `condition` or `hit_condition` was not satisfied; keep running unnoticed. - Skip, - /// A logpoint fired; execution continues but the interpolated message should be reported. - Log(String), - /// Execution should pause and invoke `DebuggerHandler::on_breakpoint_hit`. - Stop, -} - -/// Configuration options for the evaluator. -#[derive(Debug, Clone)] -pub struct Options { - /// Maximum depth of the call stack to prevent infinite recursion. - pub max_call_stack_depth: u32, - /// Maximum wall-clock duration for a single evaluation. Disabled (`None`) by default; - /// checked periodically, so a run may overshoot the deadline slightly. - pub timeout: Option, -} - -#[cfg(debug_assertions)] -impl Default for Options { - fn default() -> Self { - Self { - max_call_stack_depth: 40, - timeout: None, - } - } -} - -#[cfg(not(debug_assertions))] -impl Default for Options { - fn default() -> Self { - Self { - max_call_stack_depth: 192, - timeout: None, - } - } -} - -/// The AST evaluator for executing mq programs. -/// -/// Evaluates abstract syntax trees and manages the runtime environment, -/// including variable bindings, function calls, and module loading. -/// -/// Fully independent of Tarn (the bytecode VM) — no shared state with `Engine`'s VM side. -/// Slated for removal once Tarn is the sole engine. -#[derive(Debug)] -pub struct Evaluator> { - env: Shared>, - token_arena: Shared>>>, - - call_stack_depth: u32, - /// Deadline for the current `eval` call; `None` if no timeout is set. - deadline: Option, - /// Step counter so `Instant::now()` is only sampled every `TIMEOUT_CHECK_INTERVAL` steps. - timeout_step: u32, - pub(crate) options: Options, - pub(crate) module_loader: module::ModuleLoader, - /// Ambient during `eval()` for builtins to reach (see [`io_context`]). Defaults to an - /// all-denied [`SandboxedIo`], matching the fail-safe default the retired process-wide - /// capability flags used to have. Statically dispatched: `IO` is fixed per `Evaluator` - /// instantiation rather than a `dyn Io` trait object. - pub(crate) io: Shared, - /// Host-registered native functions, consulted in [`Self::eval_fn`] when a called - /// identifier isn't a local binding, before falling back to the built-in table. Empty by - /// default: a host must opt in via [`crate::Engine::register_fn`]. - pub(crate) host_functions: Shared>, - - #[cfg(feature = "debugger")] - pub(crate) debugger: Shared>, - #[cfg(feature = "debugger")] - pub(crate) debugger_handler: Shared>>, -} - -impl Default for Evaluator { - fn default() -> Self { - Self { - env: Shared::new(SharedCell::new(Env::default())), - token_arena: Shared::new(SharedCell::new(Arena::new(10))), - call_stack_depth: 0, - deadline: None, - timeout_step: 0, - options: Options::default(), - module_loader: module::ModuleLoader::new(T::default()), - io: Shared::new(IO::default()), - host_functions: Shared::new(SharedCell::new(HostFunctions::default())), - #[cfg_attr(feature = "sync", allow(clippy::arc_with_non_send_sync))] - #[cfg(feature = "debugger")] - debugger: Shared::new(SharedCell::new(Debugger::new())), - #[cfg(feature = "debugger")] - debugger_handler: Shared::new(SharedCell::new(Box::new(DefaultDebuggerHandler))), - } - } -} - -impl Clone for Evaluator { - fn clone(&self) -> Self { - Self { - env: Shared::clone(&self.env), - token_arena: Shared::clone(&self.token_arena), - call_stack_depth: self.call_stack_depth, - deadline: self.deadline, - timeout_step: self.timeout_step, - options: self.options.clone(), - module_loader: self.module_loader.clone(), - io: Shared::clone(&self.io), - host_functions: Shared::clone(&self.host_functions), - #[cfg(feature = "debugger")] - debugger: Shared::clone(&self.debugger), - #[cfg(feature = "debugger")] - debugger_handler: Shared::clone(&self.debugger_handler), - } - } -} - -impl Evaluator> { - pub(crate) fn new( - module_loader: module::ModuleLoader, - token_arena: Shared>>>, - ) -> Self { - Self { - module_loader, - token_arena, - ..Default::default() - } - } - - #[allow(unused)] - pub(crate) fn with_env( - token_arena: Shared>>>, - env: Shared>, - ) -> Self { - Self { - token_arena, - env: Shared::clone(&env), - ..Default::default() - } - } -} - -impl Evaluator { - /// Like the [`SandboxedIo`]-pinned [`Evaluator::new`], but generic over `IO` - /// and takes the [`Io`] value up front — for hosts that need to select the `Io` *type* - /// at construction time (not just its value via [`set_io`](Self::set_io)), e.g. a test - /// runner installing an in-memory mock instead of the default sandboxed real filesystem. - pub(crate) fn with_io( - module_loader: module::ModuleLoader, - token_arena: Shared>>>, - io: Shared, - ) -> Self { - Self { - env: Shared::new(SharedCell::new(Env::default())), - token_arena, - call_stack_depth: 0, - deadline: None, - timeout_step: 0, - options: Options::default(), - module_loader, - io, - host_functions: Shared::new(SharedCell::new(HostFunctions::default())), - #[cfg_attr(feature = "sync", allow(clippy::arc_with_non_send_sync))] - #[cfg(feature = "debugger")] - debugger: Shared::new(SharedCell::new(Debugger::new())), - #[cfg(feature = "debugger")] - debugger_handler: Shared::new(SharedCell::new(Box::new(DefaultDebuggerHandler))), - } - } - - /// Sets the [`Io`] used by builtins (via ambient access, see [`io_context`]) for the - /// duration of `eval()` calls on this evaluator. - pub(crate) fn set_io(&mut self, io: Shared) { - self.io = io; - } - - /// Registers a native Rust function under `name`, callable from mq code as `name(...)`. - /// See [`crate::Engine::register_fn`]. - pub(crate) fn register_fn(&self, name: impl Into, f: Shared) { - #[cfg(not(feature = "sync"))] - self.host_functions.borrow_mut().insert_shared(name, f); - #[cfg(feature = "sync")] - self.host_functions.write().unwrap().insert_shared(name, f); - } - #[cfg_attr(feature = "tarn", allow(dead_code))] - pub(crate) fn eval(&mut self, program: &Program, input: I) -> Result, InnerError> - where - I: Iterator, - { - let result = self.eval_body(program, input); - - #[cfg(feature = "debugger")] - if let Err(ref e) = result { - self.notify_uncaught_error(e); - } - - result - } - - /// Notifies the debugger handler of an error that propagated all the way out of - /// [`Self::eval`], i.e. one not caught by any `try`/`catch` in the query. A no-op unless - /// the debugger is active. - #[cfg(feature = "debugger")] - fn notify_uncaught_error(&mut self, error: &InnerError) { - if !self.debugger.read().unwrap().is_active() { - return; - } - - let token = match error.token() { - Some(token) => Shared::new(token.clone()), - None => Shared::new(Token { - kind: TokenKind::Eof, - range: crate::Range::default(), - module_id: Module::TOP_LEVEL_MODULE_ID, - }), - }; - let context = DebugContext { - token: Shared::clone(&token), - call_stack: self.debugger.read().unwrap().current_call_stack(), - #[cfg(not(feature = "tarn"))] - env: Shared::clone(&self.env), - source: Source { - name: if token.module_id == Module::TOP_LEVEL_MODULE_ID { - None - } else { - Some(self.module_loader.module_name(token.module_id).to_string()) - }, - code: self - .module_loader - .get_source_code_for_debug(token.module_id) - .unwrap_or_default(), - }, - ..Default::default() - }; - - self.debugger_handler - .read() - .unwrap() - .on_error(&error.to_string(), &context); - } - - #[cfg_attr(feature = "tarn", allow(dead_code))] - fn eval_body(&mut self, program: &Program, input: I) -> Result, InnerError> - where - I: Iterator, - { - let _io_guard = io_context::scoped(Shared::clone(&self.io) as Shared); - self.deadline = self.options.timeout.map(|timeout| Instant::now() + timeout); - self.timeout_step = 0; - - // First pass: handle includes and imports, collect other nodes - let program = program.iter().try_fold( - Vec::with_capacity(program.len()), - |mut nodes: Vec>, node: &Shared| -> Result<_, InnerError> { - match &*node.expr { - ast::Expr::Include(module_id) => { - self.eval_include(module_id.to_owned(), &Shared::clone(&self.env)) - .map_err(InnerError::from)?; - } - ast::Expr::Import(module_path, alias) => { - self.eval_import( - module_path.to_owned(), - alias.as_ref().map(|ident| ident.name), - &Shared::clone(&self.env), - ) - .map_err(|e| e.into_inner_error())?; - } - ast::Expr::Module(ident, program) => { - self.eval_module(&RuntimeValue::NONE, ident, program, &Shared::clone(&self.env)) - .map_err(|e| e.into_inner_error())?; - } - _ => nodes.push(Shared::clone(node)), - }; - - Ok(nodes) - }, - )?; - - // Register function definitions - let mut program = program.iter().try_fold( - Vec::with_capacity(program.len()), - |mut nodes: Vec>, node: &Shared| -> Result<_, InnerError> { - match &*node.expr { - ast::Expr::Def(ident, params, program) => { - define( - &self.env, - ident.name, - RuntimeValue::new_function( - Shared::new(params.clone()), - Shared::new(program.clone()), - Shared::clone(&self.env), - ), - ); - } - _ => nodes.push(Shared::clone(node)), - }; - - Ok(nodes) - }, - )?; - let nodes_index = &program.iter().position(|node| node.is_nodes()); - - if let Some(index) = nodes_index { - let (program, nodes_program) = program.split_at_mut(*index); - let program = program.to_vec(); - let nodes_program = nodes_program.to_vec(); - - let values: Result, InnerError> = input - .map(|runtime_value| match &runtime_value { - RuntimeValue::Markdown(node, _) => self.eval_markdown_node(&program, node), - _ => self - .eval_program(&program, runtime_value, &Shared::clone(&self.env)) - .map_err(|e| e.into_inner_error()), - }) - .collect(); - - if nodes_program.is_empty() { - values - } else { - self.eval_program(&nodes_program, values?.into(), &Shared::clone(&self.env)) - .map(|value| { - if let RuntimeValue::Array(values) = value { - Shared::unwrap_or_clone(values) - } else { - vec![value] - } - }) - .map_err(|e| e.into_inner_error()) - } - } else { - input - .map(|runtime_value| match &runtime_value { - RuntimeValue::Markdown(node, _) => self.eval_markdown_node(&program, node), - _ => self - .eval_program(&program, runtime_value, &Shared::clone(&self.env)) - .map_err(|e| e.into_inner_error()), - }) - .collect() - } - } - - #[inline(always)] - #[cfg_attr(feature = "tarn", allow(dead_code))] - fn eval_markdown_node(&mut self, program: &Program, node: &mq_markdown::Node) -> Result { - node.map_values(&mut |child_node| { - let value = self - .eval_program( - program, - RuntimeValue::new_markdown(child_node.clone()), - &Shared::clone(&self.env), - ) - .map_err(|e| e.into_inner_error())?; - - Ok(match value { - RuntimeValue::None => child_node.to_fragment(), - RuntimeValue::Function(_) | RuntimeValue::NativeFunction(_) | RuntimeValue::Module(_) => { - mq_markdown::Node::Empty - } - #[cfg(feature = "tarn")] - RuntimeValue::VmClosure(_) => mq_markdown::Node::Empty, - RuntimeValue::Array(arr) => arr - .iter() - .filter_map(|v| if v.is_none() { None } else { Some(v.to_string()) }) - .collect::>() - .join("\n") - .into(), - RuntimeValue::Dict(_) - | RuntimeValue::Boolean(_) - | RuntimeValue::Number(_) - | RuntimeValue::String(_) - | RuntimeValue::Bytes(_) => value.to_string().into(), - RuntimeValue::Symbol(i) => i.as_str().into(), - RuntimeValue::Markdown(node, _) => Shared::unwrap_or_clone(node), - }) - }) - .map(RuntimeValue::new_markdown) - } - - /// Defines a runtime value in the current environment. - pub fn define_value(&self, name: &str, value: RuntimeValue) { - define(&self.env, Ident::new(name), value); - } - - #[cfg(not(feature = "tarn"))] - pub(crate) fn load_builtin_module(&mut self) -> Result<(), RuntimeError> { - match self.module_loader.load_builtin(Shared::clone(&self.token_arena)) { - Ok(module) => self.load_module(module), - Err(ModuleError::AlreadyLoaded(_)) => Ok(()), - Err(e) => Err(e.into()), - } - } - - /// Always the full population, regardless of feature config — for tests that use the - /// tree-walker directly as an oracle even in a `tarn`-enabled build. - #[cfg(all(test, feature = "tarn"))] - pub(crate) fn load_builtin_module_full(&mut self) -> Result<(), RuntimeError> { - match self.module_loader.load_builtin(Shared::clone(&self.token_arena)) { - Ok(module) => self.load_module(module), - Err(ModuleError::AlreadyLoaded(_)) => Ok(()), - Err(e) => Err(e.into()), - } - } - - pub(crate) fn load_module(&mut self, module: module::Module) -> Result<(), RuntimeError> { - self.load_module_with_env(module, &Shared::clone(&self.env)) - } - - pub(crate) fn import_module(&mut self, module: Module) -> Result { - self.import_module_with_env(module, None, &Shared::clone(&self.env)) - .map_err(|e| e.into_runtime_error()) - } - - pub(crate) fn load_module_with_env( - &mut self, - module: module::Module, - env: &Shared>, - ) -> Result<(), RuntimeError> { - for node in &module.modules { - let _ = match &*node.expr { - ast::Expr::Include(_) => self - .eval_expr(&RuntimeValue::NONE, node, env) - .map_err(|e| e.into_runtime_error())?, - ast::Expr::Module(ident, program) => self - .eval_module(&RuntimeValue::NONE, ident, program, env) - .map_err(|e| e.into_runtime_error())?, - ast::Expr::Import(module_path, alias) => self - .eval_import(module_path.to_owned(), alias.as_ref().map(|ident| ident.name), env) - .map_err(|e| e.into_runtime_error())?, - _ => { - return Err(RuntimeError::InternalError( - (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - )); - } - }; - } - - for node in &module.functions { - if let ast::Expr::Def(ident, params, program) = &*node.expr { - define( - env, - ident.name, - RuntimeValue::new_function( - Shared::new(params.clone()), - Shared::new(program.clone()), - Shared::clone(env), - ), - ); - } - } - - for node in &module.vars { - if let ast::Expr::Let(pattern, rhs) = &*node.expr { - let val = self - .eval_expr(&RuntimeValue::NONE, rhs, env) - .map_err(|e| e.into_runtime_error())?; - if let Pattern::Ident(ident) = pattern { - define(env, ident.name, val); - } else { - match self.match_pattern(&val, pattern)? { - Some(bindings) => { - for (name, bound_val) in bindings { - define(env, name, bound_val); - } - } - None => { - let token = (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(); - return Err(RuntimeError::DestructuringFailed(token)); - } - } - } - } else { - return Err(RuntimeError::InternalError( - (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - )); - } - } - - Ok(()) - } - - #[inline(always)] - fn eval_program( - &mut self, - program: &Program, - runtime_value: RuntimeValue, - env: &Shared>, - ) -> EvalResult { - let mut value = runtime_value; - for expr in program { - let new_value = self.eval_expr(&value, expr, env)?; - value = self.maybe_auto_call_pipeline_ident(new_value, &value, expr, env)?; - } - Ok(value) - } - - #[inline(always)] - fn auto_call_ident(expr: &Shared) -> Option { - match &*expr.expr { - ast::Expr::Ident(ident) => Some(ident.name), - ast::Expr::QualifiedAccess(_, ast::AccessTarget::Ident(ident)) => Some(ident.name), - _ => None, - } - } - - #[inline] - fn maybe_auto_call_pipeline_ident( - &mut self, - value: RuntimeValue, - runtime_value: &RuntimeValue, - expr: &Shared, - env: &Shared>, - ) -> EvalResult { - match &value { - RuntimeValue::Function(f) => { - let Some(ident) = Self::auto_call_ident(expr) else { - return Ok(value); - }; - - let required_params = f - .params - .iter() - .filter(|p| p.default.is_none() && !p.is_variadic) - .take(2) - .count(); - if required_params > 1 { - return Ok(value); - } - - self.call_fn( - &value, - Shared::clone(expr), - ident, - &ast::Args::new(), - runtime_value, - env, - ) - } - RuntimeValue::NativeFunction(native_ident) => { - let can_auto_call = builtin::get_builtin_functions(native_ident) - .is_some_and(|f| f.num_params.is_valid(0) || f.num_params.is_missing_one_params(0)); - if !can_auto_call || Self::auto_call_ident(expr).is_none() { - return Ok(value); - } - - self.eval_builtin(runtime_value, Shared::clone(expr), native_ident, &ast::Args::new(), env) - } - _ => Ok(value), - } - } - - #[inline(always)] - fn eval_ident(&self, ident: Ident, token_id: TokenId, env: &Shared>) -> EvalResult { - #[cfg(not(feature = "sync"))] - { - env.borrow() - .resolve(ident) - .map_err(|e| EvalError::from(e.to_runtime_error(token_id, Shared::clone(&self.token_arena)))) - } - #[cfg(feature = "sync")] - { - env.read() - .unwrap() - .resolve(ident) - .map_err(|e| EvalError::from(e.to_runtime_error(token_id, Shared::clone(&self.token_arena)))) - } - } - - #[inline(never)] - #[cfg(feature = "debugger")] - fn eval_debugger(&self, runtime_value: &RuntimeValue, node: Shared, _env: Shared>) { - let current_call_stack = self.debugger.read().unwrap().current_call_stack(); - let token = get_token(Shared::clone(&self.token_arena), node.token_id); - - let debug_context = DebugContext { - current_value: runtime_value.clone(), - current_node: Shared::clone(&node), - token: Shared::clone(&token), - call_stack: current_call_stack, - #[cfg(not(feature = "tarn"))] - env: Shared::clone(&_env), - #[cfg(feature = "tarn")] - vm_frame: Default::default(), - #[cfg(feature = "debug-trace")] - operand_stack: Vec::new(), - source: Source { - name: if token.module_id == Module::TOP_LEVEL_MODULE_ID { - None - } else { - Some(self.module_loader.module_name(token.module_id).to_string()) - }, - code: self - .module_loader - .get_source_code_for_debug(token.module_id) - .unwrap_or_default(), - }, - }; - let breakpoint = Breakpoint { - id: 0, - line: token.range.start.line as usize, - column: Some(token.range.start.column), - enabled: true, - source: None, - ..Default::default() - }; - - let next_action = self - .debugger_handler - .read() - .unwrap() - .on_breakpoint_hit(&breakpoint, &debug_context); - self.debugger.write().unwrap().next(next_action); - } - - #[inline(always)] - fn eval_include(&mut self, module: ast::Literal, env: &Shared>) -> Result<(), RuntimeError> { - match module { - ast::Literal::String(module_name) => { - let module = self - .module_loader - .load_from_file(&module_name, Shared::clone(&self.token_arena))?; - #[cfg(feature = "http-import")] - self.module_loader.push_http_boundary(); - let result = self.load_module_with_env(module, env); - #[cfg(feature = "http-import")] - self.module_loader.pop_http_boundary(); - result - } - _ => Err(RuntimeError::ModuleLoadError(ModuleError::InvalidModule)), - } - } - - fn eval_module( - &mut self, - runtime_value: &RuntimeValue, - ident: &IdentWithToken, - program: &Program, - env: &Shared>, - ) -> EvalResult { - let module_name_to_use = &ident.name.as_str(); - - let module_env = if let Ok(RuntimeValue::Module(module_env)) = resolve(module_name_to_use, env) { - Shared::clone(module_env.exports()) - } else { - // Create a new environment for the module exports - Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))) - }; - - for node in program { - match &*node.expr { - ast::Expr::Include(_) => { - self.eval_expr(&RuntimeValue::NONE, node, &Shared::clone(&module_env))?; - } - ast::Expr::Def(ident, params, program) => { - define( - &module_env, - ident.name, - RuntimeValue::new_function( - Shared::new(params.clone()), - Shared::new(program.clone()), - Shared::clone(&module_env), - ), - ); - } - ast::Expr::Let(pattern, rhs) => { - let val = self.eval_expr(&RuntimeValue::NONE, rhs, &Shared::clone(&module_env))?; - if let Pattern::Ident(ident) = pattern { - define(&module_env, ident.name, val); - } else { - match self.match_pattern(&val, pattern)? { - Some(bindings) => { - for (name, bound_val) in bindings { - define(&module_env, name, bound_val); - } - } - None => { - let token = (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(); - return Err(RuntimeError::DestructuringFailed(token).into()); - } - } - } - } - ast::Expr::Import(module_path, alias) => { - self.eval_import( - module_path.to_owned(), - alias.as_ref().map(|ident| ident.name), - &Shared::clone(&module_env), - )?; - } - ast::Expr::Module(ident, program) => { - let _ = self.eval_module(&RuntimeValue::NONE, ident, program, &module_env)?; - } - _ => {} - } - } - - // Register the module in the environment - let module_runtime_value = RuntimeValue::Module(Shared::new(runtime_value::ModuleEnv::new( - module_name_to_use, - Shared::clone(&module_env), - ))); - - define(&self.env, Ident::new(module_name_to_use), module_runtime_value); - - Ok(runtime_value.clone()) - } - - fn import_module_with_env( - &mut self, - module: Module, - alias: Option, - env: &Shared>, - ) -> EvalResult { - // Create a new environment for the module exports - let module_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - let module_name_to_use = module.name.to_string(); - - self.load_module_with_env(module, &Shared::clone(&module_env))?; - - // Register the module in the environment - let module_runtime_value = RuntimeValue::Module(Shared::new(runtime_value::ModuleEnv::new( - &module_name_to_use, - Shared::clone(&module_env), - ))); - - // The alias, if given, rebinds the module under that name instead of its canonical one. - let bind_name = alias.unwrap_or_else(|| Ident::new(&module_name_to_use)); - define(&self.env, bind_name, module_runtime_value); - - Ok(RuntimeValue::Module(Shared::new(ModuleEnv::new( - &module_name_to_use, - module_env, - )))) - } - - fn eval_import( - &mut self, - module_path: ast::Literal, - alias: Option, - env: &Shared>, - ) -> EvalResult { - match module_path { - ast::Literal::String(module_name) => { - let module = self - .module_loader - .load_from_file(&module_name, Shared::clone(&self.token_arena)); - match module { - Ok(module) => { - #[cfg(feature = "http-import")] - self.module_loader.push_http_boundary(); - let result = self.import_module_with_env(module, alias, env); - #[cfg(feature = "http-import")] - self.module_loader.pop_http_boundary(); - result - } - Err(ModuleError::AlreadyLoaded(_)) => { - let canonical = self.module_loader.canonical_name(&module_name).to_owned(); - match resolve(&canonical, env) { - Ok(value) => { - // Already loaded under its canonical name; also bind the alias if given. - if let Some(alias) = alias { - define(env, alias, value.clone()); - } - Ok(value) - } - Err(_) => { - Err(RuntimeError::ModuleLoadError(ModuleError::NotFound(Cow::Owned(canonical))).into()) - } - } - } - Err(e) => Err(RuntimeError::ModuleLoadError(e).into()), - } - } - _ => Err(RuntimeError::ModuleLoadError(ModuleError::InvalidModule).into()), - } - } - - fn eval_qualified_access( - &mut self, - runtime_value: &RuntimeValue, - module_path: &[ast::IdentWithToken], - access_target: &ast::AccessTarget, - token_id: TokenId, - env: &Shared>, - ) -> EvalResult { - // Traverse the module path to get to the final module - let mut current_value = if let Some(first_module) = module_path.first() { - self.eval_ident(first_module.name, token_id, env)? - } else { - let token = get_token(Shared::clone(&self.token_arena), token_id); - return Err(RuntimeError::InternalError((*token).clone()).into()); - }; - - // Traverse nested modules - for module_ident in &module_path[1..] { - match current_value { - RuntimeValue::Module(module_env) => { - let module_exports = Shared::clone(module_env.exports()); - - #[cfg(not(feature = "sync"))] - let resolved = module_exports.borrow().resolve(module_ident.name); - #[cfg(feature = "sync")] - let resolved = module_exports.read().unwrap().resolve(module_ident.name); - - current_value = resolved - .map_err(|e| EvalError::from(e.to_runtime_error(token_id, Shared::clone(&self.token_arena))))?; - } - _ => { - let token = get_token(Shared::clone(&self.token_arena), token_id); - return Err(RuntimeError::NotDefined( - (*token).clone(), - module_ident.name.to_string(), - Box::new([]), - ) - .into()); - } - } - } - - // Now access the target from the final module - match current_value { - RuntimeValue::Module(module_env) => { - let module_exports = Shared::clone(module_env.exports()); - - match access_target { - ast::AccessTarget::Call(func_name, args) => { - // Resolve function from module exports and call it - #[cfg(not(feature = "sync"))] - let resolved = module_exports.borrow().resolve(func_name.name); - #[cfg(feature = "sync")] - let resolved = module_exports.read().unwrap().resolve(func_name.name); - - match resolved { - Ok(fn_value) => { - // Create a dummy node for the function call - let call_node = Shared::new(ast::Node { - token_id, - expr: Shared::new(ast::Expr::Call(func_name.clone(), args.clone())), - }); - self.call_fn(&fn_value, call_node, func_name.name, args, runtime_value, env) - } - Err(_) => { - let token = func_name - .token - .as_ref() - .cloned() - .unwrap_or(get_token(Shared::clone(&self.token_arena), token_id)); - #[cfg(not(feature = "sync"))] - let candidates = module_exports.borrow().defined_names(); - #[cfg(feature = "sync")] - let candidates = module_exports.read().unwrap().defined_names(); - - Err(RuntimeError::NotDefined( - (*token).clone(), - func_name.name.to_string(), - candidates.into_boxed_slice(), - ) - .into()) - } - } - } - ast::AccessTarget::Ident(ident) => { - // Resolve value from module exports - #[cfg(not(feature = "sync"))] - let resolved = module_exports.borrow().resolve(ident.name); - #[cfg(feature = "sync")] - let resolved = module_exports.read().unwrap().resolve(ident.name); - - resolved.map_err(|e| { - EvalError::from(e.to_runtime_error(token_id, Shared::clone(&self.token_arena))) - }) - } - } - } - _ => { - let (token, last_module) = module_path - .last() - .map(|m| (m.token.clone(), m.name.to_string())) - .unwrap_or_default(); - let token = token.unwrap_or(get_token(Shared::clone(&self.token_arena), token_id)); - Err(RuntimeError::NotDefined((*token).clone(), last_module, Box::new([])).into()) - } - } - } - - #[inline(always)] - fn eval_selector_expr_with_args( - runtime_value: &RuntimeValue, - selector: &Selector, - args: &[RuntimeValue], - ) -> RuntimeValue { - match runtime_value { - RuntimeValue::Markdown(node_value, _) => builtin::eval_selector_with_args(node_value, selector, args), - RuntimeValue::Array(values) => { - if let Selector::List(Some(idx), None) = selector { - return values.get(*idx).cloned().unwrap_or(RuntimeValue::NONE); - } - let values = values - .iter() - .flat_map(|value| match value { - RuntimeValue::Markdown(node_value, _) => { - match builtin::eval_selector_with_args(node_value, selector, args) { - RuntimeValue::Array(arr) => Shared::unwrap_or_clone(arr), - other => vec![other], - } - } - _ if matches!(selector, Selector::List(None, None)) && args.is_empty() => { - vec![value.clone()] - } - RuntimeValue::Dict(_) => { - vec![Self::eval_selector_expr_with_args(value, selector, args)] - } - _ => vec![RuntimeValue::NONE], - }) - .collect::>(); - RuntimeValue::Array(Shared::new(values)) - } - RuntimeValue::Dict(map) => { - let new_map: BTreeMap<_, _> = map - .iter() - .map(|(k, v)| { - let new_v = if *k == *TYPE_IDENT { - v.clone() - } else { - Self::eval_selector_expr_with_args(v, selector, args) - }; - (*k, new_v) - }) - .collect(); - if new_map.is_empty() { - RuntimeValue::NONE - } else { - RuntimeValue::Dict(Shared::new(new_map)) - } - } - _ => RuntimeValue::NONE, - } - } - - fn eval_property_selector_expr(runtime_value: &RuntimeValue, property_name: &Ident) -> RuntimeValue { - match runtime_value { - RuntimeValue::Array(values) => { - let values = values - .iter() - .map(|value| match value { - RuntimeValue::Dict(_) => Self::eval_property_selector_expr(value, property_name), - _ => RuntimeValue::NONE, - }) - .collect::>(); - RuntimeValue::Array(Shared::new(values)) - } - RuntimeValue::Dict(map) => map.get(property_name).cloned().unwrap_or(RuntimeValue::NONE), - _ => RuntimeValue::NONE, - } - } - - /// Collects the value itself and all nested values recursively (depth-first). - fn collect_recursive(value: &RuntimeValue) -> Vec { - let mut result = vec![value.clone()]; - match value { - RuntimeValue::Array(items) => { - for item in items.iter() { - result.extend(Self::collect_recursive(item)); - } - } - RuntimeValue::Dict(map) => { - for v in map.values() { - result.extend(Self::collect_recursive(v)); - } - } - _ => {} - } - result - } - - fn eval_selector_expr(runtime_value: &RuntimeValue, selector: &Selector) -> RuntimeValue { - if let Selector::Property(property_name) = selector { - return Self::eval_property_selector_expr(runtime_value, property_name); - } - - match runtime_value { - RuntimeValue::Markdown(node_value, _) => builtin::eval_selector(node_value, selector), - RuntimeValue::Array(values) => { - if let Selector::List(Some(idx), None) = selector { - return values.get(*idx).cloned().unwrap_or(RuntimeValue::NONE); - } - let values = values - .iter() - .flat_map(|value| match value { - RuntimeValue::Markdown(node_value, _) => match builtin::eval_selector(node_value, selector) { - RuntimeValue::Array(arr) => Shared::unwrap_or_clone(arr), - other => vec![other], - }, - _ if matches!(selector, Selector::List(None, None)) => { - vec![value.clone()] - } - RuntimeValue::Dict(_) => match Self::eval_selector_expr(value, selector) { - RuntimeValue::Array(arr) if matches!(selector, Selector::Recursive) => { - Shared::unwrap_or_clone(arr) - } - other => vec![other], - }, - _ => vec![RuntimeValue::NONE], - }) - .collect::>(); - - RuntimeValue::Array(Shared::new(values)) - } - RuntimeValue::Dict(map) => { - if matches!(selector, Selector::List(None, None)) { - return RuntimeValue::Array(Shared::new(map.values().cloned().collect())); - } - if matches!(selector, Selector::Recursive) { - return RuntimeValue::Array(Shared::new(Self::collect_recursive(runtime_value))); - } - let new_map: BTreeMap<_, _> = map - .iter() - .map(|(k, v)| { - let new_v = if *k == *TYPE_IDENT { - v.clone() - } else { - Self::eval_selector_expr(v, selector) - }; - (*k, new_v) - }) - .collect(); - - if new_map.is_empty() { - RuntimeValue::NONE - } else { - RuntimeValue::Dict(Shared::new(new_map)) - } - } - _ => RuntimeValue::NONE, - } - } - - fn eval_interpolated_string( - &mut self, - runtime_value: &RuntimeValue, - segments: &[ast::StringSegment], - token_id: TokenId, - env: &Shared>, - ) -> EvalResult { - // Calculate estimated capacity based on segment content - let estimated_capacity = segments - .iter() - .map(|segment| match segment { - ast::StringSegment::Text(s) => s.len(), - ast::StringSegment::Expr(_) => 32, // Estimated size for expression result - ast::StringSegment::Env(_) => 32, // Estimated size for environment variable - ast::StringSegment::Self_ => 64, // Estimated size for self reference - }) - .sum(); - - segments - .iter() - .try_fold(String::with_capacity(estimated_capacity), |mut acc, segment| { - match segment { - ast::StringSegment::Text(s) => acc.push_str(s), - ast::StringSegment::Expr(expr_node) => { - let value = self.eval_expr(runtime_value, expr_node, env)?; - match &value { - RuntimeValue::String(s) => acc.push_str(s), - _ => acc.push_str(&value.to_string()), - } - } - ast::StringSegment::Env(env_var) => { - acc.push_str(&io_context::current().env_var(env_var).map_err(|e| { - let token = (*get_token(Shared::clone(&self.token_arena), token_id)).clone(); - match e { - crate::io::IoError::PermissionDenied(msg) => { - RuntimeError::Runtime(token, msg.into_owned()) - } - _ => RuntimeError::EnvNotFound(token, env_var.clone()), - } - })?); - } - ast::StringSegment::Self_ => match runtime_value { - RuntimeValue::String(s) => acc.push_str(s), - _ => acc.push_str(&runtime_value.to_string()), - }, - } - - Ok(acc) - }) - .map(|acc| acc.into()) - } - - /// Decides what a matched breakpoint should do: evaluates its `condition` and - /// `hit_condition` (if any) and, for logpoints, interpolates the `log_message`. - #[cfg(feature = "debugger")] - fn eval_breakpoint( - &mut self, - breakpoint: &Breakpoint, - runtime_value: &RuntimeValue, - token: &Shared, - env: &Shared>, - ) -> Result { - if let Some(condition) = &breakpoint.condition { - let value = self.eval_debug_expr(condition, token, env)?; - if !value.is_truthy() { - return Ok(BreakpointDecision::Skip); - } - } - - if let Some(hit_condition) = &breakpoint.hit_condition { - let count = self.debugger.write().unwrap().record_hit(breakpoint.id); - if !self.eval_hit_condition(hit_condition, count, token, env)? { - return Ok(BreakpointDecision::Skip); - } - } - - if let Some(log_message) = &breakpoint.log_message { - return Ok(BreakpointDecision::Log(self.interpolate_log_message( - log_message, - runtime_value, - token, - env, - )?)); - } - - Ok(BreakpointDecision::Stop) - } - - /// Parses and evaluates an mq expression (a breakpoint condition or a `${}` segment of a - /// logpoint message) against `env`, the environment active at the breakpoint location. - /// - /// Deactivates the debugger for the duration so the expression can't re-trigger breakpoint - /// handling, and temporarily swaps in `env` so in-scope identifiers resolve correctly. - #[cfg(feature = "debugger")] - fn eval_debug_expr( - &mut self, - code: &str, - token: &Shared, - env: &Shared>, - ) -> Result { - let program = parse(code, Shared::clone(&self.token_arena)).map_err(|e| { - RuntimeError::Runtime( - (**token).clone(), - format!("Invalid breakpoint expression \"{code}\": {e}"), - ) - })?; - - let saved_env = std::mem::replace(&mut self.env, Shared::clone(env)); - self.debugger.write().unwrap().deactivate(); - let result = self.eval(&program, std::iter::once(RuntimeValue::NONE)); - self.debugger.write().unwrap().activate(); - self.env = saved_env; - - let values = result.map_err(|e| { - RuntimeError::Runtime( - (**token).clone(), - format!("Failed to evaluate breakpoint expression \"{code}\": {e}"), - ) - })?; - Ok(values.into_iter().next_back().unwrap_or(RuntimeValue::NONE)) - } - - /// Evaluates a breakpoint's `hit_condition` against the current hit `count`. - /// - /// A bare integer (e.g. `"5"`) is shorthand for `hit_count >= 5`, matching the - /// hit-count-condition convention used by DAP clients such as VS Code. Anything else is - /// parsed and evaluated as a full mq expression with `hit_count` bound to `count` in a - /// child scope of `env`, so conditions can reference other in-scope variables too, e.g. - /// `hit_count >= 3 && x == 1`. - #[cfg(feature = "debugger")] - fn eval_hit_condition( - &mut self, - expr: &str, - count: usize, - token: &Shared, - env: &Shared>, - ) -> Result { - let trimmed = expr.trim(); - let code = match trimmed.parse::() { - Ok(threshold) => format!("hit_count >= {threshold}"), - Err(_) => trimmed.to_string(), - }; - - let hit_count_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - define( - &hit_count_env, - Ident::new("hit_count"), - RuntimeValue::Number(count.into()), - ); - - let value = self.eval_debug_expr(&code, token, &hit_count_env)?; - Ok(value.is_truthy()) - } - - /// Interpolates a logpoint message using mq's `${expr}` string interpolation syntax. - /// `${self}` is the current pipeline value, `${$VAR}` reads an env var, and any other - /// `${expr}` is evaluated as an mq expression against `env`. - #[cfg(feature = "debugger")] - fn interpolate_log_message( - &mut self, - message: &str, - runtime_value: &RuntimeValue, - token: &Shared, - env: &Shared>, - ) -> Result { - let segments = crate::lexer::parse_interpolation_segments(message, token.module_id) - .map_err(|_| RuntimeError::Runtime((**token).clone(), format!("Invalid log message \"{message}\"")))?; - - segments - .iter() - .try_fold(String::with_capacity(message.len()), |mut acc, segment| { - match segment { - crate::lexer::token::StringSegment::Text(text, _) => acc.push_str(text), - crate::lexer::token::StringSegment::Expr(expr_str, _) => { - let expr_str = expr_str.trim(); - - if expr_str == constants::identifiers::SELF { - acc.push_str(&runtime_value.to_string()); - } else if let Some(var) = expr_str.strip_prefix('$') { - acc.push_str(&io_context::current().env_var(var).map_err(|e| match e { - crate::io::IoError::PermissionDenied(msg) => { - RuntimeError::Runtime((**token).clone(), msg.into_owned()) - } - _ => RuntimeError::EnvNotFound((**token).clone(), var.into()), - })?); - } else { - let value = self.eval_debug_expr(expr_str, token, env)?; - acc.push_str(&value.to_string()); - } - } - } - - Ok(acc) - }) - } - - fn eval_expr( - &mut self, - runtime_value: &RuntimeValue, - node: &Shared, - env: &Shared>, - ) -> EvalResult { - #[cfg(feature = "debugger")] - if self.debugger.read().unwrap().is_active() { - let token = &get_token(Shared::clone(&self.token_arena), node.token_id); - let call_stack = self.debugger.read().unwrap().current_call_stack(); - let debug_context = DebugContext { - current_value: runtime_value.clone(), - current_node: Shared::clone(node), - token: Shared::clone(token), - call_stack, - #[cfg(not(feature = "tarn"))] - env: Shared::clone(env), - #[cfg(feature = "tarn")] - vm_frame: Default::default(), - #[cfg(feature = "debug-trace")] - operand_stack: Vec::new(), - source: Source { - name: if token.module_id == Module::TOP_LEVEL_MODULE_ID { - None - } else { - Some(self.module_loader.module_name(token.module_id).to_string()) - }, - code: self - .module_loader - .get_source_code_for_debug(token.module_id) - .unwrap_or_default(), - }, - }; - - let breakpoint = self - .debugger - .read() - .unwrap() - .get_hit_breakpoint(&debug_context, Shared::clone(token)); - - if let Some(breakpoint) = breakpoint { - match self.eval_breakpoint(&breakpoint, runtime_value, token, env)? { - BreakpointDecision::Skip => {} - BreakpointDecision::Log(message) => { - self.debugger_handler - .read() - .unwrap() - .on_log_point(&breakpoint, &message, &debug_context); - } - BreakpointDecision::Stop => { - let next_action = self - .debugger_handler - .read() - .unwrap() - .on_breakpoint_hit(&breakpoint, &debug_context); - self.debugger.write().unwrap().next(next_action); - } - } - } else if self.debugger.write().unwrap().should_break(&debug_context) { - let next_action = self.debugger_handler.read().unwrap().on_step(&debug_context); - self.debugger.write().unwrap().next(next_action); - } - } - - match &*node.expr { - ast::Expr::Selector(ident) => Ok(Self::eval_selector_expr(runtime_value, ident)), - ast::Expr::SelectorChain(selectors) => Ok(selectors - .iter() - .fold(runtime_value.clone(), |v, sel| Self::eval_selector_expr(&v, sel))), - ast::Expr::SelectorCall(selector, args) => { - let evaluated_args = args - .iter() - .map(|arg| self.eval_expr(runtime_value, arg, env)) - .collect::, _>>()?; - Ok(Self::eval_selector_expr_with_args( - runtime_value, - selector, - &evaluated_args, - )) - } - ast::Expr::Call(ident, args) => { - #[cfg(feature = "debugger")] - if ident.name == constants::builtins::BREAKPOINT.into() { - self.eval_debugger(runtime_value, Shared::clone(node), Shared::clone(env)); - return Ok(runtime_value.clone()); - } - - self.eval_fn(runtime_value, Shared::clone(node), ident.name, args, env) - } - ast::Expr::Ident(ident) => self.eval_ident(ident.name, node.token_id, env), - ast::Expr::Literal(literal) => Ok(self.eval_literal(literal)), - ast::Expr::Self_ | ast::Expr::Nodes => Ok(runtime_value.clone()), - ast::Expr::QualifiedAccess(module_name, access_target) => { - self.eval_qualified_access(runtime_value, module_name, access_target, node.token_id, env) - } - ast::Expr::Block(program) => { - let block_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - self.eval_program(program, runtime_value.clone(), &block_env) - } - ast::Expr::CallDynamic(callable, args) => self.eval_call_dynamic(runtime_value, callable, args, env), - ast::Expr::If(condition) => self.eval_if(runtime_value, condition, env), - ast::Expr::Unless(condition) => self.eval_unless(runtime_value, condition, env), - ast::Expr::Def(ident, params, program) => { - let function = RuntimeValue::new_function( - Shared::new(params.clone()), - Shared::new(program.clone()), - Shared::clone(env), - ); - define(env, ident.name, function.clone()); - Ok(function) - } - ast::Expr::Fn(params, program) => Ok(RuntimeValue::new_function( - Shared::new(params.clone()), - Shared::new(program.clone()), - Shared::clone(env), - )), - ast::Expr::As(ident, node) => { - let val = self.eval_expr(runtime_value, node, env)?; - define(env, ident.name, val); - Ok(runtime_value.clone()) - } - ast::Expr::Let(pattern, node) => { - let val = self.eval_expr(runtime_value, node, env)?; - if let Pattern::Ident(ident) = pattern { - define(env, ident.name, val); - } else { - match self.match_pattern(&val, pattern)? { - Some(bindings) => { - for (name, bound_val) in bindings { - define(env, name, bound_val); - } - } - None => { - let token = (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(); - return Err(RuntimeError::DestructuringFailed(token).into()); - } - } - } - Ok(runtime_value.clone()) - } - ast::Expr::Var(pattern, node) => { - let val = self.eval_expr(runtime_value, node, env)?; - if let Pattern::Ident(ident) = pattern { - define_mutable(env, ident.name, val); - } else { - match self.match_pattern(&val, pattern)? { - Some(bindings) => { - for (name, bound_val) in bindings { - define_mutable(env, name, bound_val); - } - } - None => { - let token = (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(); - return Err(RuntimeError::DestructuringFailed(token).into()); - } - } - } - Ok(runtime_value.clone()) - } - ast::Expr::Assign(ident, node) => { - let val = self.eval_expr(runtime_value, node, env)?; - #[cfg(not(feature = "sync"))] - { - env.borrow_mut().assign(ident.name, val).map_err(|e| { - e.to_runtime_error_with_token( - ident - .token - .as_ref() - .map(|t| (**t).clone()) - .unwrap_or((*get_token(Shared::clone(&self.token_arena), node.token_id)).clone()), - ) - })?; - } - - #[cfg(feature = "sync")] - { - env.write().unwrap().assign(ident.name, val).map_err(|e| { - e.to_runtime_error_with_token( - ident - .token - .as_ref() - .map(|t| (**t).clone()) - .unwrap_or((*get_token(Shared::clone(&self.token_arena), node.token_id)).clone()), - ) - })?; - } - Ok(runtime_value.clone()) - } - ast::Expr::And(operands) => self.eval_and(runtime_value, operands, env), - ast::Expr::Or(operands) => self.eval_or(runtime_value, operands, env), - ast::Expr::While(cond, program) => self.eval_while(runtime_value, cond, program, env), - ast::Expr::Until(cond, program) => self.eval_until(runtime_value, cond, program, env), - ast::Expr::Loop(program) => self.eval_loop(runtime_value, program, env), - ast::Expr::Try(try_expr, error_binder, catch_expr) => { - self.eval_try(runtime_value, try_expr, error_binder, catch_expr, env) - } - ast::Expr::Foreach(ident, values, body) => { - self.eval_foreach(runtime_value, ident.name, values, body, node.token_id, env) - } - ast::Expr::InterpolatedString(segments) => { - self.eval_interpolated_string(runtime_value, segments, node.token_id, env) - } - ast::Expr::Include(module_id) => { - self.eval_include(module_id.to_owned(), env)?; - Ok(runtime_value.clone()) - } - ast::Expr::Import(module_path, alias) => { - self.eval_import(module_path.to_owned(), alias.as_ref().map(|ident| ident.name), env) - } - ast::Expr::Module(ident, program) => self.eval_module(runtime_value, ident, program, env), - - ast::Expr::Match(value_node, arms) => self.eval_match(runtime_value, value_node, arms, env), - ast::Expr::Break(value_node) => { - let token = get_token(Shared::clone(&self.token_arena), node.token_id); - let value = match value_node { - Some(node) => Some(Box::new(self.eval_expr(runtime_value, node, env)?)), - None => None, - }; - Err(EvalError::Flow(ControlFlow::Break((*token).clone(), value))) - } - ast::Expr::Continue => { - let token = get_token(Shared::clone(&self.token_arena), node.token_id); - Err(EvalError::Flow(ControlFlow::Continue((*token).clone()))) - } - ast::Expr::Paren(expr) => self.eval_expr(runtime_value, expr, env), - } - } - - #[inline(always)] - fn eval_literal(&self, literal: &ast::Literal) -> RuntimeValue { - match literal { - ast::Literal::None => RuntimeValue::None, - ast::Literal::Bool(b) => RuntimeValue::Boolean(*b), - ast::Literal::String(s) => RuntimeValue::String(Shared::new(s.clone())), - ast::Literal::Bytes(b) => RuntimeValue::Bytes(Shared::new(b.clone())), - ast::Literal::Symbol(i) => RuntimeValue::Symbol(*i), - ast::Literal::Number(n) => RuntimeValue::Number(*n), - } - } - - #[inline(always)] - fn eval_and( - &mut self, - runtime_value: &RuntimeValue, - operands: &[Shared], - env: &Shared>, - ) -> EvalResult { - let mut last_value = RuntimeValue::Boolean(true); - for operand in operands { - last_value = self.eval_expr(runtime_value, operand, env)?; - if !last_value.is_truthy() { - return Ok(RuntimeValue::Boolean(false)); - } - } - Ok(last_value) - } - - #[inline(always)] - fn eval_or( - &mut self, - runtime_value: &RuntimeValue, - operands: &[Shared], - env: &Shared>, - ) -> EvalResult { - for operand in operands { - let value = self.eval_expr(runtime_value, operand, env)?; - if value.is_truthy() { - return Ok(value); - } - } - Ok(RuntimeValue::Boolean(false)) - } - - fn eval_foreach( - &mut self, - runtime_value: &RuntimeValue, - ident: Ident, - values: &Shared, - body: &Program, - token_id: TokenId, - env: &Shared>, - ) -> EvalResult { - let values = self.eval_expr(runtime_value, values, env)?; - let values = match values { - RuntimeValue::Array(values) => { - let env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - let mut results = Vec::with_capacity(values.len()); - - for value in Shared::unwrap_or_clone(values) { - self.check_timeout()?; - define(&env, ident, value.clone()); - match self.eval_program(body, value, &env) { - Ok(result) => results.push(result), - Err(EvalError::Flow(ControlFlow::Break(_, Some(v)))) => return Ok(*v), - Err(EvalError::Flow(ControlFlow::Break(_, None))) => break, - Err(EvalError::Flow(ControlFlow::Continue(_))) => continue, - Err(e) => return Err(e), - } - } - - results - } - RuntimeValue::String(s) => { - let env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - let mut results = Vec::with_capacity(s.len()); - - for c in s.chars() { - self.check_timeout()?; - define(&env, ident, RuntimeValue::String(Shared::new(c.to_string()))); - match self.eval_program(body, RuntimeValue::String(Shared::new(c.to_string())), &env) { - Ok(result) => results.push(result), - Err(EvalError::Flow(ControlFlow::Break(_, Some(v)))) => return Ok(*v), - Err(EvalError::Flow(ControlFlow::Break(_, None))) => break, - Err(EvalError::Flow(ControlFlow::Continue(_))) => continue, - Err(e) => return Err(e), - } - } - - results - } - _ => { - return Err(RuntimeError::InvalidTypes { - token: (*get_token(Shared::clone(&self.token_arena), token_id)).clone(), - name: TokenKind::Foreach.to_string(), - args: vec![values.to_string().into()], - } - .into()); - } - }; - - Ok(RuntimeValue::Array(Shared::new(values))) - } - - #[inline(always)] - fn eval_while( - &mut self, - runtime_value: &RuntimeValue, - cond: &Shared, - body: &Program, - env: &Shared>, - ) -> EvalResult { - self.eval_conditional_loop(runtime_value, cond, body, env, false) - } - - #[inline(always)] - fn eval_until( - &mut self, - runtime_value: &RuntimeValue, - cond: &Shared, - body: &Program, - env: &Shared>, - ) -> EvalResult { - self.eval_conditional_loop(runtime_value, cond, body, env, true) - } - - fn eval_conditional_loop( - &mut self, - runtime_value: &RuntimeValue, - cond: &Shared, - body: &Program, - env: &Shared>, - invert: bool, - ) -> EvalResult { - let mut runtime_value = runtime_value.clone(); - let env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - let mut cond_value = self.eval_expr(&runtime_value, cond, &env)?; - let should_run = |v: &RuntimeValue| v.is_truthy() != invert; - - if !should_run(&cond_value) { - return Ok(RuntimeValue::NONE); - } - let mut first = true; - - while should_run(&cond_value) { - self.check_timeout()?; - match self.eval_program(body, runtime_value.clone(), &env) { - Ok(mut new_runtime_value) => { - std::mem::swap(&mut runtime_value, &mut new_runtime_value); - cond_value = self.eval_expr(&runtime_value, cond, &env)?; - } - Err(EvalError::Flow(ControlFlow::Break(_, Some(v)))) => { - runtime_value = *v; - break; - } - Err(EvalError::Flow(ControlFlow::Break(_, None))) if first => { - runtime_value = RuntimeValue::NONE; - break; - } - Err(EvalError::Flow(ControlFlow::Break(_, None))) => break, - Err(EvalError::Flow(ControlFlow::Continue(_))) if first => { - runtime_value = RuntimeValue::NONE; - continue; - } - Err(EvalError::Flow(ControlFlow::Continue(_))) => continue, - Err(e) => return Err(e), - } - - first = false; - } - - Ok(runtime_value) - } - - fn eval_loop(&mut self, runtime_value: &RuntimeValue, body: &Program, env: &Shared>) -> EvalResult { - let mut runtime_value = runtime_value.clone(); - let env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - - loop { - self.check_timeout()?; - match self.eval_program(body, runtime_value.clone(), &env) { - Ok(mut new_runtime_value) => { - std::mem::swap(&mut runtime_value, &mut new_runtime_value); - } - Err(EvalError::Flow(ControlFlow::Break(_, Some(v)))) => { - runtime_value = *v; - break; - } - Err(EvalError::Flow(ControlFlow::Break(_, None))) => break, - Err(EvalError::Flow(ControlFlow::Continue(_))) => continue, - Err(e) => return Err(e), - } - } - - Ok(runtime_value) - } - - #[inline(always)] - fn eval_try( - &mut self, - runtime_value: &RuntimeValue, - try_expr: &Shared, - error_binder: &Option, - catch_expr: &Shared, - env: &Shared>, - ) -> EvalResult { - match self.eval_expr(runtime_value, try_expr, env) { - Ok(result) => Ok(result), - // Control flow signals (break/continue) are not errors; let them propagate. - Err(EvalError::Flow(flow)) => Err(EvalError::Flow(flow)), - Err(EvalError::Runtime(err)) => match error_binder { - Some(binder) => { - let mut error_dict = BTreeMap::new(); - error_dict.insert(*ERROR_MESSAGE_IDENT, RuntimeValue::String(Shared::new(err.to_string()))); - let catch_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - define(&catch_env, binder.name, RuntimeValue::Dict(Shared::new(error_dict))); - self.eval_expr(runtime_value, catch_expr, &catch_env) - } - None => self.eval_expr(runtime_value, catch_expr, env), - }, - } - } - - #[inline(always)] - fn eval_if( - &mut self, - runtime_value: &RuntimeValue, - conditions: &Branches, - env: &Shared>, - ) -> EvalResult { - for (cond_node, body) in conditions { - if let Some(result) = self.eval_branch(runtime_value, cond_node, body, env, false)? { - return Ok(result); - } - } - - Ok(RuntimeValue::NONE) - } - - #[inline(always)] - fn eval_unless( - &mut self, - runtime_value: &RuntimeValue, - conditions: &Branches, - env: &Shared>, - ) -> EvalResult { - if let Some((cond_node, body)) = conditions.first() - && let Some(result) = self.eval_branch(runtime_value, cond_node, body, env, true)? - { - return Ok(result); - } - - Ok(RuntimeValue::NONE) - } - - #[inline(always)] - fn eval_branch( - &mut self, - runtime_value: &RuntimeValue, - cond_node: &Option>, - body: &Shared, - env: &Shared>, - invert: bool, - ) -> Result, EvalError> { - match cond_node { - Some(cond_node) => { - let cond = self.eval_expr(runtime_value, cond_node, env)?; - - if cond.is_truthy() != invert { - return Ok(Some(self.eval_expr(runtime_value, body, env)?)); - } - - Ok(None) - } - None => Ok(Some(self.eval_expr(runtime_value, body, env)?)), - } - } - - fn eval_match( - &mut self, - runtime_value: &RuntimeValue, - value_node: &Shared, - arms: &MatchArms, - env: &Shared>, - ) -> EvalResult { - let match_value = self.eval_expr(runtime_value, value_node, env)?; - - // Try each arm in order - for arm in arms { - // Check if the pattern matches - if let Some(bindings) = self.match_pattern(&match_value, &arm.pattern)? { - // If there's a guard, evaluate it - if let Some(guard_node) = &arm.guard { - // Create a new environment with pattern bindings - let guard_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - for (name, value) in bindings.iter() { - define(&guard_env, *name, value.clone()); - } - - // Guard sees the matched value as `.`, so `.depth`/`.lang` work unbound. - let guard_result = self.eval_expr(&match_value, guard_node, &guard_env)?; - if !guard_result.is_truthy() { - // Guard failed, try next arm - continue; - } - } - - // Pattern matched (and guard passed if present), evaluate body - let body_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(env)))); - for (name, value) in bindings { - define(&body_env, name, value); - } - - return self.eval_expr(&match_value, &arm.body, &body_env); - } - } - - Ok(RuntimeValue::NONE) - } - - fn match_pattern( - &self, - value: &RuntimeValue, - pattern: &Pattern, - ) -> Result>, RuntimeError> { - match pattern { - Pattern::Wildcard => { - // Wildcard always matches, no bindings - Ok(Some(Vec::new())) - } - Pattern::Ident(ident) => { - // Identifier matches and binds the value - Ok(Some(vec![(ident.name, value.clone())])) - } - Pattern::Literal(lit) => { - // Literal pattern: check equality - let pattern_value = self.eval_literal(lit); - if *value == pattern_value { - Ok(Some(Vec::new())) - } else { - Ok(None) - } - } - Pattern::Type(type_name) => { - // Type pattern: check runtime type - let type_str = type_name.as_str(); - let matches = match type_str.as_str() { - "string" => matches!(value, RuntimeValue::String(_)), - "number" => matches!(value, RuntimeValue::Number(_)), - "bool" => matches!(value, RuntimeValue::Boolean(_)), - "array" => matches!(value, RuntimeValue::Array(_)), - "dict" => matches!(value, RuntimeValue::Dict(_)), - "bytes" => matches!(value, RuntimeValue::Bytes(_)), - "markdown" => matches!(value, RuntimeValue::Markdown(_, _)), - "function" => matches!(value, RuntimeValue::Function(_)), - "symbol" => matches!(value, RuntimeValue::Symbol(_)), - "none" => matches!(value, RuntimeValue::None), - // Node-kind pattern (`:h1`, `:code`, `:list`), backed by the selector table. - _ => match value { - RuntimeValue::Markdown(node, _) => Selector::from_selector_str(&format!(".{type_str}")) - .filter(|selector| !selector.is_attribute_selector()) - .is_some_and(|selector| builtin::eval_selector(node, &selector) != RuntimeValue::NONE), - _ => false, - }, - }; - - if matches { Ok(Some(Vec::new())) } else { Ok(None) } - } - Pattern::Array(patterns) => { - // Array pattern: match array elements - if let RuntimeValue::Array(values) = value { - if values.len() != patterns.len() { - return Ok(None); - } - - let mut all_bindings = Vec::new(); - for (pattern, value) in patterns.iter().zip(values.iter()) { - if let Some(bindings) = self.match_pattern(value, pattern)? { - all_bindings.extend(bindings); - } else { - return Ok(None); - } - } - Ok(Some(all_bindings)) - } else { - Ok(None) - } - } - Pattern::ArrayRest(patterns, rest_binding) => { - // Array rest pattern: match prefix and bind rest - if let RuntimeValue::Array(values) = value { - if values.len() < patterns.len() { - return Ok(None); - } - - let mut all_bindings = Vec::new(); - - // Match the prefix patterns - for (pattern, value) in patterns.iter().zip(values.iter()) { - if let Some(bindings) = self.match_pattern(value, pattern)? { - all_bindings.extend(bindings); - } else { - return Ok(None); - } - } - - // Bind the rest of the array - let rest_values = values[patterns.len()..].to_vec(); - all_bindings.push((rest_binding.name, RuntimeValue::Array(Shared::new(rest_values)))); - - Ok(Some(all_bindings)) - } else { - Ok(None) - } - } - Pattern::Dict(field_patterns) => { - // Dict pattern: match dictionary fields - if let RuntimeValue::Dict(dict) = value { - let mut all_bindings = Vec::new(); - - for (key, pattern) in field_patterns { - if let Some(field_value) = dict.get(&key.name) { - if let Some(bindings) = self.match_pattern(field_value, pattern)? { - all_bindings.extend(bindings); - } else { - return Ok(None); - } - } else { - // Required field is missing - return Ok(None); - } - } - - Ok(Some(all_bindings)) - } else { - Ok(None) - } - } - Pattern::Or(patterns) => { - // Or pattern: match if any alternative matches - for pattern in patterns { - if let Some(bindings) = self.match_pattern(value, pattern)? { - return Ok(Some(bindings)); - } - } - Ok(None) - } - } - } - - #[inline(always)] - fn eval_fn( - &mut self, - runtime_value: &RuntimeValue, - node: Shared, - ident: Ident, - args: &ast::Args, - env: &Shared>, - ) -> EvalResult { - #[cfg(not(feature = "sync"))] - let resolved = env.borrow().resolve(ident); - #[cfg(feature = "sync")] - let resolved = env.read().unwrap().resolve(ident); - - match resolved { - Ok(fn_value) => self.call_fn(&fn_value, node, ident, args, runtime_value, env), - // `Env::resolve` itself already falls back to the raw builtin table (see its use of - // `get_builtin_functions_by_str`), so reaching here means `ident` is neither a local - // binding, a builtin loaded into scope, *nor* a raw builtin — host functions can - // therefore only fill in names that don't collide with any builtin, not override one. - Err(_) => { - #[cfg(not(feature = "sync"))] - let host_fn = self.host_functions.borrow().get(&ident); - #[cfg(feature = "sync")] - let host_fn = self.host_functions.read().unwrap().get(&ident); - - match host_fn { - Some(host_fn) => self.eval_host_fn(host_fn, runtime_value, node, &ident, args, env), - None => self.eval_builtin(runtime_value, node, &ident, args, env), - } - } - } - } - - /// Evaluates a call to a host-registered native function: marshals args, guards recursion - /// depth and the wall-clock timeout the same way a user-defined function call would (see - /// [`Self::enter_scope`]), and catches panics at the boundary so a misbehaving host closure - /// can't unwind through the evaluator. - fn eval_host_fn( - &mut self, - host_fn: Shared, - runtime_value: &RuntimeValue, - node: Shared, - ident: &Ident, - args: &ast::Args, - env: &Shared>, - ) -> EvalResult { - self.enter_scope()?; - let evaluated = self.eval_call_args(runtime_value, &node, ident, args, env); - let result = match evaluated { - Ok(evaluated) => std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| host_fn.call(&evaluated))) - .unwrap_or_else(|payload| { - Err(host::HostFunctionError::new(format!( - "panic: {}", - host::panic_message(&*payload) - ))) - }), - Err(e) => { - self.exit_scope(); - return Err(e); - } - }; - self.exit_scope(); - - result.map_err(|e| { - EvalError::from(RuntimeError::HostFunctionError( - (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - ident.to_string().into_boxed_str(), - e.message().to_string().into_boxed_str(), - )) - }) - } - - #[inline(always)] - fn eval_builtin( - &mut self, - runtime_value: &RuntimeValue, - node: Shared, - ident: &Ident, - args: &ast::Args, - env: &Shared>, - ) -> EvalResult { - let args = self.eval_call_args(runtime_value, &node, ident, args, env)?; - builtin::eval_builtin(runtime_value, ident, args, env) - .map_err(|e| EvalError::from(e.to_runtime_error(node.token_id, Shared::clone(&self.token_arena)))) - } - - /// Evaluates call args, expanding `...expr` spread markers for `array`/`dict` calls. - /// Other builtins take the plain evaluation fast path below. - fn eval_call_args( - &mut self, - runtime_value: &RuntimeValue, - node: &Shared, - ident: &Ident, - args: &ast::Args, - env: &Shared>, - ) -> Result { - if *ident != *ARRAY_IDENT && *ident != *DICT_IDENT { - return args.iter().map(|arg| self.eval_expr(runtime_value, arg, env)).collect(); - } - - let mut evaluated: builtin::Args = builtin::Args::with_capacity(args.len()); - - for arg in args.iter() { - match &*arg.expr { - ast::Expr::Call(spread_ident, spread_args) if spread_ident.name == *SPREAD_IDENT => { - let spread_value = self.eval_expr(runtime_value, &spread_args[0], env)?; - self.expand_spread(node, ident, spread_value, &mut evaluated)?; - } - _ => evaluated.push(self.eval_expr(runtime_value, arg, env)?), - } - } - - Ok(evaluated) - } - - /// Splices a spread target into `out`: array elements, or `[Symbol(key), value]` - /// pairs for a dict. `None` contributes nothing; any other type is a runtime error. - fn expand_spread( - &self, - node: &Shared, - ident: &Ident, - value: RuntimeValue, - out: &mut builtin::Args, - ) -> Result<(), EvalError> { - match value { - RuntimeValue::None => Ok(()), - RuntimeValue::Dict(map) if *ident == *DICT_IDENT => { - out.extend( - Shared::unwrap_or_clone(map) - .into_iter() - .map(|(k, v)| RuntimeValue::Array(Shared::new(vec![RuntimeValue::Symbol(k), v]))), - ); - Ok(()) - } - RuntimeValue::Array(items) if *ident != *DICT_IDENT => { - out.extend(Shared::unwrap_or_clone(items)); - Ok(()) - } - other => Err(EvalError::from( - builtin::Error::InvalidTypes(ident.to_string(), vec![other]) - .to_runtime_error(node.token_id, Shared::clone(&self.token_arena)), - )), - } - } - - #[inline(always)] - fn eval_call_dynamic( - &mut self, - runtime_value: &RuntimeValue, - callable: &Shared, - args: &ast::Args, - env: &Shared>, - ) -> EvalResult { - let fn_value = self.eval_expr(runtime_value, callable, env)?; - - self.call_fn( - &fn_value, - Shared::clone(callable), - *DYNAMIC_IDENT, - args, - runtime_value, - env, - ) - } - - #[inline(always)] - fn enter_scope(&mut self) -> Result<(), EvalError> { - if self.call_stack_depth >= self.options.max_call_stack_depth { - return Err(RuntimeError::RecursionError(self.options.max_call_stack_depth).into()); - } - self.check_timeout()?; - self.call_stack_depth += 1; - Ok(()) - } - - #[inline(always)] - fn exit_scope(&mut self) { - if self.call_stack_depth > 0 { - self.call_stack_depth -= 1; - } - } - - /// Checks the configured `timeout`; a no-op when unset. - #[inline(always)] - fn check_timeout(&mut self) -> Result<(), RuntimeError> { - let Some(deadline) = self.deadline else { - return Ok(()); - }; - - self.timeout_step = self.timeout_step.wrapping_add(1); - if self.timeout_step & (TIMEOUT_CHECK_INTERVAL - 1) != 0 { - return Ok(()); - } - - if Instant::now() >= deadline { - Err(RuntimeError::Timeout( - self.options.timeout.expect("deadline implies options.timeout is set"), - )) - } else { - Ok(()) - } - } - - fn call_fn( - &mut self, - fn_value: &RuntimeValue, - node: Shared, - ident: Ident, - args: &ast::Args, - runtime_value: &RuntimeValue, - env: &Shared>, - ) -> EvalResult { - if let RuntimeValue::Function(f) = fn_value { - let params = &f.params; - let program = &f.body; - let fn_env = &f.env; - self.enter_scope()?; - #[cfg(feature = "debugger")] - self.debugger.write().unwrap().push_call_stack(Shared::clone(&node)); - - let new_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(fn_env)))); - - // If the function name matches a built-in, expose the native builtin in the - // body's scope so calls to the same name invoke the builtin rather than - // recursing back into this user-defined function. - if builtin::get_builtin_functions(&ident).is_some() { - define(&new_env, ident, RuntimeValue::NativeFunction(ident)); - } - - let has_variadic = params.iter().any(|p| p.is_variadic); - let required_params = params.iter().filter(|p| p.default.is_none() && !p.is_variadic).count(); - let arg_count = args.len(); - let param_count = params.len(); - - let use_self_param = if has_variadic { - if arg_count >= required_params { - false - } else if arg_count + 1 >= required_params { - true - } else { - return Err(RuntimeError::InvalidNumberOfArguments { - token: (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - name: ident.to_string(), - expected: required_params, - actual: args.len(), - } - .into()); - } - } else if arg_count >= required_params && arg_count <= param_count { - false - } else if arg_count + 1 >= required_params && arg_count < param_count { - true - } else { - // arg_count > param_count: too many arguments - return Err(RuntimeError::InvalidNumberOfArguments { - token: (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - name: ident.to_string(), - expected: params.len(), - actual: args.len(), - } - .into()); - }; - - let mut param_iter = params.iter(); - let mut arg_iter = args.iter(); - - if use_self_param && let Some(param) = param_iter.next() { - define(&new_env, param.ident.name, runtime_value.clone()); - } - - for param in param_iter { - if param.is_variadic { - // Collect all remaining arguments into an array - let mut variadic_args = Vec::new(); - for arg in arg_iter.by_ref() { - variadic_args.push(self.eval_expr(runtime_value, arg, env)?); - } - define( - &new_env, - param.ident.name, - RuntimeValue::Array(Shared::new(variadic_args)), - ); - } else if let Some(arg) = arg_iter.next() { - let val = self.eval_expr(runtime_value, arg, env)?; - define(&new_env, param.ident.name, val); - } else if let Some(default_expr) = ¶m.default { - let val = self.eval_expr(runtime_value, default_expr, &new_env)?; - define(&new_env, param.ident.name, val); - } else { - return Err(RuntimeError::InvalidNumberOfArguments { - token: (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - name: ident.to_string(), - expected: params.len(), - actual: args.len(), - } - .into()); - } - } - - let result = self.eval_program(program, runtime_value.clone(), &new_env); - self.exit_scope(); - #[cfg(feature = "debugger")] - self.debugger.write().unwrap().pop_call_stack(); - - result - } else if let RuntimeValue::NativeFunction(ident) = fn_value { - self.eval_builtin(runtime_value, node, ident, args, env) - } else { - Err(RuntimeError::InvalidDefinition( - (*get_token(Shared::clone(&self.token_arena), node.token_id)).clone(), - ident.to_string(), - ) - .into()) - } - } -} - -#[inline(always)] -fn define(env: &Shared>, ident: Ident, runtime_value: RuntimeValue) { - #[cfg(not(feature = "sync"))] - { - env.borrow_mut().define(ident, runtime_value); - } - #[cfg(feature = "sync")] - { - env.write().unwrap().define(ident, runtime_value); - } -} - -#[inline(always)] -fn define_mutable(env: &Shared>, ident: Ident, runtime_value: RuntimeValue) { - #[cfg(not(feature = "sync"))] - { - env.borrow_mut().define_mutable(ident, runtime_value); - } - #[cfg(feature = "sync")] - { - env.write().unwrap().define_mutable(ident, runtime_value); - } -} - -#[inline(always)] -fn resolve(ident: &str, env: &Shared>) -> Result { - #[cfg(not(feature = "sync"))] - { - env.borrow().resolve(ident.into()) - } - #[cfg(feature = "sync")] - { - env.read().unwrap().resolve(ident.into()) - } -} - -#[cfg(test)] -mod tests { - use std::f64::consts::PI; - use std::vec; - - use crate::ast::node::{Args, IdentWithToken, MatchArm, Param}; - use crate::error::runtime::RuntimeError; - use crate::eval::module::error::ModuleError; - use crate::number::{INFINITE, NAN, Number}; - use crate::range::Range; - use crate::{AstExpr, AstNode, DefaultModuleLoader, ModuleLoader, token_alloc}; - use crate::{Token, TokenKind}; - - use super::*; - use rstest::{fixture, rstest}; - use scopeguard::defer; - use smallvec::{SmallVec, smallvec}; - use std::io::Write; - use std::{fs::File, path::PathBuf}; - - type TempDir = PathBuf; - type TempFile = PathBuf; - - fn create_file(name: &str, content: &str) -> (TempDir, TempFile) { - let temp_dir = std::env::temp_dir(); - let temp_file_path = temp_dir.join(name); - let mut file = File::create(&temp_file_path).expect("Failed to create temp file"); - file.write_all(content.as_bytes()) - .expect("Failed to write to temp file"); - - (temp_dir, temp_file_path) - } - - #[fixture] - fn token_arena() -> Shared>>> { - let token_arena = Shared::new(SharedCell::new(Arena::new(10))); - - token_alloc( - &token_arena, - &Shared::new(Token { - kind: TokenKind::Eof, - range: Range::default(), - module_id: 1.into(), - }), - ); - - token_arena - } - - fn ast_node(expr: AstExpr) -> Shared { - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(expr), - }) - } - - fn ast_call(name: &str, args: Args) -> Shared { - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Call(IdentWithToken::new(name), args)), - }) - } - - #[allow(clippy::crate_in_macro_def)] - #[macro_export] - macro_rules! eval_table_cases { - ($name:ident, $token_arena:ident, $runtime_values:ident, $program:ident, $expected:ident, $body:block) => { - #[rstest] - #[case::starts_with(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("starts_with", smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string())))]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::starts_with(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_call("starts_with", smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string())))]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "true".to_string(), position: None}))]))] - #[case::starts_with(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("starts_with", smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("st".to_string())))]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::starts_with(vec![RuntimeValue::Array(Shared::new(vec!["start".to_string().into(), "end".to_string().into()]))], - vec![ - ast_call("starts_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("start".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::starts_with(vec![RuntimeValue::Array(Shared::new(vec!["start".to_string().into(), "end".to_string().into()]))], - vec![ - ast_call("starts_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("end".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::starts_with(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("starts_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("end".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "starts_with".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::ends_with(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("st".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::ends_with(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("st".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "true".to_string(), position: None}))]))] - #[case::ends_with(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::ends_with(vec![RuntimeValue::Array(Shared::new(vec!["start".to_string().into(), "end".to_string().into()]))], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("end".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::ends_with(vec![RuntimeValue::Array(Shared::new(vec!["start".to_string().into(), "end".to_string().into()]))], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("start".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::ends_with(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "ends_with".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::downcase(vec![RuntimeValue::String(Shared::new("TEST".to_string()))], - vec![ast_call("downcase", SmallVec::new())], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::downcase(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "TEST".to_string(), position: None}))], - vec![ast_call("downcase", SmallVec::new())], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))]))] - #[case::upcase(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ast_call("upcase", SmallVec::new())], - Ok(vec![RuntimeValue::String(Shared::new("TEST".to_string()))]))] - #[case::upcase(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ast_call("upcase", SmallVec::new())], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "TEST".to_string(), position: None}))]))] - #[case::upcase(vec![RuntimeValue::NONE], - vec![ast_call("upcase", SmallVec::new())], - Ok(vec![RuntimeValue::NONE]))] - #[case::upcase(vec![RuntimeValue::Number(123.into())], - vec![ast_call("upcase", SmallVec::new())], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "upcase".to_string(), - args: vec!["number".into()]})))] - #[case::replace(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("replace", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("exam".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("examString".to_string()))]))] - #[case::replace(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "testString".to_string(), position: None}))], - vec![ - ast_call("replace", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("exam".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "examString".to_string(), position: None}))]))] - #[case::replace(vec![RuntimeValue::NONE], - vec![ - ast_call("replace", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("exam".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::replace(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("replace", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("exam".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "replace".to_string(), - args: vec!["number".into(), "string".into(), "string".into()]})))] - #[case::gsub_regex(vec![RuntimeValue::String(Shared::new("test123".to_string()))], - vec![ - ast_call("gsub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("456".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("test456".to_string()))]))] - #[case::gsub_regex(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test123".to_string(), position: None}))], - vec![ - ast_call("gsub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("456".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test456".to_string(), position: None}))]))] - #[case::gsub_regex(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("gsub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "gsub".to_string(), - args: vec!["number".into(), "string".into(), "string".into()]})))] - #[case::len(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ast_call("len", SmallVec::new())], - Ok(vec![RuntimeValue::Number(10.into())]))] - #[case::len(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "testString".to_string(), position: None}))], - vec![ast_call("len", SmallVec::new())], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "10".to_string(), position: None}))]))] - #[case::len(vec![RuntimeValue::TRUE], - vec![ast_call("len", SmallVec::new())], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::len(vec![RuntimeValue::String(Shared::new("テスト".to_string()))], - vec![ast_call("len", SmallVec::new())], - Ok(vec![RuntimeValue::Number(3.into())]))] - #[case::len(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "テスト".to_string(), position: None}))], - vec![ast_call("len", SmallVec::new())], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "3".to_string(), position: None}))]))] - #[case::utf8bytelen(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(4.into())]))] - #[case::utf8bytelen(vec![RuntimeValue::String(Shared::new("テスト".to_string()))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(9.into())]))] - #[case::utf8bytelen(vec![RuntimeValue::String(Shared::new("😊".to_string()))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(4.into())]))] - #[case::utf8bytelen(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "4".to_string(), position: None}))]))] - #[case::utf8bytelen(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "テスト".to_string(), position: None}))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "9".to_string(), position: None}))]))] - #[case::utf8bytelen(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "😊".to_string(), position: None}))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "4".to_string(), position: None}))]))] - #[case::utf8bytelen(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test".to_string()))]))], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::utf8bytelen(vec![RuntimeValue::TRUE], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::index(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("index", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number(0.into())]))] - #[case::index(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "testString".to_string(), position: None}))], - vec![ - ast_call("index", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "0".to_string(), position: None}))]))] - #[case::index(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("index", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "index".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::array_index(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string())), RuntimeValue::String(Shared::new("test3".to_string()))]))], - vec![ - ast_call("index", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test2".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::array_index_not_found(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string())), RuntimeValue::String(Shared::new("test3".to_string()))]))], - vec![ - ast_call("index", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test4".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number((-1).into())]))] - #[case::rindex(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("String".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number(4.into())]))] - #[case::rindex(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "testString".to_string(), position: None}))], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("String".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "4".to_string(), position: None}))]))] - #[case::rindex(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("String".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "rindex".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::array_rindex(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string())), RuntimeValue::String(Shared::new("test1".to_string()))]))], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test1".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number(2.into())]))] - #[case::array_rindex(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string())), RuntimeValue::String(Shared::new("test3".to_string()))]))], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test4".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number((-1).into())]))] - #[case::array_rindex_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number((-1).into())]))] - #[case::eq(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("eq", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("eq".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("eq".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::eq(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("eq", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("eq".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("eq1".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::ne(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("ne", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("eq".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("eq1".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::ne(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("ne", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("eq".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("eq".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::ne(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("ne", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::ne(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("ne", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.2.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gt(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gt(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.4.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gt(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.4.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gt(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gt(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gt(vec![RuntimeValue::FALSE], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gt(vec![RuntimeValue::FALSE], - vec![ - ast_call("gt", smallvec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ]), - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ]), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("gt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("1".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec! [ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gte(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gte(vec![RuntimeValue::Number(1.3.into())], - vec![ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec! [ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::gte(vec![RuntimeValue::TRUE], - vec![ - ast_call("gte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::gte(vec![RuntimeValue::TRUE], - vec![ - ast_call("gte", smallvec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ]), - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ]), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.2.into()))), - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lt(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lt(vec![RuntimeValue::TRUE], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lt(vec![RuntimeValue::TRUE], - vec![ - ast_call("lt", smallvec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ]), - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ]), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lt(vec![RuntimeValue::TRUE], - vec![ - ast_call("lt", smallvec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ]), - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ]), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lt(vec![RuntimeValue::TRUE], - vec![ - ast_call("lt", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.4.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(2.to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(1.to_string()))), - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("2".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::lte(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("lte", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]) - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::add(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("add", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(2.into())]))] - #[case::add(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("add", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("st".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::add(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("add", smallvec![ - ast_call("array", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))) - ]), - ast_call("array", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))) - ]) - ]), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec!["te".to_string().into(), "te".to_string().into()]))]))] - #[case::add(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("add", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "add".to_string(), - args: vec!["bool".into(), "number".into()]})))] - #[case::add(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("add", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(2.6.into())]))] - #[case::add(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("add", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(2.6.into())]))] - #[case::add(vec![RuntimeValue::TRUE], - vec![ - ast_call("add", smallvec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{value: "21".to_string(), lang: None, fence: true, meta: None, position: None}))]))] - #[case::add(vec![RuntimeValue::TRUE], - vec![ - ast_call("add", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{value: "21".to_string(), lang: None, fence: true, meta: None, position: None}))]))] - #[case::add(vec![RuntimeValue::TRUE], - vec![ - ast_call("add", smallvec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{value: "21".to_string(), lang: None, fence: true, meta: None, position: None}))]))] - #[case::sub(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("sub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(0.into())]))] - #[case::sub(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("sub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Err(InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, "invalid float literal".to_string()))))] - #[case::sub(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("sub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(0.10000000000000009.into())]))] - #[case::div(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("div", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::div(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("div", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, "invalid float literal".to_string()))))] - #[case::div(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("div", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::ZeroDivision(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}))))] - #[case::div(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("div", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.1.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(1.1818181818181817.into())]))] - #[case::mul(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("mul", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(2.into())]))] - #[case::mul(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("mul", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.3.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(2.6.into())]))] - #[case::mul(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("mul", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::String(Shared::new("tete".to_string()))]))] - #[case::mod_(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("mod", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::mod_(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("mod", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.0.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(1.1.into())]))] - #[case::mod_(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("mod", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Err(InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, "invalid float literal".to_string()))))] - #[case::pow(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("pow", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))), - ]), - ], - Ok(vec![RuntimeValue::Number(8.into())]))] - #[case::pow(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("pow", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "pow".to_string(), - args: vec!["string".into(), "number".into()]})))] - #[case::and(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::and(vec![RuntimeValue::TRUE], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::and(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::and(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::or(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::or(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::or(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::or(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::not(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("not", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::not(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("not", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]), - ], - Ok(vec![RuntimeValue::TRUE]))] - #[case::to_string(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("to_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::to_string(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_call("to_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))]))] - #[case::to_string_symbol(vec![RuntimeValue::Symbol("test".to_string().into())], - vec![ - ast_call("to_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::split1(vec![RuntimeValue::String(Shared::new("test1,test2".to_string()))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(",".to_string())))] - ) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string()))]))]))] - #[case::split2(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test1,test2".to_string(), position: None}))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(",".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test1\ntest2".to_string(), position: None}))]))] - #[case::split(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("split", smallvec![ast_node(ast::Expr::Literal(ast::Literal::String(",".to_string())))]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "split".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::split_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("value1".to_string())), - RuntimeValue::String(Shared::new("separator".to_string())), - RuntimeValue::String(Shared::new("value2".to_string())), - ]))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("separator".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("value1".to_string()))])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("value2".to_string()))])) - ]))]))] - #[case::split_array_multiple_separators(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("value1".to_string())), - RuntimeValue::String(Shared::new("separator".to_string())), - RuntimeValue::String(Shared::new("value2".to_string())), - RuntimeValue::String(Shared::new("separator".to_string())), - RuntimeValue::String(Shared::new("value3".to_string())), - ]))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("separator".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("value1".to_string()))])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("value2".to_string()))])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("value3".to_string()))])) - ]))]))] - #[case::split_array_no_separator(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("value1".to_string())), - RuntimeValue::String(Shared::new("value2".to_string())), - RuntimeValue::String(Shared::new("value3".to_string())), - ]))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("separator".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("value1".to_string())), - RuntimeValue::String(Shared::new("value2".to_string())), - RuntimeValue::String(Shared::new("value3".to_string())) - ])) - ]))]))] - #[case::split_array_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("separator".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))]))] - #[case::split_array_mixed_types(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::String(Shared::new("separator".to_string())), - RuntimeValue::Boolean(true), - ]))], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("separator".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into())])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::Boolean(true)])) - ]))]))] - #[case::join1(vec![RuntimeValue::String(Shared::new("test1,test2".to_string()))], - vec![ - ast_call("join", smallvec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(",".to_string()))) - ]), - ast_node(ast::Expr::Literal(ast::Literal::String("#".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("test1#test2".to_string()))]))] - #[case::join_error(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("join", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("#".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "join".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::reverse_string(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("reverse", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("tset".to_string()))]))] - #[case::reverse_string_empty(vec![RuntimeValue::String(Shared::new("".to_string()))], - vec![ - ast_call("reverse", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("".to_string()))]))] - #[case::reverse_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))], - vec![ - ast_call("reverse", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - ]))]))] - #[case::reverse_array_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("reverse", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::reverse_number(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("reverse", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "reverse".to_string(), - args: vec!["number".into()]})))] - #[case::base64(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("base64", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("dGVzdA==".to_string()))]))] - #[case::base64(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value:"test".to_string(), position: None}))], - vec![ - ast_call("base64", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "dGVzdA==".to_string(), position: None}))]))] - #[case::base64(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("base64", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "base64".to_string(), - args: vec!["number".into()]})))] - #[case::base64d(vec![RuntimeValue::String(Shared::new("dGVzdA==".to_string()))], - vec![ - ast_call("base64d", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("dGVzdA==".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::base64d(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value:"dGVzdA==".to_string(), position: None}))], - vec![ - ast_call("base64d", smallvec![ - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))]))] - #[case::base64d(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("base64d", smallvec![ - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "base64d".to_string(), - args: vec!["number".into()]})))] - #[case::base64url_encode( - vec![RuntimeValue::String(Shared::new("hello".into()))], - vec![ - ast_call("base64url", smallvec![ - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("aGVsbG8".into()))]) - )] - #[case::base64url_decode( - vec![RuntimeValue::String(Shared::new("aGVsbG8".into()))], - vec![ - ast_call("base64urld", smallvec![ - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("hello".into()))]) - )] - #[case::base64url(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("base64url", smallvec![ - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "base64url".to_string(), - args: vec!["number".into()]})))] - #[case::base64urld(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("base64urld", smallvec![ - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "base64urld".to_string(), - args: vec!["number".into()]})))] - #[case::def(vec![RuntimeValue::String(Shared::new("test1,test2".to_string()))], - vec![ - ast_node(ast::Expr::Def( - IdentWithToken::new("split2"), - smallvec![ - Param::new(IdentWithToken::new("str")), - ], - vec![ast_call("split", - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("str"))), - ast_node(ast::Expr::Literal(ast::Literal::String(",".to_string()))), - ]) - ] - )), - ast_call("split2", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test1,test2".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string()))]))]))] - #[case::def2(vec![RuntimeValue::String(Shared::new("Hello".to_string()))], - vec![ - ast_node(ast::Expr::Def( - IdentWithToken::new("concat_self"), - smallvec![ - Param::new(IdentWithToken::new("str1")), - Param::new(IdentWithToken::new("str2")), - ], - vec![ast_call("add", - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("str1"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("str2"))), - ]) - ] - )), - ast_call("concat_self", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("Hello".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("World".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("HelloWorld".to_string()))]))] - #[case::def3(vec![RuntimeValue::String(Shared::new("Test".to_string()))], - vec![ - ast_node(ast::Expr::Def( - IdentWithToken::new("prepend_self"), - smallvec![ - Param::new(IdentWithToken::new("str1")), - Param::new(IdentWithToken::new("str2")), - ], - vec![ast_call("add", - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("str1"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("str2"))), - ]) - ] - )), - ast_call("prepend_self", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("Testtest".to_string()))]))] - #[case::type_string(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("type", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("string".to_string()))]))] - #[case::type_int(vec![RuntimeValue::Number(42.into())], - vec![ - ast_call("type", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("number".to_string()))]))] - #[case::type_bool(vec![RuntimeValue::TRUE], - vec![ - ast_call("type", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("bool".to_string()))]))] - #[case::type_array(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test".to_string()))]))], - vec![ - ast_call("type", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("array".to_string()))]))] - #[case::min(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("min", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::min(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("min", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("1".into()))]))] - #[case::min(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("min", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(1.into())]))] - #[case::min(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("min", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "min".to_string(), - args: vec!["string".into(), "number".into()]})))] - #[case::max(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("max", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(2.into())]))] - #[case::max(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("max", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("2".into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("2".into()))]))] - #[case::max(vec![RuntimeValue::Number(3.into())], - vec![ - ast_call("max", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(3.into())]))] - #[case::max(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("max", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "max".to_string(), - args: vec!["string".into(), "number".into()]})))] - #[case::trim(vec![RuntimeValue::String(Shared::new(" test ".to_string()))], - vec![ - ast_call("trim", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::trim(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: " test ".to_string(), position: None}))], - vec![ - ast_call("trim", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))]))] - #[case::trim(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("trim", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "trim".to_string(), - args: vec!["number".into()]})))] - #[case::slice(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "testString".to_string(), position: None}))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))]))] - #[case::slice(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::slice(vec![RuntimeValue::String(Shared::new("testString".to_string()))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(10.into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("String".to_string()))]))] - #[case::slice(vec![RuntimeValue::NONE], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::slice_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - RuntimeValue::String(Shared::new("item4".to_string())), - RuntimeValue::String(Shared::new("item5".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - RuntimeValue::String(Shared::new("item4".to_string())), - ]))]))] - #[case::slice_array_from_start(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - ]))]))] - #[case::slice_array_to_end(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::slice_array_out_of_bounds(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(5.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::slice_array_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::slice_array_mixed_types(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::Number(42.into()), - RuntimeValue::Boolean(true), - RuntimeValue::String(Shared::new("item4".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(42.into()), - RuntimeValue::Boolean(true), - ]))]))] - #[case::slice(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "slice".to_string(), - args: vec!["number".into(), "number".into(), "number".into()]})))] - #[case::match_regex1(vec![RuntimeValue::String(Shared::new("test123".to_string()))], - vec![ - ast_call("regex_match", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("123".to_string()))]))]))] - #[case::match_regex2(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test123".to_string(), position: None}))], - vec![ - ast_call("regex_match", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "123".to_string(), position: None}))]))] - #[case::match_regex3(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("regex_match", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "regex_match".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::explode(vec![RuntimeValue::String(Shared::new("ABC".to_string()))], - vec![ - ast_call("explode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(65.into()), - RuntimeValue::Number(66.into()), - RuntimeValue::Number(67.into()), - ]))]))] - #[case::explode(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("explode", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "explode".to_string(), - args: vec!["number".into()]})))] - #[case::implode(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(65.into()), - RuntimeValue::Number(66.into()), - RuntimeValue::Number(67.into()), - ]))], - vec![ - ast_call("implode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("ABC".to_string()))]))] - #[case::implode(vec!["test".to_string().into()], - vec![ - ast_call("implode", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "implode".to_string(), - args: vec!["string".into()]})))] - #[case::explode_markdown(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "ABC".to_string(), position: None}))], - vec![ - ast_call("explode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "65\n66\n67".to_string(), position: None}))]))] - #[case::to_number(vec![RuntimeValue::String(Shared::new("42".to_string()))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::to_number(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "42".to_string(), position: None}))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "42".to_string(), position: None}))]))] - #[case::to_number(vec![RuntimeValue::String(Shared::new("42.5".to_string()))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.5.into())]))] - #[case::to_number(vec![RuntimeValue::String(Shared::new("not a number".to_string()))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, "invalid float literal".to_string()))))] - #[case::to_number_array(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("42".to_string())), RuntimeValue::String(Shared::new("43".to_string())), RuntimeValue::String(Shared::new("44".to_string()))]))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(42.into()), RuntimeValue::Number(43.into()), RuntimeValue::Number(44.into())]))]))] - #[case::to_number_array(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "42".to_string(), position: None}))]))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(42.into())]))]))] - #[case::to_number_array_with_invalid(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("42".to_string())), RuntimeValue::String(Shared::new("not a number".to_string())), RuntimeValue::String(Shared::new("44".to_string()))]))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, "invalid float literal".to_string()))))] - #[case::to_number_array_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::to_number_array_mixed_types(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("42".to_string())), RuntimeValue::Number(43.into()), RuntimeValue::String(Shared::new("44".to_string()))]))], - vec![ - ast_call("to_number", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(42.into()), RuntimeValue::Number(43.into()), RuntimeValue::Number(44.into())]))]))] - #[case::trunc(vec![RuntimeValue::Number(42.5.into())], - vec![ - ast_call("trunc", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::trunc(vec![RuntimeValue::Number((-42.5).into())], - vec![ - ast_call("trunc", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number((-42).into())]))] - #[case::trunc(vec!["42.5".to_string().into()], - vec![ - ast_call("trunc", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "trunc".to_string(), - args: vec!["string".into()]})))] - #[case::abs_positive(vec![RuntimeValue::Number(42.into())], - vec![ - ast_call("abs", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::abs_negative(vec![RuntimeValue::Number((-42).into())], - vec![ - ast_call("abs", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::abs_zero(vec![RuntimeValue::Number(0.into())], - vec![ - ast_call("abs", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(0.into())]))] - #[case::abs_decimal(vec![RuntimeValue::Number((-42.5).into())], - vec![ - ast_call("abs", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.5.into())]))] - #[case::abs_invalid_type(vec![RuntimeValue::String(Shared::new("42".to_string()))], - vec![ - ast_call("abs", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "abs".to_string(), - args: vec!["string".into()]})))] - #[case::ceil(vec![RuntimeValue::Number(42.1.into())], - vec![ - ast_call("ceil", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(43.into())]))] - #[case::ceil(vec![RuntimeValue::Number((-42.1).into())], - vec![ - ast_call("ceil", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number((-42).into())]))] - #[case::ceil(vec!["42".to_string().into()], - vec![ - ast_call("ceil", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "ceil".to_string(), - args: vec!["string".into()]})))] - #[case::round(vec![RuntimeValue::Number(42.5.into())], - vec![ - ast_call("round", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(43.into())]))] - #[case::round(vec![RuntimeValue::Number(42.4.into())], - vec![ - ast_call("round", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::round(vec!["42.4".to_string().into()], - vec![ - ast_call("round", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "round".to_string(), - args: vec!["string".into()]})))] - #[case::floor(vec![RuntimeValue::Number(42.9.into())], - vec![ - ast_call("floor", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::floor(vec![RuntimeValue::Number((-42.9).into())], - vec![ - ast_call("floor", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number((-43).into())]))] - #[case::floor_error(vec!["42.9".to_string().into()], - vec![ - ast_call("floor", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "floor".to_string(), - args: vec!["string".into()]})))] - #[case::del(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string()))]))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ]), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test2".to_string()))]))]))] - #[case::del(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string())), RuntimeValue::String(Shared::new("test2".to_string()))]))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("test1".to_string()))]))]))] - #[case::del(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]), - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::del(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]), - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "del".to_string(), - args: vec!["number".into(), "number".into()]})))] - #[case::to_code(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("elm".into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{lang: Some("elm".to_string()), value: "test1".to_string(), fence: true, meta: None, position: None}))]))] - #[case::to_code(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("elm".into()))), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{lang: None, value: "elm".to_string(), fence: true, meta: None, position: None}))]))] - #[case::md_h1(vec![RuntimeValue::String(Shared::new("Heading 1".to_string()))], - vec![ - ast_call("to_h", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 1, values: vec!["Heading 1".to_string().into()], position: None}))]))] - #[case::md_h2(vec![RuntimeValue::String(Shared::new("Heading 2".to_string()))], - vec![ - ast_call("to_h", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 2, values: vec!["Heading 2".to_string().into()], position: None}))]))] - #[case::md_h3(vec![RuntimeValue::String(Shared::new("Heading 3".to_string()))], - vec![ - ast_call("to_h", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 3, values: vec!["Heading 3".to_string().into()], position: None}))]))] - #[case::md_h3(vec![RuntimeValue::String(Shared::new("Heading 3".to_string()))], - vec![ - ast_call("to_h", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("3".into()))), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::md_h(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Heading".to_string(), position: None}))], - vec![ - ast_call("to_h", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 2, values: vec!["Heading".to_string().into()], position: None}))]))] - #[case::to_math(vec![RuntimeValue::String(Shared::new("E=mc^2".to_string()))], - vec![ - ast_call("to_math", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Math(mq_markdown::Math{value: "E=mc^2".to_string(), position: None}))]))] - #[case::to_math_inline(vec![RuntimeValue::String(Shared::new("E=mc^2".to_string()))], - vec![ - ast_call("to_math_inline", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::MathInline(mq_markdown::MathInline{value: "E=mc^2".into(), position: None}))]))] - #[case::to_md_text(vec![RuntimeValue::String(Shared::new("This is a text".to_string()))], - vec![ - ast_call("to_md_text", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "This is a text".to_string(), position: None}))]))] - #[case::to_strong(vec![RuntimeValue::String(Shared::new("Bold text".to_string()))], - vec![ - ast_call("to_strong", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Strong(mq_markdown::Strong{values: vec!["Bold text".to_string().into()], position: None}))]))] - #[case::to_strong(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Bold text".to_string(), position: None}))], - vec![ - ast_call("to_strong", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Strong(mq_markdown::Strong{values: vec![mq_markdown::Node::Text(mq_markdown::Text{value: "Bold text".to_string(), position: None})], position: None}))]))] - #[case::to_em(vec![RuntimeValue::String(Shared::new("Italic text".to_string()))], - vec![ - ast_call("to_em", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Emphasis(mq_markdown::Emphasis{values: vec!["Italic text".to_string().into()], position: None}))]))] - #[case::to_em(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Italic text".to_string(), position: None}))], - vec![ - ast_call("to_em", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Emphasis(mq_markdown::Emphasis{values: vec![mq_markdown::Node::Text(mq_markdown::Text{value: "Italic text".to_string(), position: None})], position: None}))]))] - #[case::to_blockquote(vec![RuntimeValue::String(Shared::new("Quoted text".to_string()))], - vec![ - ast_call("to_blockquote", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Blockquote(mq_markdown::Blockquote{values: vec!["Quoted text".to_string().into()], position: None}))]))] - #[case::to_delete(vec![RuntimeValue::String(Shared::new("Deleted text".to_string()))], - vec![ - ast_call("to_delete", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Delete(mq_markdown::Delete{values: vec!["Deleted text".to_string().into()], position: None}))]))] - #[case::to_callout(vec![RuntimeValue::String(Shared::new("Heads up".to_string()))], - vec![ - ast_call("to_callout", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("note".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Title".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Callout(mq_markdown::Callout{kind: "NOTE".to_string(), title: Some("Title".to_string()), values: vec!["Heads up".to_string().into()], position: None}))]))] - #[case::to_image(vec![RuntimeValue::String(Shared::new("Image Alt".to_string()))], - vec![ - ast_call("to_image", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("https://example.com/image.png".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Image Alt".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Image Title".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Image(mq_markdown::Image{ - url: "https://example.com/image.png".to_string(), - alt: "Image Alt".to_string(), - title: Some("Image Title".to_string()), - position: None - }))]))] - #[case::to_link(vec![RuntimeValue::String(Shared::new("Link Text".to_string()))], - vec![ - ast_call("to_link", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("https://example.com".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Link Value".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Link Title".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{ - url: mq_markdown::Url::new("https://example.com".to_string()), - title: Some(mq_markdown::Title::new("Link Title".to_string())), - values: vec!["Link Value".to_string().into()], - position: None - }))]))] - #[case::to_link(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("to_link", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("Link Title".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Link Value".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_hr(vec![RuntimeValue::String(Shared::new("".to_owned()))], - vec![ - ast_call("to_hr", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::HorizontalRule(mq_markdown::HorizontalRule{position: None}))]))] - #[case::to_md_list(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "list".to_string(), position: None}))], - vec![ - ast_call("to_md_list", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::List( - mq_markdown::List{start: None, spread: false, values: vec!["list".to_string().into()], ordered: false, index: 0, level: 1_u8, checked: None, position: None}))]))] - #[case::to_md_list(vec![RuntimeValue::String(Shared::new("list".to_string()))], - vec![ - ast_call("to_md_list", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::List( - mq_markdown::List{start: None, spread: false, values: vec!["list".to_string().into()], ordered: false, index: 0, level: 1_u8, checked: None, position: None}))]))] - #[case::to_md_fragment(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "first".to_string(), position: None})), - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "second".to_string(), position: None})), - ]))], - vec![ - ast_call("to_md_fragment", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Fragment(mq_markdown::Fragment{values: vec![ - mq_markdown::Node::Text(mq_markdown::Text{value: "first".to_string(), position: None}), - mq_markdown::Node::Text(mq_markdown::Text{value: "second".to_string(), position: None}), - ]}))]))] - #[case::to_md_fragment_flattens_nested_arrays(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "first".to_string(), position: None})), - RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "second".to_string(), position: None})), - RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "third".to_string(), position: None})), - ])), - ])), - ]))], - vec![ - ast_call("to_md_fragment", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Fragment(mq_markdown::Fragment{values: vec![ - mq_markdown::Node::Text(mq_markdown::Text{value: "first".to_string(), position: None}), - mq_markdown::Node::Text(mq_markdown::Text{value: "second".to_string(), position: None}), - mq_markdown::Node::Text(mq_markdown::Text{value: "third".to_string(), position: None}), - ]}))]))] - #[case::to_md_table_align(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("left".to_string())), - RuntimeValue::String(Shared::new("right".to_string())), - RuntimeValue::String(Shared::new("center".to_string())), - RuntimeValue::String(Shared::new("none".to_string())), - ]))], - vec![ - ast_call("to_md_table_align", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::TableAlign(mq_markdown::TableAlign{ - align: vec![ - mq_markdown::TableAlignKind::Left, - mq_markdown::TableAlignKind::Right, - mq_markdown::TableAlignKind::Center, - mq_markdown::TableAlignKind::None, - ], - position: None, - }))]))] - #[case::set_check(vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List{start: None, spread: false, values: vec!["Checked Item".to_string().into()], ordered: false, level: 0, index: 0, checked: None, position: None}))], - vec![ - ast_call("set_check", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List{start: None, spread: false, values: vec!["Checked Item".to_string().into()], ordered: false, level: 0, index: 0, checked: Some(true), position: None}))]))] - #[case::set_check(vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List{start: None, spread: false, values: vec!["Unchecked Item".to_string().into()], ordered: false, level: 0, index: 0, checked: None, position: None}))], - vec![ - ast_call("set_check", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List{start: None, spread: false, values: vec!["Unchecked Item".to_string().into()], ordered: false, level: 0, index: 0, checked: Some(false), position: None}))]))] - #[case::compact(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("test2".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("test3".to_string())), - ]))], - vec![ - ast_call("compact", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::String(Shared::new("test2".to_string())), - RuntimeValue::String(Shared::new("test3".to_string())), - ]))]))] - #[case::compact(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("test2".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("test3".to_string())), - ]))], - vec![ - ast_call("compact", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::String(Shared::new("test2".to_string())), - RuntimeValue::String(Shared::new("test3".to_string())), - ]))]))] - #[case::compact_empty(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::NONE, - RuntimeValue::NONE, - ]))], - vec![ - ast_call("compact", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::compact_no_none(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::String(Shared::new("test2".to_string())), - ]))], - vec![ - ast_call("compact", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::String(Shared::new("test2".to_string())), - ]))]))] - #[case::text_selector(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_node(ast::Expr::Selector(Selector::Text)), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))]))] - #[case::text_selector_heading(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 1, values: vec!["Heading 1".to_string().into()], position: None}))], - vec![ - ast_node(ast::Expr::Selector(Selector::Text)), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Fragment(mq_markdown::Fragment { values: vec!["Heading 1".to_string().into()] }))]))] - // SelectorCall: .h(1) matches h1 - #[case::selector_call_heading_exact_match( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 1, values: vec!["H1".to_string().into()], position: None}))], - vec![ast_node(ast::Expr::SelectorCall(Selector::Heading(None), smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(crate::number::Number::new(1.0)))), - ]))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 1, values: vec!["H1".to_string().into()], position: None}))]))] - // SelectorCall: .h(2) does not match a non-markdown value - #[case::selector_call_heading_no_match( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::SelectorCall(Selector::Heading(None), smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(crate::number::Number::new(2.0)))), - ]))], - Ok(vec![RuntimeValue::NONE]))] - // SelectorCall: .h(1, 2) matches h2 - #[case::selector_call_heading_multi_arg( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 2, values: vec!["H2".to_string().into()], position: None}))], - vec![ast_node(ast::Expr::SelectorCall(Selector::Heading(None), smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(crate::number::Number::new(1.0)))), - ast_node(ast::Expr::Literal(ast::Literal::Number(crate::number::Number::new(2.0)))), - ]))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 2, values: vec!["H2".to_string().into()], position: None}))]))] - // SelectorCall: .code("rust") matches rust code block - #[case::selector_call_code_lang_match( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{lang: Some("rust".to_string()), meta: None, value: "fn main() {}".to_string(), fence: true, position: None}))], - vec![ast_node(ast::Expr::SelectorCall(Selector::Code, smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("rust".to_string()))), - ]))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{lang: Some("rust".to_string()), meta: None, value: "fn main() {}".to_string(), fence: true, position: None}))]))] - // SelectorCall: .code("python") does not match a non-markdown value - #[case::selector_call_code_lang_no_match( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::SelectorCall(Selector::Code, smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("python".to_string()))), - ]))], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_md_table_row(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("Cell 1".to_string())), - RuntimeValue::String(Shared::new("Cell 2".to_string())), - RuntimeValue::String(Shared::new("Cell 3".to_string())), - ]))], - vec![ - ast_call("to_md_table_row", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::TableRow(mq_markdown::TableRow{ - values: vec![ - mq_markdown::Node::TableCell(mq_markdown::TableCell{ - row: 0, - column: 0, - values: vec!["Cell 1".to_string().into()], - position: None - }), - mq_markdown::Node::TableCell(mq_markdown::TableCell{ - row: 0, - column: 1, - values: vec!["Cell 2".to_string().into()], - position: None - }), - mq_markdown::Node::TableCell(mq_markdown::TableCell{ - row: 0, - column: 2, - values: vec!["Cell 3".to_string().into()], - position: None - }), - ], - position: None - }))]))] - #[case::to_md_table_row(vec![RuntimeValue::String(Shared::new("Cell 4".to_string()))], - vec![ - ast_call("to_md_table_row", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("Cell 1".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Cell 2".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::TableRow(mq_markdown::TableRow{ - values: vec![ - mq_markdown::Node::TableCell(mq_markdown::TableCell{ - row: 0, - column: 0, - values: vec!["Cell 1".to_string().into()], - position: None - }), - mq_markdown::Node::TableCell(mq_markdown::TableCell{ - row: 0, - column: 1, - values: vec!["Cell 2".to_string().into()], - position: None - }), - ], - position: None - }))]))] - #[case::get_title(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{url: mq_markdown::Url::new("https://example.com".to_string()), title: Some(mq_markdown::Title::new("title".to_string())), values: vec!["Link".to_string().into()], position: None}))], - vec![ - ast_call("get_title", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "title".to_string(), position: None}))]))] - #[case::get_title(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{url: mq_markdown::Url::new("https://example.com".to_string()), title: None, values: vec!["Link".to_string().into()], position: None}))], - vec![ - ast_call("get_title", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Empty)]))] - #[case::get_title(vec![RuntimeValue::new_markdown(mq_markdown::Node::Image(mq_markdown::Image{url: "https://example.com/image.png".to_string(), alt: "Image Alt".to_string(), title: Some("Image Title".to_string()), position: None}))], - vec![ - ast_call("get_title", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Image Title".to_string(), position: None}))]))] - #[case::get_title(vec![RuntimeValue::new_markdown(mq_markdown::Node::Image(mq_markdown::Image{url: "https://example.com/image.png".to_string(), alt: "Image Alt".to_string(), title: None, position: None}))], - vec![ - ast_call("get_title", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Empty)]))] - #[case::get_string(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("get", smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(0.into())))]) - ], - Ok(vec![RuntimeValue::String(Shared::new("t".to_string()))]))] - #[case::get_string(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("get", smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(5.into())))]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::get_string(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("get", smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(Number::new(-1.0))))]) - ], - Ok(vec![RuntimeValue::String(Shared::new("1".to_string()))]))] - #[case::get_array(vec![RuntimeValue::Array(Shared::new(vec!["1".to_string().into()]))], - vec![ - ast_call("get", smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(2.into())))]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::get_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test1".to_string())), - RuntimeValue::String(Shared::new("test2".to_string())), - ]))], - vec![ - ast_call("get", smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(Number::new(-1.0))))]) - ], - Ok(vec![RuntimeValue::String(Shared::new("test2".to_string()))]))] - #[case::get(vec![RuntimeValue::TRUE], - vec![ - ast_call("get", smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(0.into())))]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "get".to_string(), - args: vec!["bool".into(), "number".into()]})))] - #[case::to_date(vec![RuntimeValue::Number(1609459200_i64.into())], - vec![ - ast_call("to_date", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("%Y-%m-%d".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("2021-01-01".to_string()))]))] - #[case::to_date(vec![RuntimeValue::Number(1609459200_i64.into())], - vec![ - ast_call("to_date", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("%Y/%m/%d %H:%M:%S".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("2021/01/01 00:00:00".to_string()))]))] - #[case::to_date(vec![RuntimeValue::Number(1609488000_i64.into())], - vec![ - ast_call("to_date", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("%d %b %Y %H:%M".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("01 Jan 2021 08:00".to_string()))]))] - #[case::to_date(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("to_date", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("%Y-%m-%d".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "to_date".to_string(), - args: vec!["string".into(), "string".into()]})))] - #[case::to_string_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("test".to_string())), - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Boolean(false), - ]))], - vec![ - ast_call("to_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new(r#"["test", 1, 2, false]"#.to_string()))]))] - #[case::to_string_empty_array(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("to_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("[]".to_string()))]))] - #[case::to_text(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("to_text", SmallVec::new()) - ], - Ok(vec!["test".to_string().into()]))] - #[case::to_text(vec![RuntimeValue::Number(42.into())], - vec![ - ast_call("to_text", SmallVec::new()) - ], - Ok(vec!["42".to_string().into()]))] - #[case::to_text(vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("to_text", SmallVec::new()) - ], - Ok(vec!["true".to_string().into()]))] - #[case::to_text(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 1, values: vec!["Heading".to_string().into()], position: None}))], - vec![ - ast_call("to_text", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Heading".to_string(), position: None}))]))] - #[case::to_text(vec![RuntimeValue::String(Shared::new("Original".to_string()))], - vec![ - ast_call("to_text", - smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("Override".to_string())))]) - ], - Ok(vec!["Override".to_string().into()]))] - #[case::to_text(vec![RuntimeValue::Array(Shared::new(vec!["val1".to_string().into(), "val2".to_string().into()]))], - vec![ - ast_call("to_text", SmallVec::new()) - ], - Ok(vec!["val1,val2".to_string().into()]))] - #[case::url_encode(vec![RuntimeValue::String(Shared::new("test string with spaces".to_string()))], - vec![ - ast_call("url_encode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test%20string%20with%20spaces".to_string()))]))] - #[case::url_encode(vec![RuntimeValue::String(Shared::new("test!@#$%^&*()".to_string()))], - vec![ - ast_call("url_encode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test%21%40%23%24%25%5E%26%2A%28%29".to_string()))]))] - #[case::url_encode(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test string".to_string(), position: None}))], - vec![ - ast_call("url_encode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test%20string".to_string(), position: None}))]))] - #[case::url_encode(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("url_encode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("1".to_string()))]))] - #[case::url_decode(vec![RuntimeValue::String(Shared::new("test%20string%20with%20spaces".to_string()))], - vec![ - ast_call("url_decode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test string with spaces".to_string()))]))] - #[case::url_decode(vec![RuntimeValue::String(Shared::new("test%21%40%23%24%25%5E%26%2A%28%29".to_string()))], - vec![ - ast_call("url_decode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test!@#$%^&*()".to_string()))]))] - #[case::url_decode(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test%20string".to_string(), position: None}))], - vec![ - ast_call("url_decode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test string".to_string(), position: None}))]))] - #[case::url_decode(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("url_decode", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("1".to_string()))]))] - #[case::update(vec!["".to_string().into()], - vec![ - ast_call("update", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("updated".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("updated".to_string()))]))] - #[case::update(vec!["".to_string().into()], - vec![ - ast_call("update", smallvec![ - ast_call("to_strong", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("text1".to_string()))), - ]), - ast_call("to_strong", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("text2".to_string()))), - ]) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Strong(mq_markdown::Strong{values: vec![mq_markdown::Node::Text(mq_markdown::Text{value: "text2".to_string(), position: None})], position: None}))]))] - #[case::update(vec!["".to_string().into()], - vec![ - ast_call("update", smallvec![ - ast_call("to_strong", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("text1".to_string()))), - ]), - ast_node(ast::Expr::Literal(ast::Literal::String("text2".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Strong(mq_markdown::Strong{values: vec![mq_markdown::Node::Text(mq_markdown::Text{value: "text2".to_string(), position: None})], position: None}))]))] - #[case::sort_string_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("sort", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))]))] - #[case::sort_number_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(3.into()), - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - ]))], - vec![ - ast_call("sort", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(3.into()), - ]))]))] - #[case::sort_empty_array(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("sort", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::sort_error(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("sort", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "sort".to_string(), - args: vec!["number".into()]})))] - #[case::uniq_string_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("uniq", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))]))] - #[case::uniq_number_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(1.into()), - RuntimeValue::Number(3.into()), - RuntimeValue::Number(2.into()), - ]))], - vec![ - ast_call("uniq", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(3.into()), - ]))]))] - #[case::uniq_error(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("uniq", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "uniq".to_string(), - args: vec!["number".into()]})))] - #[case::to_html(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "

test

".to_string(), position: None}))]))] - #[case::to_html(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 1, values: vec!["Heading 1".to_string().into()], position: None}))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "

Heading 1

".to_string(), position: None}))]))] - #[case::to_html(vec![RuntimeValue::new_markdown(mq_markdown::Node::Strong(mq_markdown::Strong{values: vec!["Bold".to_string().into()], position: None}))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "

Bold

".to_string(), position: None}))]))] - #[case::to_html(vec![RuntimeValue::new_markdown(mq_markdown::Node::Emphasis(mq_markdown::Emphasis{values: vec!["Italic".to_string().into()], position: None}))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "

Italic

".to_string(), position: None}))]))] - #[case::to_html(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{url: mq_markdown::Url::new("https://example.com".to_string()), title: Some(mq_markdown::Title::new("Link Title".to_string())), values: vec!["Link Title".to_string().into()], position: None}))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "

Link Title

".to_string(), position: None}))]))] - #[case::to_html(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{lang: Some("rust".to_string()), value: "println!(\"Hello\");".to_string(), fence: true, meta: None, position: None}))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "
println!("Hello");\n
".to_string(), position: None}))]))] - #[case::to_html(vec![RuntimeValue::String(Shared::new("Plain text".to_string()))], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Ok(vec!["

Plain text

".to_string().into()]))] - #[case::to_html(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("to_html", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "to_html".to_string(), - args: vec!["number".into()]})))] - #[case::repeat_string(vec![RuntimeValue::String(Shared::new("abc".to_string()))], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("abcabcabc".to_string()))]))] - #[case::repeat_markdown(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "abc".to_string(), position: None}))], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "abcabcabc".to_string(), position: None}))]))] - #[case::repeat_string_zero(vec![RuntimeValue::String(Shared::new("abc".to_string()))], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("".to_string()))]))] - #[case::repeat_array(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))]))] - #[case::repeat_array_zero(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))) - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::repeat_invalid_count(vec![RuntimeValue::String(Shared::new("abc".to_string()))], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-1).into()))) - ]) - ], - Ok(vec!["".to_string().into()]))] - #[case::repeat_invalid(vec![RuntimeValue::Number(42.into())], - vec![ - ast_call("repeat", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("".into()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "repeat".to_string(), - args: vec!["number".into(), "string".into()]})))] - #[case::debug(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("stderr", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::from_date(vec![RuntimeValue::String(Shared::new("2025-03-15T20:00:00+09:00".to_string()))], - vec![ - ast_call("from_date", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(1742036400_i64.into())]))] - #[case::from_date_invalid_format(vec![RuntimeValue::String(Shared::new("2021-01-01".to_string()))], - vec![ - ast_call("from_date", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, "premature end of input".to_string()))))] - #[case::from_date(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("from_date", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "from_date".to_string(), - args: vec!["number".into()]})))] - #[case::to_code_inline(vec![RuntimeValue::String(Shared::new("test1".to_string()))], - vec![ - ast_call("to_code_inline", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("elm".into()))), - ]), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::CodeInline(mq_markdown::CodeInline{value: "elm".into(), position: None}))]))] - #[case::to_md_name(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "name".to_string(), position: None}))], - vec![ - ast_call("to_md_name", SmallVec::new()), - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "text".to_string(), position: None}))]))] - #[case::to_md_name(vec![RuntimeValue::Number(123.into())], - vec![ - ast_call("to_md_name", SmallVec::new()), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::set_ref_markdown_definition(vec![RuntimeValue::new_markdown(mq_markdown::Node::Definition(mq_markdown::Definition{ident: "ident".into(), url: mq_markdown::Url::new("url".to_string()), title: None, label: None, position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("definition-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Definition(mq_markdown::Definition{ - ident: "ident".to_string(), - label: Some("definition-ref".to_string()), - url: mq_markdown::Url::new("url".to_string()), - title: None, - position: None - }))]))] - #[case::set_ref_markdown_link_ref(vec![RuntimeValue::new_markdown(mq_markdown::Node::LinkRef(mq_markdown::LinkRef{ident: "ident".into(), label: None, values: Vec::new(), position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("link-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::LinkRef(mq_markdown::LinkRef{ - ident: "ident".to_string(), - label: Some("link-ref".to_string()), - values: Vec::new(), - position: None - }))]))] - #[case::set_ref_markdown_link_ref(vec![RuntimeValue::new_markdown(mq_markdown::Node::LinkRef(mq_markdown::LinkRef{ident: "ident".into(), label: None, values: Vec::new(), position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("ident".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::LinkRef(mq_markdown::LinkRef{ - ident: "ident".to_string(), - label: None, - values: Vec::new(), - position: None - }))]))] - #[case::set_ref_markdown_image_ref(vec![RuntimeValue::new_markdown(mq_markdown::Node::ImageRef(mq_markdown::ImageRef{alt: "Image Alt".to_string(), ident: "ident".into(), label: None, position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("image-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::ImageRef(mq_markdown::ImageRef{ - ident: "ident".to_string(), - alt: "Image Alt".to_string(), - label: Some("image-ref".to_string()), - position: None - }))]))] - #[case::set_ref_markdown_image_ref(vec![RuntimeValue::new_markdown(mq_markdown::Node::ImageRef(mq_markdown::ImageRef{alt: "Image Alt".to_string(), ident: "ident".into(), label: None, position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("ident".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::ImageRef(mq_markdown::ImageRef{ - ident: "ident".to_string(), - alt: "Image Alt".to_string(), - label: None, - position: None - }))]))] - #[case::set_ref_markdown_footnote_ref(vec![RuntimeValue::new_markdown(mq_markdown::Node::FootnoteRef(mq_markdown::FootnoteRef{ident: "ident".into(), label: None, position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("footnote-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::FootnoteRef(mq_markdown::FootnoteRef{ - ident: "ident".to_string(), - label: Some("footnote-ref".to_string()), - position: None - }))]))] - #[case::set_ref_markdown_footnote(vec![RuntimeValue::new_markdown(mq_markdown::Node::Footnote(mq_markdown::Footnote{ident: "ident".into(), values: Vec::new(), position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("footnote".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Footnote(mq_markdown::Footnote{ - ident: "footnote".to_string(), - values: Vec::new(), - position: None - }))]))] - #[case::set_ref_not_link_or_image(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Simple text".to_string(), position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("text-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Simple text".to_string(), position: None}))]))] - #[case::set_ref_plain_string(vec![RuntimeValue::String(Shared::new("Not a markdown".to_string()))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("string-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("Not a markdown".to_string()))]))] - #[case::set_ref_none(vec![RuntimeValue::NONE], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("none-ref".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::set_ref_with_empty_id(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{url: mq_markdown::Url::new("https://example.com".to_string()), title: Some(mq_markdown::Title::new("title".to_string())), values: vec!["Link".to_string().into()], position: None}))], - vec![ - ast_call("set_ref", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{url: mq_markdown::Url::new("https://example.com".to_string()), title: Some(mq_markdown::Title::new("title".to_string())), values: vec!["Link".to_string().into()], position: None}))]))] - #[case::get_url_link(vec![RuntimeValue::new_markdown(mq_markdown::Node::Definition(mq_markdown::Definition{url: mq_markdown::Url::new("https://example.com".to_string()), ident: "ident".to_string(), label: None, title: Some(mq_markdown::Title::new("title".to_string())), position: None}))], - vec![ - ast_call("get_url", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "https://example.com".to_string(), position: None}))]))] - #[case::get_url_link(vec![RuntimeValue::new_markdown(mq_markdown::Node::Link(mq_markdown::Link{url: mq_markdown::Url::new("https://example.com".to_string()), title: Some(mq_markdown::Title::new("title".to_string())), values: vec!["Link".to_string().into()], position: None}))], - vec![ - ast_call("get_url", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "https://example.com".to_string(), position: None}))]))] - #[case::get_url_image(vec![RuntimeValue::new_markdown(mq_markdown::Node::Image(mq_markdown::Image{url: "https://example.com/image.png".to_string(), alt: "Image Alt".to_string(), title: Some("Image Title".to_string()), position: None}))], - vec![ - ast_call("get_url", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "https://example.com/image.png".to_string(), position: None}))]))] - #[case::get_url_not_link_or_image(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "Simple text".to_string(), position: None}))], - vec![ - ast_call("get_url", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Empty)]))] - #[case::get_url_string(vec![RuntimeValue::String(Shared::new("Not a markdown".to_string()))], - vec![ - ast_call("get_url", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::flatten_array_of_arrays(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("c".to_string())), RuntimeValue::String(Shared::new("d".to_string()))])) - ]))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("d".to_string())) - ]))]))] - #[case::flatten_array_with_nested_arrays(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("b".to_string())), RuntimeValue::String(Shared::new("c".to_string()))])), - RuntimeValue::String(Shared::new("d".to_string())) - ]))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("d".to_string())) - ]))]))] - #[case::flatten_deeply_nested_arrays(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))])), - RuntimeValue::String(Shared::new("c".to_string())) - ])), - RuntimeValue::String(Shared::new("d".to_string())) - ]))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("d".to_string())) - ]))]))] - #[case::flatten_empty_array(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::flatten_array_with_empty_arrays(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(Vec::new())), - RuntimeValue::Array(Shared::new(Vec::new())) - ]))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::flatten_mixed_type_arrays(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::Number(1.into())])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::Boolean(true), RuntimeValue::String(Shared::new("b".to_string()))])) - ]))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::Number(1.into()), - RuntimeValue::Boolean(true), - RuntimeValue::String(Shared::new("b".to_string())) - ]))]))] - #[case::flatten_array_with_none_values(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::NONE])), - RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("b".to_string())), RuntimeValue::String(Shared::new("c".to_string()))])) - ]))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())) - ]))]))] - #[case::flatten_non_array(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))]))] - #[case::flatten_none(vec![RuntimeValue::NONE], - vec![ - ast_call("flatten", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::set_array_valid_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("updated".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("updated".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::set_array_first_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("first".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("first".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::set_array_last_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("last".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("last".to_string())), - ]))]))] - #[case::set_array_out_of_bounds_positive(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(5.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("new".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::NONE, - RuntimeValue::NONE, - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("new".to_string())), - ]))]))] - #[case::set_array_negative_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-1).into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("negative".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("negative".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::set_array_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("value".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec!["value".into()]))]))] - #[case::set_array_mixed_types(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("text".to_string())), - RuntimeValue::Number(42.into()), - RuntimeValue::Boolean(true), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("replaced".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("text".to_string())), - RuntimeValue::String(Shared::new("replaced".to_string())), - RuntimeValue::Boolean(true), - ]))]))] - #[case::set_array_with_none(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("not_none".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("not_none".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::set_array_replace_with_none(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::set_array_single_element(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("only".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("changed".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("changed".to_string())), - ]))]))] - #[case::set_non_array(vec![RuntimeValue::String(Shared::new("not_an_array".to_string()))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("value".to_string()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "set".to_string(), - args: vec!["string".into(), "number".into(), "string".into()]})))] - #[case::set_array_non_number_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - ]))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("not_a_number".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("value".to_string()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "set".to_string(), - args: vec!["array".into(), "string".into(), "string".into()]})))] - #[case::del_dict_valid_key(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("key2"), RuntimeValue::String(Shared::new("value2".to_string()))), - (Ident::new("key3"), RuntimeValue::String(Shared::new("value3".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("key2".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("key3"), RuntimeValue::String(Shared::new("value3".to_string()))), - ].into_iter().collect()))]))] - #[case::del_dict_nonexistent_key(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("key2"), RuntimeValue::String(Shared::new("value2".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("nonexistent".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("key2"), RuntimeValue::String(Shared::new("value2".to_string()))), - ].into_iter().collect()))]))] - #[case::del_dict_empty(vec![RuntimeValue::new_dict()], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("any_key".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::new_dict()]))] - #[case::del_dict_single_key(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("only_key"), RuntimeValue::String(Shared::new("only_value".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("only_key".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::new_dict()]))] - #[case::del_dict_mixed_value_types(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("str_key"), RuntimeValue::String(Shared::new("string_value".to_string()))), - (Ident::new("num_key"), RuntimeValue::Number(42.into())), - (Ident::new("bool_key"), RuntimeValue::Boolean(true)), - (Ident::new("array_key"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("item".to_string()))]))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("num_key".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("str_key"), RuntimeValue::String(Shared::new("string_value".to_string()))), - (Ident::new("bool_key"), RuntimeValue::Boolean(true)), - (Ident::new("array_key"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("item".to_string()))]))), - ].into_iter().collect()))]))] - #[case::del_dict_with_number_key_as_string(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("2"), RuntimeValue::String(Shared::new("value2".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".to_string()))), - ]), - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("2"), RuntimeValue::String(Shared::new("value2".to_string()))), - ].into_iter().collect()))]))] - #[case::del_dict_with_number_index_error(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("key2"), RuntimeValue::String(Shared::new("value2".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]), - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "del".to_string(), - args: vec!["dict".into(), "number".into()]})))] - #[case::set_code_block_lang_string(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code { - value: "let x = 1;".to_string(), - lang: None, - fence: true, - meta: None, - position: None, - }))], - vec![ - ast_call("set_code_block_lang", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("rust".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code { - value: "let x = 1;".to_string(), - lang: Some("rust".to_string()), - fence: true, - meta: None, - position: None, - }))]))] - #[case::set_code_block_lang_empty(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code { - value: "let x = 1;".to_string(), - lang: Some("js".to_string()), - fence: true, - meta: None, - position: None, - }))], - vec![ - ast_call("set_code_block_lang", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code { - value: "let x = 1;".to_string(), - lang: None, - fence: true, - meta: None, - position: None, - }))]))] - #[case::set_code_block_lang_non_code(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "not code".to_string(), - position: None, - }))], - vec![ - ast_call("set_code_block_lang", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("rust".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "not code".to_string(), - position: None, - }))]))] - #[case::set_code_block_lang_none(vec![RuntimeValue::NONE], - vec![ - ast_call("set_code_block_lang", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("rust".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::set_list_ordered_true( - vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List { - values: vec!["Item 1".to_string().into(), "Item 2".to_string().into()], - ordered: false, - level: 1, - index: 0, - checked: None, - start: None, spread: false, - position: None, - }))], - vec![ - ast_call("set_list_ordered", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List { - values: vec!["Item 1".to_string().into(), "Item 2".to_string().into()], - ordered: true, - level: 1, - index: 0, - checked: None, - start: None, spread: false, - position: None, - }))]))] - #[case::set_list_ordered_false( - vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List { - values: vec!["Item 1".to_string().into(), "Item 2".to_string().into()], - ordered: true, - level: 1, - index: 0, - checked: None, - start: None, spread: false, - position: None, - }))], - vec![ - ast_call("set_list_ordered", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::List(mq_markdown::List { - values: vec!["Item 1".to_string().into(), "Item 2".to_string().into()], - ordered: false, - level: 1, - index: 0, - checked: None, - start: None, spread: false, - position: None, - }))]))] - #[case::set_list_ordered_non_list( - vec![RuntimeValue::String(Shared::new("not a list".to_string()))], - vec![ - ast_call("set_list_ordered", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("not a list".to_string()))]))] - #[case::range_number(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(5.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(3.into()), - RuntimeValue::Number(4.into()), - RuntimeValue::Number(5.into()), - ]))]))] - #[case::range_number_negative(vec![RuntimeValue::Number(5.into())], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(5.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(5.into()), - RuntimeValue::Number(4.into()), - RuntimeValue::Number(3.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(1.into()), - ]))]))] - #[case::range_string(vec![RuntimeValue::String(Shared::new("a".to_string()))], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("e".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("d".to_string())), - RuntimeValue::String(Shared::new("e".to_string())), - ]))]))] - #[case::range_string(vec![RuntimeValue::String(Shared::new("a".to_string()))], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("a1".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("a2".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a1".to_string())), - RuntimeValue::String(Shared::new("a2".to_string())), - ]))]))] - #[case::range_string_reverse(vec![RuntimeValue::String(Shared::new("e".to_string()))], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("e".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("e".to_string())), - RuntimeValue::String(Shared::new("d".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - ]))]))] - #[case::range_string_step_2(vec![RuntimeValue::String(Shared::new("a".to_string()))], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("e".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("e".to_string())), - ]))]))] - #[case::range_string_step_minus_2(vec![RuntimeValue::String(Shared::new("e".to_string()))], - vec![ - ast_call("range", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("e".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::Number((-2).into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("e".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - ]))]))] - #[case::insert_array_middle(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("x".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("x".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))]))] - #[case::insert_array_start(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("z".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("z".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))]))] - #[case::insert_array_end(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("c".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))]))] - #[case::insert_array_out_of_bounds(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - ]))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(5.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("b".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::NONE, - RuntimeValue::NONE, - RuntimeValue::NONE, - RuntimeValue::NONE, - RuntimeValue::String(Shared::new("b".to_string())), - ]))]))] - #[case::insert_array_negative_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-1).into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("z".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("z".to_string())), - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))]))] - #[case::insert_array_empty(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("first".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("first".to_string())), - ]))]))] - #[case::insert_non_array(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("value".to_string()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "insert".to_string(), - args: vec!["number".into(), "number".into(), "string".into()]})))] - #[case::insert_array_non_number_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - ]))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("not_a_number".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("value".to_string()))), - ]) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "insert".to_string(), - args: vec!["array".into(), "string".into(), "string".into()]})))] - #[case::insert_string_middle(vec![RuntimeValue::String(Shared::new("ac".to_string()))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("b".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("abc".to_string()))]))] - #[case::insert_string_start(vec![RuntimeValue::String(Shared::new("bc".to_string()))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("abc".to_string()))]))] - #[case::insert_string_end(vec![RuntimeValue::String(Shared::new("ab".to_string()))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("c".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("abc".to_string()))]))] - #[case::insert_string_out_of_bounds(vec![RuntimeValue::String(Shared::new("a".to_string()))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(5.into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("b".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("a b".to_string()))]))] - #[case::insert_string_negative_index(vec![RuntimeValue::String(Shared::new("bc".to_string()))], - vec![ - ast_call("insert", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-1).into()))), - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("abc".to_string()))]))] - #[case::to_markdown_string_string(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("to_markdown_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("test\n".to_string()))]))] - #[case::to_markdown_string_markdown_text(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test".to_string(), position: None}))], - vec![ - ast_call("to_markdown_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "test\n".to_string(), position: None}))]))] - #[case::to_markdown_string_markdown_heading(vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading{depth: 2, values: vec!["Heading".to_string().into()], position: None}))], - vec![ - ast_call("to_markdown_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "## Heading\n".to_string(), position: None}))]))] - #[case::to_markdown_string_markdown_code(vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code{value: "let x = 1;".to_string(), lang: Some("rust".to_string()), fence: true, meta: None, position: None}))], - vec![ - ast_call("to_markdown_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "```rust\nlet x = 1;\n```\n".to_string(), position: None}))]))] - #[case::to_markdown_string_none(vec![RuntimeValue::NONE], - vec![ - ast_call("to_markdown_string", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("".to_string()))]))] - #[case::break_in_foreach( - vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(3.into()), - ]))], - vec![ - ast_node(ast::Expr::Foreach( - IdentWithToken::new("x"), - ast_node(ast::Expr::Self_), - vec![ - ast_node(ast::Expr::If(smallvec![ - ( - Some(ast_node(ast::Expr::Call( - IdentWithToken::new("eq"), - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("x"))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ], - ))), - ast_node(ast::Expr::Break(None)), - ), - ( - None, - ast_node(ast::Expr::Ident(IdentWithToken::new("x"))), - ), - ])), - ], - )), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - ]))]) - )] - #[case::continue_in_foreach( - vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(3.into()), - ]))], - vec![ - ast_node(ast::Expr::Foreach( - IdentWithToken::new("x"), - ast_node(ast::Expr::Self_), - vec![ - ast_node(ast::Expr::If(smallvec![ - ( - Some(ast_node(ast::Expr::Call( - IdentWithToken::new("eq"), - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("x"))), - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - ], - ))), - ast_node(ast::Expr::Continue), - ), - ( - None, - ast_node(ast::Expr::Ident(IdentWithToken::new("x"))), - ), - ])), - ], - )), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(3.into()), - ]))]) - )] - #[case::foreach_string( - vec![RuntimeValue::String(Shared::new("abc".to_string()))], - vec![ - ast_node(ast::Expr::Foreach( - IdentWithToken::new("c"), - ast_node(ast::Expr::Self_), - vec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("c"))), - ], - )), - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - RuntimeValue::String(Shared::new("c".to_string())), - ]))]) - )] - #[case::loop_immediate_break( - vec![RuntimeValue::Number(10.into())], - vec![ - ast_node(ast::Expr::Loop( - vec![ - ast_node(ast::Expr::Break(None)), - ], - )), - ], - Ok(vec![RuntimeValue::Number(10.into())]) - )] - #[case::to_array_string(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("to_array", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("t".to_string())), RuntimeValue::String(Shared::new("e".to_string())), RuntimeValue::String(Shared::new("s".to_string())), RuntimeValue::String(Shared::new("t".to_string()))]))]))] - #[case::to_array_number(vec![RuntimeValue::Number(42.into())], - vec![ - ast_call("to_array", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(42.into())]))]))] - #[case::to_array_bool(vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("to_array", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Boolean(true)]))]))] - #[case::to_array_array(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]))], - vec![ - ast_call("to_array", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]))]))] - #[case::to_array_empty_array(vec![RuntimeValue::Array(Shared::new(Vec::new()))], - vec![ - ast_call("to_array", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::to_array_dict(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key"), RuntimeValue::String(Shared::new("value".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("to_array", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key"), RuntimeValue::String(Shared::new("value".to_string()))), - ].into_iter().collect()))]))]))] - #[case::type_none(vec![RuntimeValue::NONE], - vec![ - ast_call("type", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("None".to_string()))]))] - #[case::to_text(vec![RuntimeValue::NONE], - vec![ - ast_call("to_text", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::starts_with(vec![RuntimeValue::NONE], - vec![ - ast_call("starts_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::ends_with(vec![RuntimeValue::NONE], - vec![ - ast_call("ends_with", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("te".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::FALSE]))] - #[case::rindex(vec![RuntimeValue::NONE], - vec![ - ast_call("rindex", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("String".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number((-1).into())]))] - #[case::utf8bytelen(vec![RuntimeValue::NONE], - vec![ - ast_call("utf8bytelen", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(0.into())]))] - #[case::index(vec![RuntimeValue::NONE], - vec![ - ast_call("index", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))) - ]) - ], - Ok(vec![RuntimeValue::Number((-1).into())]))] - #[case::del(vec![RuntimeValue::NONE], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::downcase(vec![RuntimeValue::NONE], - vec![ - ast_call("downcase", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::slice(vec![RuntimeValue::NONE], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(0.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::slice(vec![RuntimeValue::NONE], - vec![ - ast_call("len", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(0.into())]))] - #[case::slice(vec![RuntimeValue::NONE], - vec![ - ast_call("upcase", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::slice_array_negative_start_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - RuntimeValue::String(Shared::new("item4".to_string())), - RuntimeValue::String(Shared::new("item5".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-2).into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item4".to_string())), - ]))]))] - #[case::slice_array_negative_end_index(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - RuntimeValue::String(Shared::new("item4".to_string())), - RuntimeValue::String(Shared::new("item5".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number((-1).into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - RuntimeValue::String(Shared::new("item4".to_string())), - ]))]))] - #[case::slice_array_both_negative_indices(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item1".to_string())), - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - RuntimeValue::String(Shared::new("item4".to_string())), - RuntimeValue::String(Shared::new("item5".to_string())), - ]))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-4).into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number((-2).into()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("item2".to_string())), - RuntimeValue::String(Shared::new("item3".to_string())), - ]))]))] - #[case::slice_string_negative_start_index(vec![RuntimeValue::String(Shared::new("abcdef".to_string()))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-3).into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(6.into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("def".to_string()))]))] - #[case::slice_string_negative_end_index(vec![RuntimeValue::String(Shared::new("abcdef".to_string()))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number((-1).into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("bcde".to_string()))]))] - #[case::slice_string_both_negative_indices(vec![RuntimeValue::String(Shared::new("abcdef".to_string()))], - vec![ - ast_call("slice", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number((-5).into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number((-2).into()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("bcd".to_string()))]))] - #[case::to_code(vec![RuntimeValue::NONE], - vec![ - ast_call("to_code", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_code(vec![RuntimeValue::NONE], - vec![ - ast_call("update", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_code_inline(vec![RuntimeValue::NONE], - vec![ - ast_call("to_code_inline", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_link(vec![RuntimeValue::NONE], - vec![ - ast_call("to_link", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_strong(vec![RuntimeValue::NONE], - vec![ - ast_call("to_strong", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_em(vec![RuntimeValue::NONE], - vec![ - ast_call("to_em", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_md_text(vec![RuntimeValue::NONE], - vec![ - ast_call("to_md_text", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::to_md_list(vec![RuntimeValue::NONE], - vec![ - ast_call("to_md_list", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::match_(vec![RuntimeValue::NONE], - vec![ - ast_call("regex_match", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Array(Shared::new(Vec::new()))]))] - #[case::gsub(vec![RuntimeValue::NONE], - vec![ - ast_call("gsub", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String(r"\d+".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String(r"1".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::replace(vec![RuntimeValue::NONE], - vec![ - ast_call("replace", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("1".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("2".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::trim(vec![RuntimeValue::NONE], - vec![ - ast_call("trim", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::split(vec![RuntimeValue::NONE], - vec![ - ast_call("split", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("test".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::empty_array()]))] - #[case::to_md_name(vec![RuntimeValue::NONE], - vec![ - ast_call("to_md_name", SmallVec::new()), - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::get_url_none(vec![RuntimeValue::NONE], - vec![ - ast_call("get_url", SmallVec::new()) - ], - Ok(vec![RuntimeValue::NONE]))] - #[case::negate_positive(vec![RuntimeValue::Number(1.into())], - vec![ - ast_call("negate", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number((-1).into())]))] - #[case::negate_negative(vec![RuntimeValue::Number((-42).into())], - vec![ - ast_call("negate", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(42.into())]))] - #[case::negate_zero(vec![RuntimeValue::Number(0.into())], - vec![ - ast_call("negate", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(0.into())]))] - #[case::negate_decimal(vec![RuntimeValue::Number(PI.into())], - vec![ - ast_call("negate", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number((-PI).into())]))] - #[case::negate_invalid_type(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("negate", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{ - token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into() }, - name: "negate".to_string(), - args: vec!["string".into()] - })))] - #[case::and_true_last_value( - vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::String("last".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("last".to_string()))]) - )] - #[case::and_false_first_value( - vec![RuntimeValue::Boolean(false)], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::String("should_not_evaluate".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::and_mixed_values( - vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Number(42.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(42.into())]) - )] - #[case::and_multiple_true( - vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::String("final".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("final".to_string()))]) - )] - #[case::and_first_false( - vec![RuntimeValue::Boolean(false)], - vec![ - ast_call("and", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::String("should_not_evaluate".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::or_true_first_value( - vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::String("should_not_evaluate".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::or_false_last_value( - vec![RuntimeValue::Boolean(false)], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::String("last".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("last".to_string()))]) - )] - #[case::or_multiple_false_then_true( - vec![RuntimeValue::Boolean(false)], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Number(123.into()))), - ]) - ], - Ok(vec![RuntimeValue::Number(123.into())]) - )] - #[case::or_all_false( - vec![RuntimeValue::Boolean(false)], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ]) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::or_first_true( - vec![RuntimeValue::Boolean(true)], - vec![ - ast_call("or", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Number(999.into()))), - ]) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_and_both_true( - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_and_first_false( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::expr_and_second_false( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::expr_and_return_last_value( - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::String("last".to_string()))), - ])), - }) - ], - Ok(vec![RuntimeValue::String(Shared::new("last".to_string()))]) - )] - #[case::expr_or_both_true( - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_or_first_true( - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_or_second_true( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_or_both_false( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::expr_or_return_last_value( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::String("last".to_string()))), - ])), - }) - ], - Ok(vec![RuntimeValue::String(Shared::new("last".to_string()))]) - )] - #[case::expr_and_three_all_true( - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_and_three_middle_false( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::expr_or_three_all_false( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::expr_or_three_middle_true( - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_or_three_last_true( - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_or_and_mixed_first_arm_false( - // Or([And([true, false]), And([true, true])]) => true - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - ast_node(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_or_and_mixed_all_arms_false( - // Or([And([false, true]), And([false, true])]) => false - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Or(vec![ - ast_node(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - ast_node(ast::Expr::And(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::expr_and_or_mixed_both_or_true( - // And([Or([false, true]), Or([false, true])]) => true - vec![RuntimeValue::Boolean(true)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - ast_node(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::expr_and_or_mixed_second_or_false( - // And([Or([false, true]), Or([false, false])]) => false - vec![RuntimeValue::Boolean(false)], - vec![ - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::And(vec![ - ast_node(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - ])), - ast_node(ast::Expr::Or(vec![ - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ast_node(ast::Expr::Literal(ast::Literal::Bool(false))), - ])), - ])), - }) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::intern_string( - vec![RuntimeValue::String(Shared::new("hello".to_string()))], - vec![ - ast_call("intern", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("hello".to_string()))]) - )] - #[case::intern_same_string_twice( - vec![RuntimeValue::String(Shared::new("repeat".to_string())), RuntimeValue::String(Shared::new("repeat".to_string()))], - vec![ - ast_call("intern", SmallVec::new()), - ast_call("intern", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("repeat".to_string())), RuntimeValue::String(Shared::new("repeat".to_string()))]) - )] - #[case::intern_different_strings( - vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))], - vec![ - ast_call("intern", SmallVec::new()), - ast_call("intern", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]) - )] - #[case::intern_number( - vec![RuntimeValue::Number(42.into())], - vec![ - ast_call("intern", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("42".to_string()))]) - )] - #[case::intern_none( - vec![RuntimeValue::NONE], - vec![ - ast_call("intern", SmallVec::new()) - ], - Ok(vec![RuntimeValue::String(Shared::new("".to_string()))]) - )] - #[case::infinite( - vec![RuntimeValue::NONE], - vec![ - ast_call("infinite", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Number(INFINITE)]) - )] - #[case::is_nan_with_nan( - vec![RuntimeValue::Number(NAN)], - vec![ - ast_call("is_nan", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Boolean(true)]) - )] - #[case::is_nan_with_number( - vec![RuntimeValue::Number(42.0.into())], - vec![ - ast_call("is_nan", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Boolean(false)]) - )] - #[case::coalesce_first_non_none( - vec![RuntimeValue::NONE], - vec![ - ast_call("coalesce", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ast_node(ast::Expr::Literal(ast::Literal::String("first".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("first".to_string()))]) - )] - #[case::coalesce_second_non_none( - vec![RuntimeValue::NONE], - vec![ - ast_call("coalesce", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ast_node(ast::Expr::Literal(ast::Literal::None)), - ]) - ], - Ok(vec![RuntimeValue::NONE]) - )] - #[case::coalesce_first_value_non_none( - vec![RuntimeValue::String(Shared::new("value".to_string()))], - vec![ - ast_call("coalesce", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("value".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("value".to_string()))]) - )] - #[case::coalesce_array( - vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::NONE, RuntimeValue::String(Shared::new("foo".to_string()))]))], - vec![ - ast_call("coalesce", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::None)), - ast_node(ast::Expr::Literal(ast::Literal::String("bar".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("bar".to_string()))]) - )] - #[case::eq_symbol( - vec![RuntimeValue::Symbol(Ident::new("sym"))], - vec![ - ast_call( - "eq", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym")))), - ], - ), - ], - Ok(vec![RuntimeValue::TRUE]) - )] - #[case::eq_symbol_false( - vec![RuntimeValue::Symbol(Ident::new("sym1"))], - vec![ - ast_call( - "eq", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym1")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym2")))), - ], - ), - ], - Ok(vec![RuntimeValue::FALSE]) - )] - #[case::lt_symbol( - vec![RuntimeValue::Symbol(Ident::new("a"))], - vec![ - ast_call( - "lt", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("a")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("b")))), - ], - ), - ], - Ok(vec![RuntimeValue::TRUE]) - )] - #[case::lte_symbol_true( - vec![RuntimeValue::Symbol(Ident::new("a"))], - vec![ - ast_call( - "lte", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("a")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("a")))), - ], - ), - ], - Ok(vec![RuntimeValue::TRUE]) - )] - #[case::gt_symbol_false( - vec![RuntimeValue::Symbol(Ident::new("a"))], - vec![ - ast_call( - "gt", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("a")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("b")))), - ], - ), - ], - Ok(vec![RuntimeValue::FALSE]) - )] - #[case::gte_symbol_true( - vec![RuntimeValue::Symbol(Ident::new("b"))], - vec![ - ast_call( - "gte", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("b")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("b")))), - ], - ), - ], - Ok(vec![RuntimeValue::TRUE]) - )] - #[case::gte_symbol_false( - vec![RuntimeValue::Symbol(Ident::new("a"))], - vec![ - ast_call( - "gte", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("a")))), - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("b")))), - ], - ), - ], - Ok(vec![RuntimeValue::FALSE]) - )] - #[case::get_dict_symbol_key( - vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - (Ident::new("key2"), RuntimeValue::String(Shared::new("value2".to_string()))), - (Ident::new("key3"), RuntimeValue::String(Shared::new("value3".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("get", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("key2")))), - ]) - ], - Ok(vec![RuntimeValue::String(Shared::new("value2".to_string()))]) - )] - #[case::get_dict_symbol_key_not_found( - vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("get", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("keyX")))), - ]) - ], - Ok(vec![RuntimeValue::NONE]) - )] - #[case::set_dict_symbol_key( - vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - (Ident::new("sym2"), RuntimeValue::String(Shared::new("v2".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym2")))), - ast_node(ast::Expr::Literal(ast::Literal::String("updated".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - (Ident::new("sym2"), RuntimeValue::String(Shared::new("updated".to_string()))), - ].into_iter().collect()))]) - )] - #[case::set_dict_symbol_key_new( - vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("set", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym3")))), - ast_node(ast::Expr::Literal(ast::Literal::String("newval".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - (Ident::new("sym3"), RuntimeValue::String(Shared::new("newval".to_string()))), - ].into_iter().collect()))]) - )] - #[case::del_dict_symbol_key( - vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - (Ident::new("sym2"), RuntimeValue::String(Shared::new("v2".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("sym1")))), - ]) - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym2"), RuntimeValue::String(Shared::new("v2".to_string()))), - ].into_iter().collect()))]) - )] - #[case::del_dict_symbol_key_not_found( - vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - ].into_iter().collect()))], - vec![ - ast_call("del", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Symbol(Ident::new("symX")))), - ]) - ], - Ok(vec![RuntimeValue::Dict(Shared::new(vec![ - (Ident::new("sym1"), RuntimeValue::String(Shared::new("v1".to_string()))), - ].into_iter().collect()))]) - )] - #[case::to_markdown_string_to_markdown_array( - vec![RuntimeValue::String(Shared::new("a\n\nb\n\nc".to_string()))], - vec![ - ast_call("to_markdown", SmallVec::new()) - ], - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "a".to_string(), position: None})), - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "b".to_string(), position: None})), - RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "c".to_string(), position: None})), - ]))])) - ] - #[case::to_markdown_none(vec![RuntimeValue::NONE], - vec![ - ast_call("to_markdown", SmallVec::new()) - ], - Err(InnerError::Runtime(RuntimeError::InvalidTypes{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, - name: "to_markdown".to_string(), - args: vec!["None".into()]})))] - #[case::error_with_message(vec![RuntimeValue::String(Shared::new("test".to_string()))], - vec![ - ast_call("error", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("Custom error message".to_string()))) - ]) - ], - Err(InnerError::Runtime(RuntimeError::UserDefined{token: Token { range: Range::default(), kind: TokenKind::Eof, module_id: 1.into()}, message: "Custom error message".to_string()})))] - #[case::get_markdown_position_line_col( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{ - value: "test".to_string(), - position: Some(mq_markdown::Position{start: mq_markdown::Point{line: 1, column: 10}, end: mq_markdown::Point{line: 2, column: 15}}) - }))], - vec![ - ast_call("_get_markdown_position", SmallVec::new()), - ast_call("get", smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("start_line".to_string()))), - ]) - ], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text{value: "1".to_string(), position: None}))]))] - // --- or-pattern tests --- - #[case::match_or_first_alt_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_second_alt_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_no_alt_matches_falls_to_wildcard( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(3.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("other".to_string()))]) - )] - #[case::match_or_three_alts_middle_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(2.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - Pattern::Literal(ast::Literal::Number(3.into())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_string_first_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::String("a".to_string()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::String("a".to_string())), - Pattern::Literal(ast::Literal::String("b".to_string())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_string_second_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::String("b".to_string()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::String("a".to_string())), - Pattern::Literal(ast::Literal::String("b".to_string())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_bool_true_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Bool(true)), - Pattern::Literal(ast::Literal::Bool(false)), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_none_literal_first_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::None)), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::None), - Pattern::Literal(ast::Literal::Number(1.into())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_type_string_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::String("hello".to_string()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Type(crate::Ident::new("string")), - Pattern::Type(crate::Ident::new("number")), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("string or number".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("string or number".to_string()))]) - )] - #[case::match_or_type_number_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(42.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Type(crate::Ident::new("string")), - Pattern::Type(crate::Ident::new("number")), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("string or number".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("string or number".to_string()))]) - )] - #[case::match_or_type_no_match_falls_to_wildcard( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Bool(true))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Type(crate::Ident::new("string")), - Pattern::Type(crate::Ident::new("number")), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("string or number".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("other".to_string()))]) - )] - #[case::match_or_no_arm_matches_returns_none( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(99.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - ]), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::NONE]) - )] - #[case::match_or_with_guard_passes( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - ]), - guard: Some(ast_node(ast::Expr::Literal(ast::Literal::Bool(true)))), - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("matched".to_string()))]) - )] - #[case::match_or_with_guard_fails( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - smallvec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(ast::Literal::Number(1.into())), - Pattern::Literal(ast::Literal::Number(2.into())), - ]), - guard: Some(ast_node(ast::Expr::Literal(ast::Literal::Bool(false)))), - body: ast_node(ast::Expr::Literal(ast::Literal::String("guarded".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("other".to_string()))]) - )] - #[case::match_type_bytes_matches( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_call("to_bytes", smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("hello".to_string())))]), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("bytes")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("bytes".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("bytes".to_string()))]) - )] - #[case::match_type_bytes_no_match( - vec![RuntimeValue::NONE], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Literal(ast::Literal::String("hello".to_string()))), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("bytes")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("bytes".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("other".to_string()))]) - )] - #[case::match_type_node_kind_heading_matches( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading { - values: vec![], - position: None, - depth: 1, - }))], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Self_), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("h1")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("h1".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "h1".to_string(), - position: None, - }))]) - )] - #[case::match_type_node_kind_heading_depth_mismatch_falls_to_wildcard( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading { - values: vec![], - position: None, - depth: 2, - }))], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Self_), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("h1")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("h1".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "other".to_string(), - position: None, - }))]) - )] - #[case::match_type_node_kind_generic_heading_matches_any_depth( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading { - values: vec![], - position: None, - depth: 5, - }))], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Self_), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("h")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("heading".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "heading".to_string(), - position: None, - }))]) - )] - #[case::match_type_node_kind_unknown_name_no_match( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Heading(mq_markdown::Heading { - values: vec![], - position: None, - depth: 1, - }))], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Self_), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("not_a_real_node_kind")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "other".to_string(), - position: None, - }))]) - )] - #[case::match_type_node_kind_attribute_name_does_not_match( - vec![RuntimeValue::new_markdown(mq_markdown::Node::Code(mq_markdown::Code { - value: "fn main() {}".to_string(), - lang: Some("rust".to_string()), - position: None, - meta: None, - fence: true, - }))], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Self_), - smallvec![ - MatchArm { - // `:lang` is an attribute selector, not a node-kind selector, so it must not match. - pattern: Pattern::Type(crate::Ident::new("lang")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::new_markdown(mq_markdown::Node::Text(mq_markdown::Text { - value: "other".to_string(), - position: None, - }))]) - )] - #[case::match_type_node_kind_no_match_on_non_markdown( - vec![RuntimeValue::Number(42.into())], - vec![ast_node(ast::Expr::Match( - ast_node(ast::Expr::Self_), - smallvec![ - MatchArm { - pattern: Pattern::Type(crate::Ident::new("code")), - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("matched".to_string()))), - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: ast_node(ast::Expr::Literal(ast::Literal::String("other".to_string()))), - }, - ] - ))], - Ok(vec![RuntimeValue::String(Shared::new("other".to_string()))]) - )] - fn $name( - $token_arena: Shared>>>, - #[case] $runtime_values: Vec, - #[case] $program: Program, - #[case] $expected: Result, InnerError>, - ) { - $body - } - }; - } - - crate::eval_table_cases!(test_eval, token_arena, runtime_values, program, expected, { - assert_eq!( - Evaluator::new(DefaultModuleLoader::default(), Shared::clone(&token_arena)) - .eval(&program, runtime_values.into_iter()), - expected - ); - }); - - #[test] - fn test_include() { - let (temp_dir, temp_file_path) = create_file("test_module.mq", "def func1(): 42; | let val1 = 1"); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Include(ast::Literal::String("test_module".to_string()))), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Call(IdentWithToken::new("func1"), SmallVec::new())), - }), - ]; - assert_eq!( - Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter() - ), - Ok(vec![RuntimeValue::Number(42.into())]) - ); - } - - #[test] - fn test_import_qualified_access_function() { - let (temp_dir, temp_file_path) = - create_file("test_qualified.mq", r#"def greet(name): "Hello, " + name + "!";"#); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import( - ast::Literal::String("test_qualified".to_string()), - None, - )), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::QualifiedAccess( - vec![IdentWithToken::new("test_qualified")], - ast::AccessTarget::Call( - IdentWithToken::new("greet"), - smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("World".to_string())))], - ), - )), - }), - ]; - assert_eq!( - Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter() - ), - Ok(vec![RuntimeValue::String(Shared::new("Hello, World!".to_string()))]) - ); - } - - #[test] - fn test_import_alias_qualified_access() { - let (temp_dir, temp_file_path) = create_file("test_alias.mq", r#"def greet(name): "Hello, " + name + "!";"#); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import( - ast::Literal::String("test_alias".to_string()), - Some(IdentWithToken::new("greeter")), - )), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::QualifiedAccess( - vec![IdentWithToken::new("greeter")], - ast::AccessTarget::Call( - IdentWithToken::new("greet"), - smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("World".to_string())))], - ), - )), - }), - ]; - assert_eq!( - Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter() - ), - Ok(vec![RuntimeValue::String(Shared::new("Hello, World!".to_string()))]) - ); - } - - /// The canonical module name is not bound when the import is aliased, mirroring - /// how `import "x" as y` in other languages only exposes `y`. - #[test] - fn test_import_alias_does_not_bind_canonical_name() { - let (temp_dir, temp_file_path) = create_file("test_alias_only.mq", r#"let answer = 42"#); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import( - ast::Literal::String("test_alias_only".to_string()), - Some(IdentWithToken::new("m")), - )), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::QualifiedAccess( - vec![IdentWithToken::new("test_alias_only")], - ast::AccessTarget::Ident(IdentWithToken::new("answer")), - )), - }), - ]; - let result = Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter(), - ); - assert!(matches!( - result, - Err(InnerError::Runtime(RuntimeError::UndefinedReference(_, _, _))) - )); - } - - #[test] - fn test_import_qualified_access_value() { - let (temp_dir, temp_file_path) = create_file("test_qualified_val.mq", r#"let answer = 42"#); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import( - ast::Literal::String("test_qualified_val".to_string()), - None, - )), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::QualifiedAccess( - vec![IdentWithToken::new("test_qualified_val")], - ast::AccessTarget::Ident(IdentWithToken::new("answer")), - )), - }), - ]; - assert_eq!( - Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter() - ), - Ok(vec![RuntimeValue::Number(42.into())]) - ); - } - - /// Builds an AST program that imports `module_name` and accesses `member_name` - /// via a bare `QualifiedAccess(_, AccessTarget::Ident)` (i.e. without parentheses). - fn make_paren_free_qa_program(module_name: &str, member_name: &str) -> Program { - vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import(ast::Literal::String(module_name.to_string()), None)), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::QualifiedAccess( - vec![IdentWithToken::new(module_name)], - ast::AccessTarget::Ident(IdentWithToken::new(member_name)), - )), - }), - ] - } - - /// Parameterised tests for paren-free calls via `QualifiedAccess`. - /// - /// Verifies that: - /// - 0-arg functions are called immediately. - /// - 1-arg functions receive the current pipeline value. - /// - 1-required + 1-default functions behave like 1-arg. - /// - Non-function module members are returned as-is. - #[rstest] - // 0-arg user-defined function: called immediately, returns its body. - #[case::zero_arg_user_fn( - "qa_pf_zero_arg", - r#"def greet(): "Hello!";"#, - "greet", - RuntimeValue::String(Shared::new("ignored".to_string())), - Ok(vec![RuntimeValue::String(Shared::new("Hello!".to_string()))]) - )] - // 1-arg user-defined function: pipeline value (5) is bound to the parameter. - #[case::one_arg_user_fn( - "qa_pf_one_arg", - r#"def double(x): x * 2;"#, - "double", - RuntimeValue::Number(5.into()), - Ok(vec![RuntimeValue::Number(10.into())]) - )] - // 1 required + 1 default param: ≤1 required, so pipeline value (10) is bound. - #[case::one_required_one_default( - "qa_pf_one_default", - r#"def inc(x, step = 1): x + step;"#, - "inc", - RuntimeValue::Number(10.into()), - Ok(vec![RuntimeValue::Number(11.into())]) - )] - // Non-function module constant: returned unchanged (no auto-call). - #[case::non_function_constant( - "qa_pf_non_fn", - r#"let answer = 42"#, - "answer", - RuntimeValue::String(Shared::new("ignored".to_string())), - Ok(vec![RuntimeValue::Number(42.into())]) - )] - fn test_import_qualified_access_paren_free( - token_arena: Shared>>>, - #[case] module_name: &str, - #[case] module_content: &str, - #[case] member_name: &str, - #[case] input: RuntimeValue, - #[case] expected: Result, InnerError>, - ) { - let (temp_dir, temp_file_path) = create_file(&format!("{module_name}.mq"), module_content); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = make_paren_free_qa_program(module_name, member_name); - assert_eq!( - Evaluator::new(loader, token_arena).eval(&program, vec![input].into_iter()), - expected - ); - } - - /// A 2-arg function accessed via QualifiedAccess must NOT be auto-called; - /// the function value itself should be the pipeline output. - #[rstest] - fn test_import_qualified_access_paren_free_skips_multi_arg(token_arena: Shared>>>) { - let (temp_dir, temp_file_path) = create_file("qa_pf_multi_skip.mq", r#"def add(a, b): a + b;"#); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = make_paren_free_qa_program("qa_pf_multi_skip", "add"); - let result = - Evaluator::new(loader, token_arena).eval(&program, vec![RuntimeValue::Number(1.into())].into_iter()); - assert!(matches!(result, Ok(ref v) if matches!(v[0], RuntimeValue::Function(_)))); - } - - #[test] - fn test_import_qualified_access_with_args() { - let (temp_dir, temp_file_path) = create_file( - "test_qualified_math.mq", - r#"def add2(a, b): a + b; - def multiply(x, y): x * y;"#, - ); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - - let program = vec![ - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import( - ast::Literal::String("test_qualified_math".to_string()), - None, - )), - }), - Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::QualifiedAccess( - vec![IdentWithToken::new("test_qualified_math")], - ast::AccessTarget::Call( - IdentWithToken::new("add2"), - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(10.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(20.into()))) - ], - ), - )), - }), - ]; - assert_eq!( - Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter() - ), - Ok(vec![RuntimeValue::Number(30.into())]) - ); - } - - #[test] - fn test_import_error() { - let loader: ModuleLoader = ModuleLoader::default(); - let program = vec![Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Import(ast::Literal::String("not_found".to_string()), None)), - })]; - assert_eq!( - Evaluator::new(loader, token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter() - ), - Err(InnerError::Runtime(RuntimeError::ModuleLoadError( - ModuleError::NotFound(Cow::Owned("not_found.mq".to_string())) - ))) - ); - } - - #[rstest] - #[case::simple_interpolated_string( - vec![RuntimeValue::String(Shared::new("world".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Hello, ".to_string()), - ast::StringSegment::Self_, - ast::StringSegment::Text("!".to_string()), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Hello, world!".to_string()))]) - )] - #[case::interpolated_string_with_number( - vec![RuntimeValue::Number(42.into())], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("The answer is ".to_string()), - ast::StringSegment::Self_, - ast::StringSegment::Text(".".to_string()), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("The answer is 42.".to_string()))]) - )] - #[case::interpolated_string_with_bool( - vec![RuntimeValue::Boolean(true)], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Value: ".to_string()), - ast::StringSegment::Self_, - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Value: true".to_string()))]) - )] - #[case::interpolated_string_with_none( - vec![RuntimeValue::NONE], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("None: ".to_string()), - ast::StringSegment::Self_, - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("None: ".to_string()))]) - )] - #[case::interpolated_string_with_array( - vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::String(Shared::new("b".to_string())), - ]))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Array: ".to_string()), - ast::StringSegment::Self_, - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new(r#"Array: ["a", "b"]"#.to_string()))]) - )] - #[case::interpolated_string_only_literal( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Just a string".to_string()), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Just a string".to_string()))]) - )] - #[case::interpolated_string_empty( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![])), - ], - Ok(vec![RuntimeValue::String(Shared::new("".to_string()))]) - )] - #[case::interpolated_string_with_env_var( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("HOME: ".to_string()), - ast::StringSegment::Env("HOME".into()), - ])), - ], - { - unsafe { std::env::set_var("HOME", "/home/testuser") }; - Ok(vec![RuntimeValue::String(Shared::new("HOME: /home/testuser".to_string()))]) - } - )] - #[case::interpolated_string_with_missing_env_var( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("MISSING: ".to_string()), - ast::StringSegment::Env("MQ_TEST_MISSING_ENV".into()), - ])), - ], - { - unsafe { std::env::remove_var("MQ_TEST_MISSING_ENV") }; - Err(RuntimeError::EnvNotFound( - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - "MQ_TEST_MISSING_ENV".into(), - ).into()) - } - )] - #[case::interpolated_string_env_and_self( - vec![RuntimeValue::String(Shared::new("value".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Env("USER".into()), - ast::StringSegment::Text(":".to_string()), - ast::StringSegment::Self_, - ])), - ], - { - unsafe { std::env::set_var("USER", "tester") }; - Ok(vec![RuntimeValue::String(Shared::new("tester:value".to_string()))]) - } - )] - #[case::interpolated_string_env_only( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Env("USER".into()), - ])), - ], - { - unsafe { std::env::set_var("USER", "tester") }; - Ok(vec![RuntimeValue::String(Shared::new("tester".to_string()))]) - } - )] - #[case::interpolated_string_env_and_literal( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("User: ".to_string()), - ast::StringSegment::Env("USER".into()), - ast::StringSegment::Text("!".to_string()), - ])), - ], - { - unsafe { std::env::set_var("USER", "tester") }; - Ok(vec![RuntimeValue::String(Shared::new("User: tester!".to_string()))]) - } - )] - #[case::interpolated_string_with_expr_literal( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Value: ".to_string()), - ast::StringSegment::Expr(ast_node(ast::Expr::Literal(ast::Literal::Number(42.into())))), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Value: 42".to_string()))]) - )] - #[case::interpolated_string_with_expr_string( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Result: ".to_string()), - ast::StringSegment::Expr(ast_node(ast::Expr::Literal(ast::Literal::String("hello".to_string())))), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Result: hello".to_string()))]) - )] - #[case::interpolated_string_with_expr_call( - vec![RuntimeValue::Number(10.into())], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Doubled: ".to_string()), - ast::StringSegment::Expr(ast_call("add", smallvec![ast_node(ast::Expr::Self_), ast_node(ast::Expr::Self_)])), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Doubled: 20".to_string()))]) - )] - #[case::interpolated_string_with_multiple_exprs( - vec![RuntimeValue::Number(5.into())], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Value: ".to_string()), - ast::StringSegment::Expr(ast_node(ast::Expr::Self_)), - ast::StringSegment::Text(", Squared: ".to_string()), - ast::StringSegment::Expr(ast_call("mul", smallvec![ast_node(ast::Expr::Self_), ast_node(ast::Expr::Self_)])), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Value: 5, Squared: 25".to_string()))]) - )] - #[case::interpolated_string_with_expr_and_self( - vec![RuntimeValue::String(Shared::new("world".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Expr(ast_node(ast::Expr::Literal(ast::Literal::String("Hello".to_string())))), - ast::StringSegment::Text(", ".to_string()), - ast::StringSegment::Self_, - ast::StringSegment::Text("!".to_string()), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Hello, world!".to_string()))]) - )] - #[case::interpolated_string_with_expr_bool( - vec![RuntimeValue::String(Shared::new("ignored".to_string()))], - vec![ - ast_node(ast::Expr::InterpolatedString(vec![ - ast::StringSegment::Text("Is true: ".to_string()), - ast::StringSegment::Expr(ast_node(ast::Expr::Literal(ast::Literal::Bool(true)))), - ])), - ], - Ok(vec![RuntimeValue::String(Shared::new("Is true: true".to_string()))]) - )] - fn test_interpolated_string_eval( - token_arena: Shared>>>, - #[case] runtime_values: Vec, - #[case] program: Program, - #[case] expected: Result, InnerError>, - ) { - let mut evaluator = Evaluator::new(DefaultModuleLoader::default(), token_arena); - evaluator.set_io(Shared::new(SandboxedIo::new(NativeIo::default()).allow_env(true))); - assert_eq!(evaluator.eval(&program, runtime_values.into_iter()), expected); - } - - #[test] - fn test_default_params_with_all_args() { - // Test: def greet(name, greeting = "Hello"): greeting with greet("Alice", "Hi") - let params = smallvec![ - ast::Param::new(IdentWithToken::new("name")), - ast::Param::with_default( - IdentWithToken::new("greeting"), - Some(ast_node(ast::Expr::Literal(ast::Literal::String("Hello".to_string())))) - ), - ]; - let fn_body = vec![ast_node(ast::Expr::Ident(IdentWithToken::new("greeting")))]; - - let program = vec![ - ast_node(ast::Expr::Def(IdentWithToken::new("greet"), params, fn_body)), - ast_call( - "greet", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::String("Alice".to_string()))), - ast_node(ast::Expr::Literal(ast::Literal::String("Hi".to_string()))) - ], - ), - ]; - - let result = Evaluator::new(DefaultModuleLoader::default(), token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("test".to_string()))].into_iter(), - ); - - assert_eq!(result, Ok(vec![RuntimeValue::String(Shared::new("Hi".to_string()))])); - } - - #[test] - fn test_default_params_with_default() { - // Test: def greet(name, greeting = "Hello"): greeting with greet("Alice") - let params = smallvec![ - ast::Param::new(IdentWithToken::new("name")), - ast::Param::with_default( - IdentWithToken::new("greeting"), - Some(ast_node(ast::Expr::Literal(ast::Literal::String("Hello".to_string())))) - ), - ]; - let fn_body = vec![ast_node(ast::Expr::Ident(IdentWithToken::new("greeting")))]; - - let program = vec![ - ast_node(ast::Expr::Def(IdentWithToken::new("greet"), params, fn_body)), - ast_call( - "greet", - smallvec![ast_node(ast::Expr::Literal(ast::Literal::String("Alice".to_string())))], - ), - ]; - - let result = Evaluator::new(DefaultModuleLoader::default(), token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("test".to_string()))].into_iter(), - ); - - assert_eq!(result, Ok(vec![RuntimeValue::String(Shared::new("Hello".to_string()))])); - } - - #[test] - fn test_default_params_with_self() { - // Test: def format(prefix = "[LOG]"): [prefix, self] with "message" | format() - let params = smallvec![ - ast::Param::new(IdentWithToken::new("self")), - ast::Param::with_default( - IdentWithToken::new("prefix"), - Some(ast_node(ast::Expr::Literal(ast::Literal::String("[LOG]".to_string())))) - ), - ]; - let fn_body = vec![ast_node(ast::Expr::Call( - IdentWithToken::new("array"), - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("prefix"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("self"))) - ], - ))]; - - let program = vec![ - ast_node(ast::Expr::Def(IdentWithToken::new("format"), params, fn_body)), - ast_call("format", smallvec![]), - ]; - - let result = Evaluator::new(DefaultModuleLoader::default(), token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("message".to_string()))].into_iter(), - ); - - assert_eq!( - result, - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("[LOG]".to_string())), - RuntimeValue::String(Shared::new("message".to_string())) - ]))]) - ); - } - - #[test] - fn test_multiple_default_params() { - // Test: def msg(a, b = 2, c = 3): [a, b, c] with msg(1) - let params = smallvec![ - ast::Param::new(IdentWithToken::new("a")), - ast::Param::with_default( - IdentWithToken::new("b"), - Some(ast_node(ast::Expr::Literal(ast::Literal::Number(2.into())))) - ), - ast::Param::with_default( - IdentWithToken::new("c"), - Some(ast_node(ast::Expr::Literal(ast::Literal::Number(3.into())))) - ), - ]; - let fn_body = vec![ast_node(ast::Expr::Call( - IdentWithToken::new("array"), - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("a"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("b"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("c"))) - ], - ))]; - - let program = vec![ - ast_node(ast::Expr::Def(IdentWithToken::new("msg"), params, fn_body)), - ast_call( - "msg", - smallvec![ast_node(ast::Expr::Literal(ast::Literal::Number(1.into())))], - ), - ]; - - let result = Evaluator::new(DefaultModuleLoader::default(), token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("test".to_string()))].into_iter(), - ); - - assert_eq!( - result, - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(2.into()), - RuntimeValue::Number(3.into()) - ]))]) - ); - } - - #[test] - fn test_multiple_default_params_partial() { - // Test: def msg(a, b = 2, c = 3): [a, b, c] with msg(1, 4) - let params = smallvec![ - ast::Param::new(IdentWithToken::new("a")), - ast::Param::with_default( - IdentWithToken::new("b"), - Some(ast_node(ast::Expr::Literal(ast::Literal::Number(2.into())))) - ), - ast::Param::with_default( - IdentWithToken::new("c"), - Some(ast_node(ast::Expr::Literal(ast::Literal::Number(3.into())))) - ), - ]; - let fn_body = vec![ast_node(ast::Expr::Call( - IdentWithToken::new("array"), - smallvec![ - ast_node(ast::Expr::Ident(IdentWithToken::new("a"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("b"))), - ast_node(ast::Expr::Ident(IdentWithToken::new("c"))) - ], - ))]; - - let program = vec![ - ast_node(ast::Expr::Def(IdentWithToken::new("msg"), params, fn_body)), - ast_call( - "msg", - smallvec![ - ast_node(ast::Expr::Literal(ast::Literal::Number(1.into()))), - ast_node(ast::Expr::Literal(ast::Literal::Number(4.into()))) - ], - ), - ]; - - let result = Evaluator::new(DefaultModuleLoader::default(), token_arena()).eval( - &program, - vec![RuntimeValue::String(Shared::new("test".to_string()))].into_iter(), - ); - - assert_eq!( - result, - Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::Number(4.into()), - RuntimeValue::Number(3.into()) - ]))]) - ); - } - - #[cfg(feature = "http-import")] - #[rstest] - #[case( - "include_http_in_module", - r#"include "https://example.com/dep.mq""#, - "https://example.com/dep.mq" - )] - #[case( - "include_github_in_module", - r#"include "github.com/alice/dep""#, - "github.com/alice/dep" - )] - fn test_include_http_inside_module_is_blocked( - #[case] module_name: &str, - #[case] module_content: &str, - #[case] blocked_url: &str, - ) { - let file_name = format!("{module_name}.mq"); - let (temp_dir, temp_file_path) = create_file(&file_name, module_content); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - let program = vec![Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Include(ast::Literal::String(module_name.to_string()))), - })]; - - assert!( - matches!( - Evaluator::new(loader, token_arena()) - .eval(&program, vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter()), - Err(InnerError::Runtime(RuntimeError::ModuleLoadError( - ModuleError::HttpImportNotAllowed(ref url) - ))) if url.as_ref() == blocked_url - ), - "expected HttpImportNotAllowed for '{blocked_url}'" - ); - } - - #[cfg(feature = "http-import")] - #[rstest] - #[case( - "import_http_in_module", - r#"import "https://example.com/dep.mq""#, - "https://example.com/dep.mq" - )] - #[case( - "import_github_in_module", - r#"import "github.com/alice/dep""#, - "github.com/alice/dep" - )] - fn test_import_http_inside_module_is_blocked( - #[case] module_name: &str, - #[case] module_content: &str, - #[case] blocked_url: &str, - ) { - let file_name = format!("{module_name}.mq"); - let (temp_dir, temp_file_path) = create_file(&file_name, module_content); - - defer! { - if temp_file_path.exists() { - std::fs::remove_file(&temp_file_path).expect("Failed to delete temp file"); - } - } - - let loader = ModuleLoader::new(DefaultModuleResolver::new(vec![temp_dir.clone()])); - let program = vec![Shared::new(ast::Node { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Include(ast::Literal::String(module_name.to_string()))), - })]; - - assert!( - matches!( - Evaluator::new(loader, token_arena()) - .eval(&program, vec![RuntimeValue::String(Shared::new("".to_string()))].into_iter()), - Err(InnerError::Runtime(RuntimeError::ModuleLoadError( - ModuleError::HttpImportNotAllowed(ref url) - ))) if url.as_ref() == blocked_url - ), - "expected HttpImportNotAllowed for '{blocked_url}'" - ); - } -} - -#[cfg(test)] -#[cfg(all(feature = "debugger", feature = "sync"))] -mod debugger_tests { - use rstest::{fixture, rstest}; - use smallvec::SmallVec; - - use super::*; - use crate::ast::node::Args; - use crate::runtime::debugger::{DebugContext, DebuggerHandler}; - use crate::{AstNode, DebuggerAction, IdentWithToken, ModuleLoader, Range, token_alloc}; - - #[fixture] - fn token_arena() -> Shared>>> { - let token_arena = Shared::new(SharedCell::new(Arena::new(10))); - - token_alloc( - &token_arena, - &Shared::new(Token { - kind: TokenKind::Eof, - range: Range::default(), - module_id: 1.into(), - }), - ); - - token_arena - } - - fn ast_call(name: &str, args: Args) -> Shared { - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Call(IdentWithToken::new(name), args)), - }) - } - - #[derive(Debug)] - struct TestDebuggerHandler { - breakpoints_hit: Shared>>, - steps_taken: Shared>>, - next_action: DebuggerAction, - } - - impl TestDebuggerHandler { - fn new(action: DebuggerAction) -> Self { - Self { - breakpoints_hit: Shared::new(SharedCell::new(Vec::new())), - steps_taken: Shared::new(SharedCell::new(Vec::new())), - next_action: action, - } - } - } - - impl DebuggerHandler for TestDebuggerHandler { - fn on_breakpoint_hit( - &self, - _breakpoint: &crate::runtime::debugger::Breakpoint, - context: &DebugContext, - ) -> DebuggerAction { - self.breakpoints_hit - .write() - .unwrap() - .push(format!("breakpoint:{}", context.current_value)); - self.next_action.clone() - } - - fn on_step(&self, context: &DebugContext) -> DebuggerAction { - self.steps_taken - .write() - .unwrap() - .push(format!("step:{}", context.current_value)); - self.next_action.clone() - } - } - - #[rstest] - fn test_eval_debugger_breakpoint_call(token_arena: Shared>>>) { - let handler = Shared::new(SharedCell::new( - Box::new(TestDebuggerHandler::new(DebuggerAction::Continue)) as Box, - )); - - let mut evaluator: Evaluator = Evaluator::new(ModuleLoader::default(), token_arena); - evaluator.debugger_handler = Shared::clone(&handler); - - let program = vec![ast_call(constants::builtins::BREAKPOINT, SmallVec::new())]; - let runtime_values = vec![RuntimeValue::String(Shared::new("test".to_string()))]; - - let result = evaluator.eval(&program, runtime_values.into_iter()); - - assert_eq!(result, Ok(vec![RuntimeValue::String(Shared::new("test".to_string()))])); - } - - #[derive(Debug, Default)] - struct RecordingDebuggerHandler { - breakpoint_hits: Shared>>, - log_points: Shared>>, - errors: Shared>>, - } - - impl DebuggerHandler for RecordingDebuggerHandler { - fn on_breakpoint_hit( - &self, - _breakpoint: &crate::runtime::debugger::Breakpoint, - context: &DebugContext, - ) -> DebuggerAction { - self.breakpoint_hits - .write() - .unwrap() - .push(context.current_value.to_string()); - DebuggerAction::Continue - } - - fn on_log_point( - &self, - _breakpoint: &crate::runtime::debugger::Breakpoint, - message: &str, - _context: &DebugContext, - ) { - self.log_points.write().unwrap().push(message.to_string()); - } - - fn on_error(&self, message: &str, _context: &DebugContext) { - self.errors.write().unwrap().push(message.to_string()); - } - } - - #[test] - fn test_conditional_breakpoint_stops_only_when_condition_is_true() { - let mut engine = crate::DefaultEngine::default(); - let breakpoint_hits = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - breakpoint_hits: Shared::clone(&breakpoint_hits), - log_points: Shared::default(), - errors: Shared::default(), - })); - engine.debugger().write().unwrap().activate(); - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - Some("x == 3".to_string()), - None, - None, - ); - - let query = "foreach (x, array(1, 2, 3, 4)):\n x\nend"; - engine.eval(query, crate::null_input().into_iter()).unwrap(); - - assert_eq!(*breakpoint_hits.read().unwrap(), vec!["3".to_string()]); - } - - #[test] - fn test_hit_condition_breakpoint_ignores_earlier_hits() { - let mut engine = crate::DefaultEngine::default(); - let breakpoint_hits = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - breakpoint_hits: Shared::clone(&breakpoint_hits), - log_points: Shared::default(), - errors: Shared::default(), - })); - engine.debugger().write().unwrap().activate(); - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - None, - Some("hit_count >= 3".to_string()), - None, - ); - - let query = "foreach (x, array(1, 2, 3, 4)):\n x\nend"; - engine.eval(query, crate::null_input().into_iter()).unwrap(); - - assert_eq!(*breakpoint_hits.read().unwrap(), vec!["3".to_string(), "4".to_string()]); - } - - #[test] - fn test_hit_condition_breakpoint_bare_number_is_shorthand_for_gte() { - let mut engine = crate::DefaultEngine::default(); - let breakpoint_hits = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - breakpoint_hits: Shared::clone(&breakpoint_hits), - log_points: Shared::default(), - errors: Shared::default(), - })); - engine.debugger().write().unwrap().activate(); - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - None, - Some("3".to_string()), - None, - ); - - let query = "foreach (x, array(1, 2, 3, 4)):\n x\nend"; - engine.eval(query, crate::null_input().into_iter()).unwrap(); - - assert_eq!(*breakpoint_hits.read().unwrap(), vec!["3".to_string(), "4".to_string()]); - } - - #[test] - fn test_hit_condition_breakpoint_can_reference_in_scope_variables() { - let mut engine = crate::DefaultEngine::default(); - let breakpoint_hits = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - breakpoint_hits: Shared::clone(&breakpoint_hits), - log_points: Shared::default(), - errors: Shared::default(), - })); - engine.debugger().write().unwrap().activate(); - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - None, - Some("hit_count >= 2 && x == 4".to_string()), - None, - ); - - let query = "foreach (x, array(1, 2, 3, 4)):\n x\nend"; - engine.eval(query, crate::null_input().into_iter()).unwrap(); - - assert_eq!(*breakpoint_hits.read().unwrap(), vec!["4".to_string()]); - } - - #[test] - fn test_logpoint_never_stops_and_interpolates_message() { - let mut engine = crate::DefaultEngine::default(); - let breakpoint_hits = Shared::new(SharedCell::new(Vec::new())); - let log_points = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - breakpoint_hits: Shared::clone(&breakpoint_hits), - log_points: Shared::clone(&log_points), - errors: Shared::default(), - })); - engine.debugger().write().unwrap().activate(); - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - None, - None, - Some("x is ${x}, self is ${self}".to_string()), - ); - - let query = "foreach (x, array(1, 2, 3)):\n x\nend"; - engine.eval(query, crate::null_input().into_iter()).unwrap(); - - assert!(breakpoint_hits.read().unwrap().is_empty()); - assert_eq!( - *log_points.read().unwrap(), - vec![ - "x is 1, self is 1".to_string(), - "x is 2, self is 2".to_string(), - "x is 3, self is 3".to_string() - ] - ); - } - - #[test] - fn test_logpoint_message_supports_literal_braces_and_env_vars() { - let mut engine = crate::DefaultEngine::default(); - engine.set_io(Shared::new(SandboxedIo::new(NativeIo::default()).allow_env(true))); - let log_points = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - breakpoint_hits: Shared::default(), - log_points: Shared::clone(&log_points), - errors: Shared::default(), - })); - engine.debugger().write().unwrap().activate(); - - // SAFETY: no other threads read/write this env var concurrently in this test. - unsafe { std::env::set_var("MQ_TEST_LOGPOINT_VAR", "env-value") }; - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - None, - None, - Some(r"literal \{x\} and ${$MQ_TEST_LOGPOINT_VAR}".to_string()), - ); - - let query = "foreach (x, array(1)):\n x\nend"; - engine.eval(query, crate::null_input().into_iter()).unwrap(); - // SAFETY: matches the set_var call above. - unsafe { std::env::remove_var("MQ_TEST_LOGPOINT_VAR") }; - - assert_eq!( - *log_points.read().unwrap(), - vec!["literal {x} and env-value".to_string()] - ); - } - - #[test] - fn test_invalid_breakpoint_condition_returns_error() { - let mut engine = crate::DefaultEngine::default(); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler::default())); - engine.debugger().write().unwrap().activate(); - engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - Some("(".to_string()), - None, - None, - ); - - let query = "foreach (x, array(1)):\n x\nend"; - assert!(engine.eval(query, crate::null_input().into_iter()).is_err()); - } - - #[test] - fn test_uncaught_error_notifies_debugger_handler() { - let mut engine = crate::DefaultEngine::default(); - let errors = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - errors: Shared::clone(&errors), - ..Default::default() - })); - engine.debugger().write().unwrap().activate(); - - let query = r#"error("boom")"#; - let result = engine.eval(query, crate::null_input().into_iter()); - - assert!(result.is_err()); - assert_eq!(errors.read().unwrap().len(), 1); - assert!(errors.read().unwrap()[0].contains("boom")); - } - - #[test] - fn test_error_caught_by_try_catch_does_not_notify_debugger_handler() { - let mut engine = crate::DefaultEngine::default(); - let errors = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - errors: Shared::clone(&errors), - ..Default::default() - })); - engine.debugger().write().unwrap().activate(); - - let query = r#"try: error("boom") catch: "caught""#; - let result = engine.eval(query, crate::null_input().into_iter()); - - assert_eq!( - result.unwrap(), - vec![RuntimeValue::String(Shared::new("caught".to_string()))].into() - ); - assert!(errors.read().unwrap().is_empty()); - } - - #[test] - fn test_uncaught_error_does_not_notify_when_debugger_inactive() { - let mut engine = crate::DefaultEngine::default(); - let errors = Shared::new(SharedCell::new(Vec::new())); - engine.set_debugger_handler(Box::new(RecordingDebuggerHandler { - errors: Shared::clone(&errors), - ..Default::default() - })); - // Debugger left inactive (default state). - - let query = r#"error("boom")"#; - let result = engine.eval(query, crate::null_input().into_iter()); - - assert!(result.is_err()); - assert!(errors.read().unwrap().is_empty()); - } -} diff --git a/crates/mq-lang/src/lib.rs b/crates/mq-lang/src/lib.rs index 36993234a..9e9b56b8b 100644 --- a/crates/mq-lang/src/lib.rs +++ b/crates/mq-lang/src/lib.rs @@ -46,14 +46,6 @@ mod cst; pub mod diagnostic; mod engine; mod error; -#[cfg(not(feature = "tarn"))] -mod eval; -// Keeps shared test helpers' limits path available without compiling the tree-walker. -#[cfg(feature = "tarn")] -mod eval { - #[allow(unused_imports)] - pub(crate) use crate::tarn::Options; -} mod ident; mod io; mod lexer; @@ -64,8 +56,9 @@ mod range; mod runtime; mod selector; pub mod suggest; -#[cfg(feature = "tarn")] mod tarn; +#[cfg(feature = "vm-profile")] +pub mod vm_profile; use lexer::Lexer; #[cfg(not(feature = "sync"))] @@ -117,7 +110,7 @@ pub use runtime::builtin::{ INTERNAL_FUNCTION_DOC, }; pub use runtime::host::{HostFnResult, HostFunction, HostFunctionError, HostFunctions, IntoHostFunction, ValueAdapter}; -pub use runtime::runtime_value::{RuntimeValue, RuntimeValues}; +pub use runtime::runtime_value::{DictMap, RuntimeValue, RuntimeValues}; pub use selector::{AttrKind, Selector}; pub type DefaultEngine = Engine; @@ -194,13 +187,9 @@ pub fn parse(code: &str, token_arena: TokenArena) -> Result>().iter(), - &mut token_arena, - Module::TOP_LEVEL_MODULE_ID, - ) - .parse() - .map_err(|e| Box::new(error::Error::from_error(code, e.into(), DefaultModuleLoader::default()))) + AstParser::new(tokens.iter(), &mut token_arena, Module::TOP_LEVEL_MODULE_ID) + .parse() + .map_err(|e| Box::new(error::Error::from_error(code, e.into(), DefaultModuleLoader::default()))) } /// Parses an MDX string and returns an iterator over `Value` nodes. diff --git a/crates/mq-lang/src/module.rs b/crates/mq-lang/src/module.rs index 962245766..a17554913 100644 --- a/crates/mq-lang/src/module.rs +++ b/crates/mq-lang/src/module.rs @@ -68,7 +68,6 @@ pub struct ModuleLoader { builtin_module_cache: Option<(TokenArena, Module)>, /// Parsed `Module`s, keyed by canonical name, so `reload_cached` can reuse an AST already /// parsed by this loader instead of reparsing its cached source. - #[cfg(feature = "tarn")] module_ast_cache: FxHashMap, resolver: T, /// Tracks sub-module loading depth; HTTP imports are blocked when this is greater than zero. @@ -135,7 +134,6 @@ impl ModuleLoader { source_code: None, source_cache: FxHashMap::default(), builtin_module_cache: None, - #[cfg(feature = "tarn")] module_ast_cache: FxHashMap::default(), resolver, #[cfg(feature = "http-import")] @@ -143,7 +141,6 @@ impl ModuleLoader { } } - #[cfg(feature = "tarn")] pub(crate) fn with_same_resolver(&self) -> Self { let mut loader = Self::new(self.resolver.clone()); loader.builtin_module_cache = self.builtin_module_cache.clone(); @@ -197,7 +194,6 @@ impl ModuleLoader { let module = Self::classify_module(module_name, program)?; self.loaded_modules.alloc(module_name.into()); - #[cfg(feature = "tarn")] self.module_ast_cache.insert(SmolStr::new(module_name), module.clone()); Ok(module) } @@ -207,7 +203,7 @@ impl ModuleLoader { .iter() .filter(|node| { matches!( - *node.expr, + &node.expr, ast::Expr::Include(_) | ast::Expr::Module(_, _) | ast::Expr::Import(_, _) ) }) @@ -216,13 +212,13 @@ impl ModuleLoader { let functions = program .iter() - .filter(|node| matches!(*node.expr, ast::Expr::Def(..))) + .filter(|node| matches!(&node.expr, ast::Expr::Def(..))) .cloned() .collect::>(); let vars = program .iter() - .filter(|node| matches!(*node.expr, ast::Expr::Let(..))) + .filter(|node| matches!(&node.expr, ast::Expr::Let(..))) .cloned() .collect::>(); @@ -240,7 +236,6 @@ impl ModuleLoader { }) } - #[cfg(feature = "tarn")] pub(crate) fn reload_cached(&mut self, module_path: &str, token_arena: TokenArena) -> Result { let name = self.resolver.canonical_name(module_path).to_owned(); // Already parsed by this same loader (e.g. a prelude pre-pass ran ahead of the real @@ -417,12 +412,7 @@ impl ModuleLoader { } }; - let program = Parser::new( - tokens.into_iter().map(Shared::new).collect::>().iter(), - &mut token_arena, - module_id, - ) - .parse()?; + let program = Parser::new(tokens.iter(), &mut token_arena, module_id).parse()?; Ok(program) } @@ -513,20 +503,20 @@ mod tests { functions: Vec::new(), modules: Vec::new(), vars: vec![ - Shared::new(ast::Node{token_id: 0.into(), expr: Shared::new(ast::Expr::Let( + Shared::new(ast::Node{token_id: 0.into(), expr: ast::Expr::Let( ast::Pattern::Ident(IdentWithToken::new_with_token("test", Some(Shared::new(Token{ kind: TokenKind::Ident(SmolStr::new("test")), range: Range{start: Position{line: 1, column: 5}, end: Position{line: 1, column: 9}}, module_id: 1.into() })))), - Shared::new(ast::Node{token_id: 2.into(), expr: Shared::new(ast::Expr::Literal(ast::Literal::String("value".to_string())))}) - ))})], + Shared::new(ast::Node{token_id: 2.into(), expr: ast::Expr::Literal(ast::Literal::String("value".to_string()))}) + )})], }))] #[case::load3("def test(): 1;".to_string(), Ok(Module{ name: "test".to_string(), modules: Vec::new(), functions: vec![ - Shared::new(ast::Node{token_id: 0.into(), expr: Shared::new(ast::Expr::Def( + Shared::new(ast::Node{token_id: 0.into(), expr: ast::Expr::Def( IdentWithToken::new_with_token("test", Some(Shared::new(Token{ kind: TokenKind::Ident(SmolStr::new("test")), range: Range{start: Position{line: 1, column: 5}, end: Position{line: 1, column: 9}}, @@ -534,36 +524,36 @@ mod tests { }))), SmallVec::new(), vec![ - Shared::new(ast::Node{token_id: 2.into(), expr: Shared::new(ast::Expr::Literal(ast::Literal::Number(1.into())))}) + Shared::new(ast::Node{token_id: 2.into(), expr: ast::Expr::Literal(ast::Literal::Number(1.into()))}) ] - ))})], + )})], vars: Vec::new(), }))] #[case::load4("def test(a, b): add(a, b);".to_string(), Ok(Module{ name: "test".to_string(), modules: Vec::new(), functions: vec![ - Shared::new(ast::Node{token_id: 0.into(), expr: Shared::new(ast::Expr::Def( + Shared::new(ast::Node{token_id: 0.into(), expr: ast::Expr::Def( IdentWithToken::new_with_token("test", Some(Shared::new(Token{kind: TokenKind::Ident(SmolStr::new("test")), range: Range{start: Position{line: 1, column: 5}, end: Position{line: 1, column: 9}}, module_id: 1.into()}))), smallvec![ Param::new(IdentWithToken::new_with_token("a", Some(Shared::new(Token{kind: TokenKind::Ident(SmolStr::new("a")), range: Range{start: Position{line: 1, column: 10}, end: Position{line: 1, column: 11}}, module_id: 1.into()})))), Param::new(IdentWithToken::new_with_token("b", Some(Shared::new(Token{kind: TokenKind::Ident(SmolStr::new("b")), range: Range{start: Position{line: 1, column: 13}, end: Position{line: 1, column: 14}}, module_id: 1.into()})))), ], vec![ - Shared::new(ast::Node{token_id: 4.into(), expr: Shared::new(ast::Expr::Call( + Shared::new(ast::Node{token_id: 4.into(), expr: ast::Expr::Call( IdentWithToken::new_with_token("add", Some(Shared::new(Token{kind: TokenKind::Ident(SmolStr::new("add")), range: Range{start: Position{line: 1, column: 17}, end: Position{line: 1, column: 20}}, module_id: 1.into()}))), smallvec![ Shared::new(ast::Node{token_id: 2.into(), - expr: Shared::new( + expr: ast::Expr::Ident(IdentWithToken::new_with_token("a", Some(Shared::new(Token{kind: TokenKind::Ident(SmolStr::new("a")), range: Range{start: Position{line: 1, column: 21}, end: Position{line: 1, column: 22}}, module_id: 1.into()})))) - )}), + }), Shared::new(ast::Node{token_id: 3.into(), - expr: Shared::new( + expr: ast::Expr::Ident(IdentWithToken::new_with_token("b", Some(Shared::new(Token{kind: TokenKind::Ident(SmolStr::new("b")), range: Range{start: Position{line: 1, column: 24}, end: Position{line: 1, column: 25}}, module_id: 1.into()})))) - )}) + }) ], - ))})] - ))})], + )})] + )})], vars: Vec::new(), }))] fn test_load( @@ -629,7 +619,6 @@ mod tests { )); } - #[cfg(feature = "tarn")] #[test] fn test_load_builtin_reuses_ast_for_the_same_token_arena() { let token_arena = token_arena(); diff --git a/crates/mq-lang/src/optimizer.rs b/crates/mq-lang/src/optimizer.rs index 039a838fd..52133c919 100644 --- a/crates/mq-lang/src/optimizer.rs +++ b/crates/mq-lang/src/optimizer.rs @@ -109,12 +109,12 @@ impl Optimizer { // Merge parent user_defs with any local Defs. When there are no local Defs (the // common case for loop bodies and blocks), skip the allocation entirely. let merged; - let user_defs: &FxHashSet = if program.iter().any(|n| matches!(&*n.expr, ast::Expr::Def(..))) { + let user_defs: &FxHashSet = if program.iter().any(|n| matches!(&n.expr, ast::Expr::Def(..))) { merged = parent_user_defs .iter() .copied() .chain(program.iter().filter_map(|n| { - if let ast::Expr::Def(ident, ..) = &*n.expr { + if let ast::Expr::Def(ident, ..) = &n.expr { Some(ident.name) } else { None @@ -137,11 +137,11 @@ impl Optimizer { // empty set — no heap allocation. static EMPTY_DEFS: OnceLock> = OnceLock::new(); let user_defs_owned: FxHashSet; - let user_defs: &FxHashSet = if program.iter().any(|n| matches!(&*n.expr, ast::Expr::Def(..))) { + let user_defs: &FxHashSet = if program.iter().any(|n| matches!(&n.expr, ast::Expr::Def(..))) { user_defs_owned = program .iter() .filter_map(|n| { - if let ast::Expr::Def(ident, ..) = &*n.expr { + if let ast::Expr::Def(ident, ..) = &n.expr { Some(ident.name) } else { None @@ -168,7 +168,7 @@ impl Optimizer { let program = self.merge_selector_chains(program); // Passes 2-4 are only worthwhile when Def nodes are present. - if !program.iter().any(|n| matches!(&*n.expr, ast::Expr::Def(..))) { + if !program.iter().any(|n| matches!(&n.expr, ast::Expr::Def(..))) { return program; } @@ -199,9 +199,9 @@ impl Optimizer { /// map if the result is a literal. /// - All other nodes: substitute known literals, then fold constants. fn propagate_and_fold(&self, program: Program, user_defs: &FxHashSet) -> Program { - let has_let_literal = program.iter().any(|n| { - matches!(&*n.expr, ast::Expr::Let(Pattern::Ident(_), rhs) if matches!(&*rhs.expr, ast::Expr::Literal(_))) - }); + let has_let_literal = program.iter().any( + |n| matches!(&n.expr, ast::Expr::Let(Pattern::Ident(_), rhs) if matches!(&rhs.expr, ast::Expr::Literal(_))), + ); if !has_let_literal { return lazy_map_program(program, |n| self.optimize_node(Shared::clone(n), user_defs)); @@ -212,10 +212,10 @@ impl Optimizer { for node in program { let token_id = node.token_id; - match &*node.expr { + match &node.expr { ast::Expr::Let(Pattern::Ident(ident), rhs) => { let opt_rhs = self.optimize_node(Shared::clone(rhs), user_defs); - if let ast::Expr::Literal(lit) = &*opt_rhs.expr { + if let ast::Expr::Literal(lit) = &opt_rhs.expr { env_insert(&mut env, ident.name, lit.clone()); } else { env_remove(&mut env, ident.name); @@ -226,7 +226,7 @@ impl Optimizer { } else { result.push(Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Let(Pattern::Ident(ident.clone()), opt_rhs)), + expr: ast::Expr::Let(Pattern::Ident(ident.clone()), opt_rhs), })); } } @@ -248,7 +248,7 @@ impl Optimizer { // Fast path: skip allocation when no consecutive Selector nodes exist. let has_consecutive = program .windows(2) - .any(|w| matches!(&*w[0].expr, ast::Expr::Selector(_)) && matches!(&*w[1].expr, ast::Expr::Selector(_))); + .any(|w| matches!(&w[0].expr, ast::Expr::Selector(_)) && matches!(&w[1].expr, ast::Expr::Selector(_))); if !has_consecutive { return program; } @@ -257,13 +257,13 @@ impl Optimizer { let mut iter = program.into_iter().peekable(); while let Some(node) = iter.next() { - if let ast::Expr::Selector(sel) = &*node.expr { + if let ast::Expr::Selector(sel) = &node.expr { let token_id = node.token_id; let mut chain: SmallVec<[Selector; 4]> = SmallVec::new(); chain.push(sel.clone()); while let Some(next) = iter.peek() { - if let ast::Expr::Selector(next_sel) = &*next.expr { + if let ast::Expr::Selector(next_sel) = &next.expr { chain.push(next_sel.clone()); iter.next(); } else { @@ -276,7 +276,7 @@ impl Optimizer { } else { result.push(Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::SelectorChain(chain)), + expr: ast::Expr::SelectorChain(chain), })); } } else { @@ -294,12 +294,12 @@ impl Optimizer { } let token_id = node.token_id; - match &*node.expr { + match &node.expr { ast::Expr::Ident(ident) => { if let Some(lit) = env_get(env, ident.name) { return Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(lit.clone())), + expr: ast::Expr::Literal(lit.clone()), }); } node @@ -311,7 +311,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Call(ident.clone(), subst_args)), + expr: ast::Expr::Call(ident.clone(), subst_args), }) } ast::Expr::CallDynamic(callable, args) => { @@ -322,7 +322,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::CallDynamic(subst_callable, subst_args)), + expr: ast::Expr::CallDynamic(subst_callable, subst_args), }) } ast::Expr::SelectorCall(selector, args) => { @@ -332,7 +332,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::SelectorCall(selector.clone(), subst_args)), + expr: ast::Expr::SelectorCall(selector.clone(), subst_args), }) } ast::Expr::If(branches) => { @@ -347,7 +347,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::If(subst_branches)), + expr: ast::Expr::If(subst_branches), }) } ast::Expr::Unless(branches) => { @@ -362,7 +362,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Unless(subst_branches)), + expr: ast::Expr::Unless(subst_branches), }) } ast::Expr::And(operands) => { @@ -372,7 +372,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::And(subst)), + expr: ast::Expr::And(subst), }) } ast::Expr::Or(operands) => { @@ -382,27 +382,25 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Or(subst)), + expr: ast::Expr::Or(subst), }) } ast::Expr::Paren(inner) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Paren(self.substitute_literals(Shared::clone(inner), env))), + expr: ast::Expr::Paren(self.substitute_literals(Shared::clone(inner), env)), }), // No error binder: neither branch introduces a new binding, so substitution is safe. ast::Expr::Try(try_expr, None, catch_expr) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Try( + expr: ast::Expr::Try( self.substitute_literals(Shared::clone(try_expr), env), None, self.substitute_literals(Shared::clone(catch_expr), env), - )), + ), }), ast::Expr::Break(Some(val)) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Break(Some( - self.substitute_literals(Shared::clone(val), env), - ))), + expr: ast::Expr::Break(Some(self.substitute_literals(Shared::clone(val), env))), }), // Substitute into Expr segments of interpolated strings so that // `let x = "hi" | s"${x}!"` can later be folded to `"hi!"`. @@ -418,7 +416,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::InterpolatedString(subst_segs)), + expr: ast::Expr::InterpolatedString(subst_segs), }) } // Scope-creating or leaf nodes: stop substitution here. @@ -455,7 +453,7 @@ impl Optimizer { } let token_id = node.token_id; - match &*node.expr { + match &node.expr { ast::Expr::Call(ident, args) => { let opt_args: Args = args.iter().map(|a| self.apply_inline(Shared::clone(a), fns)).collect(); @@ -465,7 +463,7 @@ impl Optimizer { Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Call(ident.clone(), opt_args)), + expr: ast::Expr::Call(ident.clone(), opt_args), }) } // Recurse into sub-expressions — but not across scope-creating nodes (Def, Fn, Block). @@ -481,34 +479,30 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::If(branches)), + expr: ast::Expr::If(branches), }) } ast::Expr::And(ops) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::And( - ops.iter().map(|o| self.apply_inline(Shared::clone(o), fns)).collect(), - )), + expr: ast::Expr::And(ops.iter().map(|o| self.apply_inline(Shared::clone(o), fns)).collect()), }), ast::Expr::Or(ops) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Or( - ops.iter().map(|o| self.apply_inline(Shared::clone(o), fns)).collect(), - )), + expr: ast::Expr::Or(ops.iter().map(|o| self.apply_inline(Shared::clone(o), fns)).collect()), }), ast::Expr::Try(try_expr, None, catch_expr) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Try( + expr: ast::Expr::Try( self.apply_inline(Shared::clone(try_expr), fns), None, self.apply_inline(Shared::clone(catch_expr), fns), - )), + ), }), ast::Expr::SelectorCall(sel, args) => { let opt_args: Args = args.iter().map(|a| self.apply_inline(Shared::clone(a), fns)).collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::SelectorCall(sel.clone(), opt_args)), + expr: ast::Expr::SelectorCall(sel.clone(), opt_args), }) } // Scope-creating and leaf nodes are left unchanged. @@ -519,7 +513,7 @@ impl Optimizer { fn optimize_node(&self, node: Shared, user_defs: &FxHashSet) -> Shared { let token_id = node.token_id; - match &*node.expr { + match &node.expr { ast::Expr::Paren(inner) => self.optimize_node(Shared::clone(inner), user_defs), ast::Expr::Call(ident, args) => { let opt_args: Args = args @@ -535,7 +529,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Call(ident.clone(), opt_args)), + expr: ast::Expr::Call(ident.clone(), opt_args), }) } ast::Expr::If(branches) => self.optimize_if(token_id, branches, user_defs), @@ -548,23 +542,23 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Block(opt)), + expr: ast::Expr::Block(opt), }) } ast::Expr::Def(ident, params, program) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Def( + expr: ast::Expr::Def( ident.clone(), self.optimize_params(params, user_defs), self.optimize_nested(program.clone(), user_defs), - )), + ), }), ast::Expr::Fn(params, program) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Fn( + expr: ast::Expr::Fn( self.optimize_params(params, user_defs), self.optimize_nested(program.clone(), user_defs), - )), + ), }), ast::Expr::While(cond, program) => { let opt_cond = self.optimize_node(Shared::clone(cond), user_defs); @@ -574,7 +568,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::While(opt_cond, opt_body)), + expr: ast::Expr::While(opt_cond, opt_body), }) } ast::Expr::Loop(program) => { @@ -584,7 +578,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Loop(opt)), + expr: ast::Expr::Loop(opt), }) } ast::Expr::Until(cond, program) => { @@ -595,7 +589,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Until(opt_cond, opt_body)), + expr: ast::Expr::Until(opt_cond, opt_body), }) } ast::Expr::Unless(branches) => { @@ -610,7 +604,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Unless(opt_branches)), + expr: ast::Expr::Unless(opt_branches), }) } ast::Expr::Foreach(ident, values, program) => { @@ -621,7 +615,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Foreach(ident.clone(), opt_values, opt_body)), + expr: ast::Expr::Foreach(ident.clone(), opt_values, opt_body), }) } ast::Expr::As(ident, inner) => { @@ -631,7 +625,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::As(ident.clone(), opt_inner)), + expr: ast::Expr::As(ident.clone(), opt_inner), }) } ast::Expr::Let(pattern, inner) => { @@ -641,7 +635,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Let(pattern.clone(), opt_inner)), + expr: ast::Expr::Let(pattern.clone(), opt_inner), }) } ast::Expr::Var(pattern, inner) => { @@ -651,7 +645,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Var(pattern.clone(), opt_inner)), + expr: ast::Expr::Var(pattern.clone(), opt_inner), }) } ast::Expr::Assign(ident, inner) => { @@ -661,7 +655,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Assign(ident.clone(), opt_inner)), + expr: ast::Expr::Assign(ident.clone(), opt_inner), }) } ast::Expr::Try(try_expr, error_binder, catch_expr) => { @@ -672,7 +666,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Try(opt_try, error_binder.clone(), opt_catch)), + expr: ast::Expr::Try(opt_try, error_binder.clone(), opt_catch), }) } ast::Expr::Break(Some(val)) => { @@ -682,7 +676,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Break(Some(opt_val))), + expr: ast::Expr::Break(Some(opt_val)), }) } ast::Expr::Match(value_node, arms) => { @@ -700,7 +694,7 @@ impl Optimizer { .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Match(opt_value, opt_arms)), + expr: ast::Expr::Match(opt_value, opt_arms), }) } ast::Expr::CallDynamic(callable, args) => { @@ -716,7 +710,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::CallDynamic(opt_callable, opt_args)), + expr: ast::Expr::CallDynamic(opt_callable, opt_args), }) } ast::Expr::SelectorCall(selector, args) => { @@ -729,7 +723,7 @@ impl Optimizer { } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::SelectorCall(selector.clone(), opt_args)), + expr: ast::Expr::SelectorCall(selector.clone(), opt_args), }) } ast::Expr::InterpolatedString(segments) => { @@ -740,7 +734,7 @@ impl Optimizer { .map(|seg| match seg { StringSegment::Expr(n) => { let opt = self.optimize_node(Shared::clone(n), user_defs); - if let ast::Expr::Literal(Literal::String(s)) = &*opt.expr { + if let ast::Expr::Literal(Literal::String(s)) = &opt.expr { StringSegment::Text(s.clone()) } else { StringSegment::Expr(opt) @@ -759,21 +753,18 @@ impl Optimizer { }); return Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(Literal::String(folded))), + expr: ast::Expr::Literal(Literal::String(folded)), }); } Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::InterpolatedString(opt_segs)), + expr: ast::Expr::InterpolatedString(opt_segs), }) } ast::Expr::Module(ident, program) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Module( - ident.clone(), - self.optimize_nested(program.clone(), user_defs), - )), + expr: ast::Expr::Module(ident.clone(), self.optimize_nested(program.clone(), user_defs)), }), ast::Expr::Literal(_) | ast::Expr::Ident(_) @@ -806,7 +797,7 @@ impl Optimizer { let make_lit = |lit: Literal| { Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(lit)), + expr: ast::Expr::Literal(lit), }) }; @@ -1029,7 +1020,7 @@ impl Optimizer { } Some(cond) => { let opt_cond = self.optimize_node(Shared::clone(cond), user_defs); - match &*opt_cond.expr { + match &opt_cond.expr { ast::Expr::Literal(Literal::Bool(true)) => { remaining.push((None, opt_body)); break; @@ -1048,12 +1039,12 @@ impl Optimizer { match remaining.len() { 0 => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(Literal::None)), + expr: ast::Expr::Literal(Literal::None), }), 1 if remaining[0].0.is_none() => Shared::clone(&remaining[0].1), _ => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::If(remaining)), + expr: ast::Expr::If(remaining), }), } } @@ -1068,11 +1059,11 @@ impl Optimizer { for op in operands { let opt = self.optimize_node(Shared::clone(op), user_defs); - match &*opt.expr { + match &opt.expr { ast::Expr::Literal(lit) if !literal_is_truthy(lit) => { return Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(Literal::Bool(false))), + expr: ast::Expr::Literal(Literal::Bool(false)), }); } ast::Expr::Literal(lit) if literal_is_truthy(lit) => continue, @@ -1083,11 +1074,11 @@ impl Optimizer { match remaining.len() { 0 => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(Literal::Bool(true))), + expr: ast::Expr::Literal(Literal::Bool(true)), }), _ => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::And(remaining)), + expr: ast::Expr::And(remaining), }), } } @@ -1102,7 +1093,7 @@ impl Optimizer { for op in operands { let opt = self.optimize_node(Shared::clone(op), user_defs); - match &*opt.expr { + match &opt.expr { ast::Expr::Literal(lit) if literal_is_truthy(lit) => return opt, ast::Expr::Literal(lit) if !literal_is_truthy(lit) => continue, _ => remaining.push(opt), @@ -1112,11 +1103,11 @@ impl Optimizer { match remaining.len() { 0 => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(Literal::Bool(false))), + expr: ast::Expr::Literal(Literal::Bool(false)), }), _ => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Or(remaining)), + expr: ast::Expr::Or(remaining), }), } } @@ -1151,7 +1142,7 @@ struct InlinableFn { fn collect_inlinable(program: &Program) -> FxHashMap { let mut map = FxHashMap::default(); for node in program { - let ast::Expr::Def(ident, params, body) = &*node.expr else { + let ast::Expr::Def(ident, params, body) = &node.expr else { continue; }; if body.len() != 1 { @@ -1178,7 +1169,7 @@ fn collect_inlinable(program: &Program) -> FxHashMap { /// Returns `true` if `node` contains a direct or indirect call to `fn_name`. fn has_recursion(node: &Shared, fn_name: Ident) -> bool { - match &*node.expr { + match &node.expr { ast::Expr::Call(ident, args) => ident.name == fn_name || args.iter().any(|a| has_recursion(a, fn_name)), ast::Expr::Ident(ident) => ident.name == fn_name, ast::Expr::And(ops) | ast::Expr::Or(ops) => ops.iter().any(|o| has_recursion(o, fn_name)), @@ -1199,7 +1190,7 @@ fn has_recursion(node: &Shared, fn_name: Ident) -> bool { /// Conservative: complex sub-expressions (blocks, lambdas, loops, let/var) cause /// the function to return `true` immediately so that inlining is skipped. fn has_free_vars(node: &Shared, params: &[Ident]) -> bool { - match &*node.expr { + match &node.expr { ast::Expr::Ident(ident) => !params.contains(&ident.name), ast::Expr::Literal(_) | ast::Expr::Self_ | ast::Expr::Selector(_) | ast::Expr::SelectorChain(_) => false, ast::Expr::Call(callee, args) => { @@ -1229,7 +1220,7 @@ fn substitute_params( call_token_id: TokenId, ) -> Shared { let token_id = call_token_id; - match &*node.expr { + match &node.expr { ast::Expr::Ident(ident) => { if let Some(pos) = params.iter().position(|p| *p == ident.name) { return Shared::clone(&args[pos]); @@ -1243,7 +1234,7 @@ fn substitute_params( .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Call(ident.clone(), subst)), + expr: ast::Expr::Call(ident.clone(), subst), }) } ast::Expr::SelectorCall(sel, call_args) => { @@ -1253,24 +1244,24 @@ fn substitute_params( .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::SelectorCall(sel.clone(), subst)), + expr: ast::Expr::SelectorCall(sel.clone(), subst), }) } ast::Expr::And(ops) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::And( + expr: ast::Expr::And( ops.iter() .map(|o| substitute_params(Shared::clone(o), params, args, call_token_id)) .collect(), - )), + ), }), ast::Expr::Or(ops) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Or( + expr: ast::Expr::Or( ops.iter() .map(|o| substitute_params(Shared::clone(o), params, args, call_token_id)) .collect(), - )), + ), }), ast::Expr::If(branches) => { let branches: ast::Branches = branches @@ -1285,17 +1276,17 @@ fn substitute_params( .collect(); Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::If(branches)), + expr: ast::Expr::If(branches), }) } // `has_free_vars` already excludes error-binder bodies from inlining. ast::Expr::Try(t, error_binder, c) => Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Try( + expr: ast::Expr::Try( substitute_params(Shared::clone(t), params, args, call_token_id), error_binder.clone(), substitute_params(Shared::clone(c), params, args, call_token_id), - )), + ), }), ast::Expr::Paren(inner) => substitute_params(Shared::clone(inner), params, args, call_token_id), _ => node, @@ -1303,7 +1294,7 @@ fn substitute_params( } fn literal_of(node: &Shared) -> Option { - match &*node.expr { + match &node.expr { ast::Expr::Literal(lit) => Some(lit.clone()), _ => None, } @@ -1326,14 +1317,14 @@ fn apply_tco_transforms(program: Program) -> Program { program .into_iter() .map(|node| { - let ast::Expr::Def(ident, params, body) = &*node.expr else { + let ast::Expr::Def(ident, params, body) = &node.expr else { return node; }; let param_names: Vec = params.iter().map(|p| p.ident.name).collect(); match try_tco_transform(ident.name, ¶m_names, body, node.token_id) { Some(new_body) => Shared::new(ast::Node { token_id: node.token_id, - expr: Shared::new(ast::Expr::Def(ident.clone(), params.clone(), new_body)), + expr: ast::Expr::Def(ident.clone(), params.clone(), new_body), }), None => node, } @@ -1348,7 +1339,7 @@ fn try_tco_transform(fn_name: Ident, param_names: &[Ident], body: &Program, toke if body.len() != 1 { return None; } - let ast::Expr::If(branches) = &*body[0].expr else { + let ast::Expr::If(branches) = &body[0].expr else { return None; }; @@ -1374,12 +1365,12 @@ fn try_tco_transform(fn_name: Ident, param_names: &[Ident], body: &Program, toke /// Returns `true` if `node` is exactly `Call(fn_name, args)`. fn is_direct_self_call(node: &Shared, fn_name: Ident) -> bool { - matches!(&*node.expr, ast::Expr::Call(ident, _) if ident.name == fn_name) + matches!(&node.expr, ast::Expr::Call(ident, _) if ident.name == fn_name) } /// Returns `true` if `node` contains any call to `fn_name` at any depth. fn contains_self_call(node: &Shared, fn_name: Ident) -> bool { - match &*node.expr { + match &node.expr { ast::Expr::Call(ident, args) => ident.name == fn_name || args.iter().any(|a| contains_self_call(a, fn_name)), ast::Expr::Ident(ident) => ident.name == fn_name, ast::Expr::And(ops) | ast::Expr::Or(ops) => ops.iter().any(|o| contains_self_call(o, fn_name)), @@ -1408,12 +1399,7 @@ fn contains_self_call(node: &Shared, fn_name: Ident) -> bool { /// } /// ``` fn build_tco_loop(fn_name: Ident, param_names: &[Ident], branches: &Branches, token_id: TokenId) -> Program { - let syn = |expr: ast::Expr| -> Shared { - Shared::new(ast::Node { - token_id, - expr: Shared::new(expr), - }) - }; + let syn = |expr: ast::Expr| -> Shared { Shared::new(ast::Node { token_id, expr }) }; let tco_ident = |p: Ident| IdentWithToken::new(&format!("__tco_{}", p.as_str())); @@ -1444,7 +1430,7 @@ fn build_tco_loop(fn_name: Ident, param_names: &[Ident], branches: &Branches, to .iter() .map(|(cond, body)| { let new_body = if is_direct_self_call(body, fn_name) { - let ast::Expr::Call(_, rec_args) = &*body.expr else { + let ast::Expr::Call(_, rec_args) = &body.expr else { unreachable!() }; // __tco_p = new_p; continue @@ -1480,7 +1466,7 @@ fn collect_called_fns(program: &Program) -> FxHashSet { } fn collect_called_fns_node(node: &Shared, set: &mut FxHashSet) { - match &*node.expr { + match &node.expr { ast::Expr::Call(ident, args) => { set.insert(ident.name); for a in args { @@ -1574,7 +1560,7 @@ fn eliminate_dead_defs(program: Program, inlinable: &FxHashMap !inlinable.contains_key(&ident.name) || used.contains(&ident.name), _ => true, }) @@ -1621,7 +1607,7 @@ mod tests { } fn assert_literal(node: &crate::Shared, expected: &str, ctx: &str) { - match &*node.expr { + match &node.expr { Expr::Literal(lit) => assert_eq!(lit.to_string(), expected, "{ctx}"), other => panic!("{ctx}: expected Literal({expected:?}), got {other:?}"), } @@ -1632,7 +1618,7 @@ mod tests { let prog = ast_none("1 + 2"); assert_eq!(prog.len(), 1); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "None: expected Call, got {:?}", prog[0].expr ); @@ -1642,8 +1628,8 @@ mod tests { fn none_consecutive_selectors_stay_separate() { let prog = ast_none(".h1 | .text"); assert_eq!(prog.len(), 2, "None must not merge selectors"); - assert!(matches!(&*prog[0].expr, Expr::Selector(_))); - assert!(matches!(&*prog[1].expr, Expr::Selector(_))); + assert!(matches!(&prog[0].expr, Expr::Selector(_))); + assert!(matches!(&prog[1].expr, Expr::Selector(_))); } #[test] @@ -1651,7 +1637,7 @@ mod tests { let prog = ast_none("if (true): 1 else: 2"); assert_eq!(prog.len(), 1); assert!( - matches!(&*prog[0].expr, Expr::If(_)), + matches!(&prog[0].expr, Expr::If(_)), "None: expected If, got {:?}", prog[0].expr ); @@ -1662,7 +1648,7 @@ mod tests { let prog = ast_none("false && ."); assert_eq!(prog.len(), 1); assert!( - matches!(&*prog[0].expr, Expr::And(_)), + matches!(&prog[0].expr, Expr::And(_)), "None: expected And, got {:?}", prog[0].expr ); @@ -1673,7 +1659,7 @@ mod tests { let prog = ast_none("s\"hello world\""); assert_eq!(prog.len(), 1); assert!( - matches!(&*prog[0].expr, Expr::InterpolatedString(_)), + matches!(&prog[0].expr, Expr::InterpolatedString(_)), "None: expected InterpolatedString, got {:?}", prog[0].expr ); @@ -1683,15 +1669,15 @@ mod tests { fn none_def_body_stays_as_if_no_tco() { let prog = ast_none("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);"); assert_eq!(prog.len(), 1); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))), + !body.iter().any(|n| matches!(&n.expr, Expr::Loop(_))), "None: must not apply TCO; Loop found in body" ); assert!( - body.iter().any(|n| matches!(&*n.expr, Expr::If(_))), + body.iter().any(|n| matches!(&n.expr, Expr::If(_))), "None: original If must remain in body" ); } @@ -1760,7 +1746,7 @@ mod tests { let prog = ast_with("1 / 0", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: div-by-zero must stay as Call, got {:?}", prog[0].expr ); @@ -1774,7 +1760,7 @@ mod tests { let prog = ast_with("add(., 1)", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: dynamic arg must prevent folding, got {:?}", prog[0].expr ); @@ -1792,7 +1778,7 @@ mod tests { let prog = ast_with(query, level); assert_eq!(prog.len(), 1, "{level:?}: {query}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: {query} must stay a Call, got {:?}", prog[0].expr ); @@ -1824,7 +1810,7 @@ mod tests { let prog = ast_with("if (false): 1", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::None)), + matches!(&prog[0].expr, Expr::Literal(Literal::None)), "{level:?}: expected Literal(None), got {:?}", prog[0].expr ); @@ -1847,7 +1833,7 @@ mod tests { let prog = ast_with("if (.): 1 else: 2", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::If(_)), + matches!(&prog[0].expr, Expr::If(_)), "{level:?}: dynamic condition must not eliminate branch, got {:?}", prog[0].expr ); @@ -1870,7 +1856,7 @@ mod tests { let prog = ast_with("if (false): 1 elif (false): 2 elif (false): 3", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::None)), + matches!(&prog[0].expr, Expr::Literal(Literal::None)), "{level:?}: expected Literal(None), got {:?}", prog[0].expr ); @@ -1894,7 +1880,7 @@ mod tests { let prog = ast_with("false && .", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(false))), + matches!(&prog[0].expr, Expr::Literal(Literal::Bool(false))), "{level:?}: expected Literal(false), got {:?}", prog[0].expr ); @@ -1908,7 +1894,7 @@ mod tests { let prog = ast_with("true && .", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::And(_)), + matches!(&prog[0].expr, Expr::And(_)), "{level:?}: expected And([.]), got {:?}", prog[0].expr ); @@ -1921,7 +1907,7 @@ mod tests { let prog = ast_with("true && true && true", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(true))), + matches!(&prog[0].expr, Expr::Literal(Literal::Bool(true))), "{level:?}: expected Literal(true), got {:?}", prog[0].expr ); @@ -1935,7 +1921,7 @@ mod tests { let prog = ast_with("true || .", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(true))), + matches!(&prog[0].expr, Expr::Literal(Literal::Bool(true))), "{level:?}: expected Literal(true), got {:?}", prog[0].expr ); @@ -1949,7 +1935,7 @@ mod tests { let prog = ast_with("false || .", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Or(_)), + matches!(&prog[0].expr, Expr::Or(_)), "{level:?}: expected Or([.]), got {:?}", prog[0].expr ); @@ -1962,7 +1948,7 @@ mod tests { let prog = ast_with("false || false || false", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(false))), + matches!(&prog[0].expr, Expr::Literal(Literal::Bool(false))), "{level:?}: expected Literal(false), got {:?}", prog[0].expr ); @@ -1977,7 +1963,7 @@ mod tests { let prog = ast_with(query, level); assert_eq!(prog.len(), 1, "{level:?}: expected single SelectorChain node"); assert!( - matches!(&*prog[0].expr, Expr::SelectorChain(c) if c.len() == expected_len), + matches!(&prog[0].expr, Expr::SelectorChain(c) if c.len() == expected_len), "{level:?}: expected SelectorChain(len={expected_len}), got {:?}", prog[0].expr ); @@ -1990,7 +1976,7 @@ mod tests { let prog = ast_with(".h1", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Selector(_)), + matches!(&prog[0].expr, Expr::Selector(_)), "{level:?}: single selector must NOT become SelectorChain" ); } @@ -2004,7 +1990,7 @@ mod tests { let prog = ast_with(query, level); assert!(prog.len() > 1, "{level:?}: call between selectors must break the chain"); assert!( - !matches!(&*prog[0].expr, Expr::SelectorChain(_)), + !matches!(&prog[0].expr, Expr::SelectorChain(_)), "{level:?}: must not merge selectors across a call" ); } @@ -2014,8 +2000,8 @@ mod tests { fn none_level_does_not_merge_selectors() { let prog = ast_none(".h1 | .text"); assert_eq!(prog.len(), 2, "None must not merge consecutive selectors"); - assert!(matches!(&*prog[0].expr, Expr::Selector(_))); - assert!(matches!(&*prog[1].expr, Expr::Selector(_))); + assert!(matches!(&prog[0].expr, Expr::Selector(_))); + assert!(matches!(&prog[1].expr, Expr::Selector(_))); } #[test] @@ -2025,11 +2011,11 @@ mod tests { // has one top-level SelectorChain (the inlined call site) and no Def. let prog = ast_full("def extract: .h1 | .text; | extract()"); assert!( - prog.iter().any(|n| matches!(&*n.expr, Expr::SelectorChain(_))), + prog.iter().any(|n| matches!(&n.expr, Expr::SelectorChain(_))), "Full: inlined extract() must produce a top-level SelectorChain" ); assert!( - !prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + !prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: fully-inlined Def must be eliminated" ); } @@ -2042,7 +2028,7 @@ mod tests { let prog = ast_with(query, level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(_)), + matches!(&prog[0].expr, Expr::Literal(_)), "{level:?}: all-text interpolated string must fold to Literal" ); assert_literal(&prog[0], expected, &format!("{level:?}")); @@ -2056,7 +2042,7 @@ mod tests { let prog = ast_with("s\"${self} end\"", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::InterpolatedString(_)), + matches!(&prog[0].expr, Expr::InterpolatedString(_)), "{level:?}: dynamic segment must prevent folding to Literal" ); } @@ -2068,7 +2054,7 @@ mod tests { let prog = ast_none("s\"hello world\""); assert_eq!(prog.len(), 1); assert!( - matches!(&*prog[0].expr, Expr::InterpolatedString(_)), + matches!(&prog[0].expr, Expr::InterpolatedString(_)), "None must not fold interpolated strings" ); } @@ -2087,7 +2073,7 @@ mod tests { let prog = ast_basic("let x = 5 | x + 1"); assert_eq!(prog.len(), 2); assert!( - matches!(&*prog[1].expr, Expr::Call(..)), + matches!(&prog[1].expr, Expr::Call(..)), "Basic must not propagate let-literals; expected Call, got {:?}", prog[1].expr ); @@ -2117,7 +2103,7 @@ mod tests { let prog = ast_with("let x = add(1, .) | x + 0", level); assert_eq!(prog.len(), 2, "{level:?}"); assert!( - !matches!(&*prog[1].expr, Expr::Literal(_)), + !matches!(&prog[1].expr, Expr::Literal(_)), "{level:?}: non-literal let must not propagate to a literal, got {:?}", prog[1].expr ); @@ -2130,7 +2116,7 @@ mod tests { let prog = ast_full("def double(x): x * 2; | double(4)"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Literal(_)), + matches!(&last.expr, Expr::Literal(_)), "Full: inlined+folded call must be Literal, got {:?}", last.expr ); @@ -2143,7 +2129,7 @@ mod tests { let prog = ast_basic("def double(x): x * 2; | double(4)"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Call(..)), + matches!(&last.expr, Expr::Call(..)), "Basic must not inline; expected Call, got {:?}", last.expr ); @@ -2154,7 +2140,7 @@ mod tests { let prog = ast_full("def pi: 3; | pi()"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Literal(_)), + matches!(&last.expr, Expr::Literal(_)), "Full: 0-param constant alias must inline to Literal, got {:?}", last.expr ); @@ -2167,7 +2153,7 @@ mod tests { let prog = ast_full("def fact(n): if (n == 0): 1 else: n * fact(n - 1); | fact(5)"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Call(..)), + matches!(&last.expr, Expr::Call(..)), "Full: recursive function must not be inlined; expected Call, got {:?}", last.expr ); @@ -2179,7 +2165,7 @@ mod tests { let prog = ast_full("let k = 10 | def add_k(x): x + k; | add_k(5)"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Call(..)), + matches!(&last.expr, Expr::Call(..)), "Full: function with free var must not be inlined; expected Call, got {:?}", last.expr ); @@ -2191,7 +2177,7 @@ mod tests { let prog = ast_full("def add1(x): x + 1; | def mul2(x): x * 2; | mul2(add1(3))"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Literal(_)), + matches!(&last.expr, Expr::Literal(_)), "Full: chained inline+fold must collapse to Literal, got {:?}", last.expr ); @@ -2201,16 +2187,16 @@ mod tests { #[test] fn tco_tail_recursive_def_gets_loop_in_full() { let prog = ast_full("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);"); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))), + body.iter().any(|n| matches!(&n.expr, Expr::Loop(_))), "Full: TCO-transformed Def must contain a Loop node" ); // The original top-level If must be replaced — not left alongside the Loop. assert!( - !body.iter().any(|n| matches!(&*n.expr, Expr::If(_))), + !body.iter().any(|n| matches!(&n.expr, Expr::If(_))), "Full: original If must be replaced by Loop after TCO" ); } @@ -2218,11 +2204,11 @@ mod tests { #[test] fn tco_not_applied_in_basic() { let prog = ast_basic("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);"); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))), + !body.iter().any(|n| matches!(&n.expr, Expr::Loop(_))), "Basic must not apply TCO; Loop found unexpectedly" ); } @@ -2230,11 +2216,11 @@ mod tests { #[test] fn tco_not_applied_in_none() { let prog = ast_none("def countdown(n): if (n == 0): \"done\" else: countdown(n - 1);"); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))), + !body.iter().any(|n| matches!(&n.expr, Expr::Loop(_))), "None must not apply TCO" ); } @@ -2243,11 +2229,11 @@ mod tests { fn tco_not_applied_to_non_tail_call() { // `n * fact(n-1)` is a binary op wrapping the recursive call — NOT a tail call. let prog = ast_full("def fact(n): if (n == 0): 1 else: n * fact(n - 1);"); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - !body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))), + !body.iter().any(|n| matches!(&n.expr, Expr::Loop(_))), "Full: non-tail-recursive function must not be TCO-transformed" ); } @@ -2255,11 +2241,11 @@ mod tests { #[test] fn tco_multi_param_def_gets_loop() { let prog = ast_full("def loop2(a, b): if (a == 0): b else: loop2(a - 1, b + 1);"); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - body.iter().any(|n| matches!(&*n.expr, Expr::Loop(_))), + body.iter().any(|n| matches!(&n.expr, Expr::Loop(_))), "Full: multi-param tail-recursive Def must contain Loop" ); } @@ -2278,7 +2264,7 @@ mod tests { let prog = ast_full("def always_false(x): x == 999; | if (always_false(0)): \"bad\" else: \"good\""); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Literal(_)), + matches!(&last.expr, Expr::Literal(_)), "Full: inline+dead-branch must collapse to Literal, got {:?}", last.expr ); @@ -2291,7 +2277,7 @@ mod tests { let prog = ast_full("def inc(x): x + 1; | let n = 9 | inc(n)"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Literal(_)), + matches!(&last.expr, Expr::Literal(_)), "Full: propagation+inline+fold must collapse to Literal, got {:?}", last.expr ); @@ -2315,7 +2301,7 @@ mod tests { let prog = ast_full("let n = 0 | n == 0 && true"); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Literal(Literal::Bool(true))), + matches!(&last.expr, Expr::Literal(Literal::Bool(true))), "Full: propagation+and fold must collapse to Literal(true), got {:?}", last.expr ); @@ -2377,7 +2363,7 @@ mod tests { let prog = ast_with("to_number(\"abc\")", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: unparsable to_number must stay as Call, got {:?}", prog[0].expr ); @@ -2418,7 +2404,7 @@ mod tests { let prog = ast_with(query, level); assert_eq!(prog.len(), 1, "{level:?}: {query}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: {query} must stay a Call, got {:?}", prog[0].expr ); @@ -2473,7 +2459,7 @@ mod tests { let prog = ast_with("coalesce(None, .)", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Self_), + matches!(&prog[0].expr, Expr::Self_), "{level:?}: coalesce(None, .) must fold to Self_, got {:?}", prog[0].expr ); @@ -2554,7 +2540,7 @@ mod tests { let prog = ast_with(q, level); assert_eq!(prog.len(), 1, "{level:?}: {q}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: {q} must remain Call" ); } @@ -2613,7 +2599,7 @@ mod tests { for q in ["floor(.)", "abs(.)"] { let prog = ast_basic(q); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{q} with dynamic arg must stay Call" ); } @@ -2668,11 +2654,11 @@ mod tests { // while(.h1 | .text) is only 2 nodes in the condition, but the condition // itself is a single Call node; so we check a def body instead. let prog = ast_basic("def f: .h1 | .text;"); - let Expr::Def(_, _, body) = &*prog[0].expr else { + let Expr::Def(_, _, body) = &prog[0].expr else { panic!("expected Def"); }; assert!( - body.iter().any(|n| matches!(&*n.expr, Expr::SelectorChain(_))), + body.iter().any(|n| matches!(&n.expr, Expr::SelectorChain(_))), "Basic: SelectorChain must be merged inside Def body" ); } @@ -2682,11 +2668,11 @@ mod tests { // A let binding defined at the top level must not propagate into a nested // def body — they are separate scopes. let prog = ast_full("let x = 99 | def f: x;"); - let Expr::Def(_, _, body) = &*prog.iter().find(|n| matches!(&*n.expr, Expr::Def(..))).unwrap().expr else { + let Expr::Def(_, _, body) = &prog.iter().find(|n| matches!(&n.expr, Expr::Def(..))).unwrap().expr else { panic!("expected Def"); }; assert!( - matches!(&*body[0].expr, Expr::Ident(_)), + matches!(&body[0].expr, Expr::Ident(_)), "Full: top-level let must not propagate into def body, got {:?}", body[0].expr ); @@ -2697,7 +2683,7 @@ mod tests { // After inlining, the Def is no longer called → eliminated. let prog = ast_full("def double(x): x * 2; | double(5)"); assert!( - !prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + !prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: fully-inlined Def must be eliminated from the program" ); let last = prog.last().unwrap(); @@ -2709,7 +2695,7 @@ mod tests { // A recursive Def is not inlinable → must be kept. let prog = ast_full("def count(n): if (n == 0): 0 else: count(n - 1);"); assert!( - prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: non-inlinable Def must be preserved" ); } @@ -2719,7 +2705,7 @@ mod tests { // When a Def is passed as a first-class function value, it must not be eliminated. let prog = ast_full("def is_pos(x): gt(x, 0); | filter(array(1, -1, 2), is_pos)"); assert!( - prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: Def passed as first-class value must be preserved" ); } @@ -2731,7 +2717,7 @@ mod tests { fn def_called_only_inside_conditional_or_loop_not_eliminated(#[case] query: &str, #[case] expected: i64) { let prog = ast_full(query); assert!( - prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: Def called only inside the branch/loop body must be preserved for query {query:?}" ); @@ -2754,8 +2740,8 @@ mod tests { let has_loop = |prog: &crate::ast::Program| { prog.iter().any(|n| { - if let Expr::Def(_, _, body) = &*n.expr { - body.iter().any(|b| matches!(&*b.expr, Expr::Loop(_))) + if let Expr::Def(_, _, body) = &n.expr { + body.iter().any(|b| matches!(&b.expr, Expr::Loop(_))) } else { false } @@ -2825,7 +2811,7 @@ mod tests { let prog = ast_basic("let x = 5 | if (x == 5): \"yes\" else: \"no\""); let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::If(_)), + matches!(&last.expr, Expr::If(_)), "Basic: let propagation must not happen, expected If, got {:?}", last.expr ); @@ -2875,7 +2861,7 @@ mod tests { let prog = ast_with("to_string(b\"hi\")", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: to_string(bytes) must stay as Call" ); } @@ -2889,7 +2875,7 @@ mod tests { let prog = ast_with("floor(nan())", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: floor(nan()) must not fold" ); } @@ -2925,7 +2911,7 @@ mod tests { // inc is called twice — both sites get inlined, so def is eliminated. let prog = ast_full("def inc(x): x + 1; | inc(3) | inc(7)"); assert!( - !prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + !prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: Def with two inlined call sites must be eliminated" ); let last = prog.last().unwrap(); @@ -2940,7 +2926,7 @@ mod tests { assert_eq!(prog.len(), 1); // The while node itself must remain (condition is not false-literal). assert!( - matches!(&*prog[0].expr, Expr::While(..)), + matches!(&prog[0].expr, Expr::While(..)), "Basic: while must remain when condition is dynamic-ish" ); } @@ -2953,7 +2939,7 @@ mod tests { assert_eq!(prog.len(), 1, "{level:?}"); // The Try node remains because catch matters even when body is constant. assert!( - matches!(&*prog[0].expr, Expr::Try(..)), + matches!(&prog[0].expr, Expr::Try(..)), "{level:?}: Try must remain; got {:?}", prog[0].expr ); @@ -2965,11 +2951,11 @@ mod tests { // foreach(x, [1]): 2 + 3 — body constant 5 should fold. let prog = ast_basic("foreach(x, [1]): 2 + 3;"); assert_eq!(prog.len(), 1, "Basic: foreach must be single node"); - let Expr::Foreach(_, _, body) = &*prog[0].expr else { + let Expr::Foreach(_, _, body) = &prog[0].expr else { panic!("expected Foreach"); }; assert!( - body.iter().any(|n| matches!(&*n.expr, Expr::Literal(_))), + body.iter().any(|n| matches!(&n.expr, Expr::Literal(_))), "Basic: Foreach body must have folded constant" ); } @@ -2982,7 +2968,7 @@ mod tests { assert_eq!(prog.len(), 1, "{level:?}"); // The match node remains but its value should be folded. assert!( - matches!(&*prog[0].expr, Expr::Match(..)), + matches!(&prog[0].expr, Expr::Match(..)), "{level:?}: Match must remain when value is not a pattern-eliminating literal" ); } @@ -3012,11 +2998,11 @@ mod tests { // double(3) should be inlined and folded to 6. // fact(4) must remain as Call. assert!( - prog.iter().any(|n| matches!(&*n.expr, Expr::Def(..))), + prog.iter().any(|n| matches!(&n.expr, Expr::Def(..))), "Full: recursive fact must be preserved" ); let last = prog.last().unwrap(); - assert!(matches!(&*last.expr, Expr::Call(..)), "Full: fact(4) must stay as Call"); + assert!(matches!(&last.expr, Expr::Call(..)), "Full: fact(4) must stay as Call"); } // ---- substitute_literals into a CallDynamic node ---- @@ -3047,7 +3033,7 @@ mod tests { let prog = ast_with("0 && .", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Literal(Literal::Bool(false))), + matches!(&prog[0].expr, Expr::Literal(Literal::Bool(false))), "{level:?}: 0 && . must short-circuit to false" ); } @@ -3060,7 +3046,7 @@ mod tests { let prog = ast_with("1 && .", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::And(_)), + matches!(&prog[0].expr, Expr::And(_)), "{level:?}: 1 && . truthy lit must be dropped leaving And([.])" ); } @@ -3073,7 +3059,7 @@ mod tests { let prog = ast_with("coalesce(., .)", level); assert_eq!(prog.len(), 1, "{level:?}"); assert!( - matches!(&*prog[0].expr, Expr::Call(..)), + matches!(&prog[0].expr, Expr::Call(..)), "{level:?}: coalesce(., .) with both dynamic must stay as Call" ); } @@ -3086,7 +3072,7 @@ mod tests { // Has a default param → not inlineable. let last = prog.last().unwrap(); assert!( - matches!(&*last.expr, Expr::Call(..)), + matches!(&last.expr, Expr::Call(..)), "Full: def with default param must not be inlined; expected Call" ); } diff --git a/crates/mq-lang/src/runtime.rs b/crates/mq-lang/src/runtime.rs index 6b904d666..3a8e8f67f 100644 --- a/crates/mq-lang/src/runtime.rs +++ b/crates/mq-lang/src/runtime.rs @@ -1,9 +1,7 @@ -//! Runtime model shared by the tree-walking evaluator ([`crate::eval`]) and the Tarn VM. +//! Runtime model used by the Tarn VM. pub mod builtin; #[cfg(feature = "debugger")] pub mod debugger; -#[cfg(not(feature = "tarn"))] -pub mod env; pub mod host; pub mod runtime_value; diff --git a/crates/mq-lang/src/runtime/builtin.rs b/crates/mq-lang/src/runtime/builtin.rs index 10b9d3a02..c25fd47da 100644 --- a/crates/mq-lang/src/runtime/builtin.rs +++ b/crates/mq-lang/src/runtime/builtin.rs @@ -14,6 +14,7 @@ mod range; mod regex; pub(super) mod tokenizer; +use crate::DictMap; use crate::arena::Arena; use crate::ast::constants; use crate::error::runtime::RuntimeError; @@ -24,10 +25,7 @@ use crate::io::HttpRequestSpec; use crate::io::{FileKind, Io}; use crate::number::{self}; use crate::runtime::builtin::convert::Convert; -#[cfg(not(feature = "tarn"))] -use crate::runtime::env::{self, Env}; use crate::selector::Selector; -#[cfg(feature = "tarn")] use crate::tarn::VmEnv; use crate::{Ident, Shared, SharedCell, Token, get_token, parse_markdown_input, parse_mdx_input}; use base64::Engine; @@ -40,7 +38,6 @@ use similar::{ChangeTag, TextDiff}; use smallvec::SmallVec; use smol_str::SmolStr; use std::borrow::Cow; -use std::collections::BTreeMap; use std::io; use std::process::exit; use std::sync::LazyLock; @@ -103,9 +100,6 @@ fn checked_index(value: &number::Number, operation: &str) -> Result; -#[cfg(not(feature = "tarn"))] -type SharedEnv = Shared>; -#[cfg(feature = "tarn")] type SharedEnv = VmEnv; pub type Args = SmallVec<[RuntimeValue; 2]>; @@ -170,37 +164,6 @@ fn partial_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) let provided = args; match fn_value { - #[cfg(not(feature = "tarn"))] - RuntimeValue::Function(f) => { - if provided.len() >= f.params.len() { - return Err(Error::InvalidNumberOfArguments( - ident.to_string(), - f.params.len() as u8, - provided.len() as u8 + 1, - )); - } - let partial_env = Shared::new(SharedCell::new(Env::with_parent(Shared::downgrade(&f.env)))); - let mut remaining = crate::ast::node::Params::new(); - for (i, param) in f.params.iter().enumerate() { - if i < provided.len() { - #[cfg(not(feature = "sync"))] - partial_env.borrow_mut().define(param.ident.name, provided[i].clone()); - #[cfg(feature = "sync")] - partial_env - .write() - .unwrap() - .define(param.ident.name, provided[i].clone()); - } else { - remaining.push(param.clone()); - } - } - Ok(RuntimeValue::new_function( - Shared::new(remaining), - Shared::clone(&f.body), - partial_env, - )) - } - #[cfg(feature = "tarn")] RuntimeValue::VmClosure(vc) => { let total_params = vc.chunks[vc.chunk_index as usize].param_shape.bindings.len(); let already_bound = vc.bound_args.len(); @@ -2245,12 +2208,12 @@ fn del_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) -> R [RuntimeValue::None, RuntimeValue::Number(_)] => Ok(RuntimeValue::NONE), [RuntimeValue::Dict(dict), RuntimeValue::String(key)] => { let mut dict = std::mem::take(dict); - runtime_value::dict_mut(&mut dict).remove(&Ident::new(key)); + runtime_value::dict_mut(&mut dict).shift_remove(&Ident::new(key)); Ok(RuntimeValue::Dict(dict)) } [RuntimeValue::Dict(dict), RuntimeValue::Symbol(key)] => { let mut dict = std::mem::take(dict); - runtime_value::dict_mut(&mut dict).remove(key); + runtime_value::dict_mut(&mut dict).shift_remove(key); Ok(RuntimeValue::Dict(dict)) } [a, b] => Err(Error::InvalidTypes( @@ -3488,7 +3451,7 @@ fn dict_impl(_: &Ident, _: &RuntimeValue, args: Args, _: &SharedEnv) -> Result = match args.as_slice() { [RuntimeValue::Array(entries)] => match entries.as_slice() { [RuntimeValue::Array(_)] if args.len() == 1 => Cow::Borrowed(entries), @@ -3929,7 +3892,7 @@ fn _csv_parse_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEn .records() .map(|record| { let record = record.map_err(|e| Error::Runtime(format!("Failed to parse CSV record: {e}")))?; - let map: BTreeMap = headers + let map: DictMap = headers .iter() .enumerate() .map(|(i, k)| { @@ -4147,11 +4110,11 @@ fn _xml_parse_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEn reader.config_mut().trim_text(true); let mut buf = Vec::new(); #[allow(clippy::type_complexity)] - let mut stack: Vec<(String, BTreeMap, Vec, Option)> = Vec::new(); + let mut stack: Vec<(String, DictMap, Vec, Option)> = Vec::new(); let mut root: Option = None; let parse_attrs = |e: &quick_xml::events::BytesStart<'_>| { - let mut attrs = BTreeMap::new(); + let mut attrs = DictMap::default(); for attr in e.attributes() { let attr = attr.map_err(|e| Error::Runtime(format!("XML attribute error: {}", e)))?; let key = attr.key.as_ref().to_string(); @@ -4190,7 +4153,7 @@ fn _xml_parse_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEn ))); } - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("tag"), RuntimeValue::String(tag.into())); dict.insert(Ident::new("attributes"), RuntimeValue::Dict(Shared::new(attrs))); dict.insert(Ident::new("children"), RuntimeValue::Array(Shared::new(children))); @@ -4211,7 +4174,7 @@ fn _xml_parse_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEn Ok(quick_xml::events::Event::Empty(e)) => { let tag = e.name().as_ref().to_string(); let attrs = parse_attrs(&e)?; - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("tag"), RuntimeValue::String(tag.into())); dict.insert(Ident::new("attributes"), RuntimeValue::Dict(Shared::new(attrs))); dict.insert(Ident::new("children"), RuntimeValue::empty_array()); @@ -4276,95 +4239,6 @@ fn _html_parse_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedE } } -/// Sets a symbol or variable in the current environment with the given value. -/// -/// Deprecated: relies on the tree-walker's dynamic [`Env`], which the Tarn bytecode VM -/// does not maintain, so this builtin is unavailable under the `tarn` feature and is -/// scheduled for removal in the next release. -#[cfg(not(feature = "tarn"))] -#[mq_macros::mq_fn(name = "set_variable", params = Fixed(2))] -fn set_variable_impl( - ident: &Ident, - value: &RuntimeValue, - mut args: Args, - env: &SharedEnv, -) -> Result { - match args.as_mut_slice() { - [RuntimeValue::Symbol(var_ident), v] => { - #[cfg(not(feature = "sync"))] - { - env.borrow_mut().define(std::mem::take(var_ident), std::mem::take(v)); - } - - #[cfg(feature = "sync")] - { - env.write() - .unwrap() - .define(std::mem::take(var_ident), std::mem::take(v)); - } - - Ok(value.clone()) - } - [RuntimeValue::String(var_name), v] => { - #[cfg(not(feature = "sync"))] - { - env.borrow_mut().define(Ident::new(var_name), std::mem::take(v)); - } - - #[cfg(feature = "sync")] - { - env.write().unwrap().define(Ident::new(var_name), std::mem::take(v)); - } - - Ok(value.clone()) - } - [a, b] => Err(Error::InvalidTypes( - ident.to_string(), - vec![std::mem::take(a), std::mem::take(b)], - )), - _ => unreachable!("set_variable should always receive exactly two arguments"), - } -} - -/// Retrieves the value of a symbol or variable from the current environment. -/// -/// Deprecated: relies on the tree-walker's dynamic [`Env`], which the Tarn bytecode VM -/// does not maintain, so this builtin is unavailable under the `tarn` feature and is -/// scheduled for removal in the next release. -#[cfg(not(feature = "tarn"))] -#[mq_macros::mq_fn(name = "get_variable", params = Fixed(1))] -fn get_variable_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, env: &SharedEnv) -> Result { - match args.as_mut_slice() { - [RuntimeValue::Symbol(var_name)] => { - #[cfg(not(feature = "sync"))] - { - env.borrow().resolve(std::mem::take(var_name)).map_err(Into::into) - } - - #[cfg(feature = "sync")] - { - env.read() - .unwrap() - .resolve(std::mem::take(var_name)) - .map_err(Into::into) - } - } - [RuntimeValue::String(var_name)] => { - #[cfg(not(feature = "sync"))] - { - env.borrow().resolve(Ident::new(var_name)).map_err(Into::into) - } - - #[cfg(feature = "sync")] - { - env.read().unwrap().resolve(Ident::new(var_name)).map_err(Into::into) - } - } - [a] => Err(Error::InvalidTypes(ident.to_string(), vec![std::mem::take(a)])), - _ => unreachable!("get_variable should always receive exactly one argument"), - } -} - #[mq_macros::mq_fn(name = "is_debug_mode", params = None)] fn is_debug_mode_impl(_: &Ident, _: &RuntimeValue, _: Args, _: &SharedEnv) -> Result { #[cfg(feature = "debugger")] @@ -4465,20 +4339,20 @@ fn build_char_inline_diff(s1: &str, s2: &str) -> (Vec, Vec { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("delete".into()))); m.insert(Ident::new("value"), val); del_inline.push(RuntimeValue::Dict(Shared::new(m))); } ChangeTag::Insert => { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("insert".into()))); m.insert(Ident::new("value"), val); ins_inline.push(RuntimeValue::Dict(Shared::new(m))); } ChangeTag::Equal => { for inline in [&mut del_inline, &mut ins_inline] { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("equal".into()))); m.insert( Ident::new("value"), @@ -4515,22 +4389,22 @@ fn _diff_impl(_: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) -> Res let new_val = &a2[new_idx]; if let (RuntimeValue::String(s1), RuntimeValue::String(s2)) = (old_val, new_val) { let (del_inline, ins_inline) = build_char_inline_diff(s1.as_str(), s2.as_str()); - let mut del_map = BTreeMap::new(); + let mut del_map = DictMap::default(); del_map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("delete".into()))); del_map.insert(Ident::new("value"), old_val.clone()); del_map.insert(Ident::new("inline"), RuntimeValue::Array(Shared::new(del_inline))); result.push(RuntimeValue::Dict(Shared::new(del_map))); - let mut ins_map = BTreeMap::new(); + let mut ins_map = DictMap::default(); ins_map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("insert".into()))); ins_map.insert(Ident::new("value"), new_val.clone()); ins_map.insert(Ident::new("inline"), RuntimeValue::Array(Shared::new(ins_inline))); result.push(RuntimeValue::Dict(Shared::new(ins_map))); } else { - let mut del_map = BTreeMap::new(); + let mut del_map = DictMap::default(); del_map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("delete".into()))); del_map.insert(Ident::new("value"), old_val.clone()); result.push(RuntimeValue::Dict(Shared::new(del_map))); - let mut ins_map = BTreeMap::new(); + let mut ins_map = DictMap::default(); ins_map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("insert".into()))); ins_map.insert(Ident::new("value"), new_val.clone()); result.push(RuntimeValue::Dict(Shared::new(ins_map))); @@ -4546,7 +4420,7 @@ fn _diff_impl(_: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) -> Res ChangeTag::Equal | ChangeTag::Delete => a1[changes[i].old_index().unwrap()].clone(), ChangeTag::Insert => a2[changes[i].new_index().unwrap()].clone(), }; - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new(tag_str.into()))); map.insert(Ident::new("value"), value); result.push(RuntimeValue::Dict(Shared::new(map))); @@ -4570,7 +4444,7 @@ fn _diff_impl(_: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) -> Res let old_val = changes[i].value().trim_end_matches('\n'); let new_val = changes[i + 1].value().trim_end_matches('\n'); let (del_inline, ins_inline) = build_char_inline_diff(old_val, new_val); - let mut del_map = BTreeMap::new(); + let mut del_map = DictMap::default(); del_map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("delete".into()))); del_map.insert( Ident::new("value"), @@ -4578,7 +4452,7 @@ fn _diff_impl(_: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) -> Res ); del_map.insert(Ident::new("inline"), RuntimeValue::Array(Shared::new(del_inline))); result.push(RuntimeValue::Dict(Shared::new(del_map))); - let mut ins_map = BTreeMap::new(); + let mut ins_map = DictMap::default(); ins_map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("insert".into()))); ins_map.insert( Ident::new("value"), @@ -4594,7 +4468,7 @@ fn _diff_impl(_: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv) -> Res ChangeTag::Insert => "insert", }; let val = changes[i].value().trim_end_matches('\n').to_string(); - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new(tag_str.into()))); map.insert(Ident::new("value"), RuntimeValue::String(val.into())); result.push(RuntimeValue::Dict(Shared::new(map))); @@ -4726,7 +4600,7 @@ fn file_info_impl(ident: &Ident, _: &RuntimeValue, mut args: Args, _: &SharedEnv .metadata(std::path::Path::new(path.as_str())) .map_err(|e| Error::Runtime(format!("Failed to get info for {}: {}", path, e)))?; - let mut record = BTreeMap::new(); + let mut record = DictMap::default(); record.insert(Ident::new("path"), RuntimeValue::String(path.clone())); record.insert( Ident::new("kind"), @@ -5190,7 +5064,7 @@ fn collection_record(path: String, raw: &str) -> Result { body_nodes.iter().cloned().map(RuntimeValue::from).collect(), )); - let mut record = BTreeMap::new(); + let mut record = DictMap::default(); record.insert(Ident::new("path"), RuntimeValue::String(path.into())); record.insert(Ident::new("title"), title); record.insert(Ident::new("frontmatter"), frontmatter); @@ -5698,10 +5572,6 @@ mq_macros::builtin_dispatch! { _CBOR_PARSE, _CBOR_STRINGIFY, _XML_PARSE, - #[cfg(not(feature = "tarn"))] - SET_VARIABLE, - #[cfg(not(feature = "tarn"))] - GET_VARIABLE, IS_DEBUG_MODE, SHIFT_LEFT, SHIFT_RIGHT, @@ -9348,30 +9218,6 @@ x capability: None, }, ); - #[cfg(not(feature = "tarn"))] - map.insert( - SmolStr::new("set_variable"), - BuiltinFunctionDoc { - description: "Deprecated: tree-walker only, scheduled for removal in the next release. Sets a symbol or variable in the current environment with the given value.", - params: &["symbol_or_string", "value"], - param_types: &["dynamic", "dynamic"], - returns: "dynamic", - examples: &[], - capability: None, - }, - ); - #[cfg(not(feature = "tarn"))] - map.insert( - SmolStr::new("get_variable"), - BuiltinFunctionDoc { - description: "Deprecated: tree-walker only, scheduled for removal in the next release. Retrieves the value of a symbol or variable from the current environment.", - params: &["symbol_or_string"], - param_types: &["dynamic"], - returns: "dynamic", - examples: &[], - capability: None, - }, - ); map.insert( SmolStr::new(constants::builtins::BREAKPOINT), BuiltinFunctionDoc { @@ -9441,7 +9287,7 @@ pub enum Error { #[error("")] NotDefined(FunctionName, Vec), #[error("")] - #[cfg_attr(feature = "tarn", allow(dead_code))] + #[allow(dead_code)] UndefinedReference(String, Vec), #[error("")] InvalidDateTimeFormat(String), @@ -9458,26 +9304,15 @@ pub enum Error { #[error("")] UserDefined(String), #[error("")] - #[cfg_attr(feature = "tarn", allow(dead_code))] + #[allow(dead_code)] AssignToImmutable(String), #[error("")] - #[cfg_attr(feature = "tarn", allow(dead_code))] + #[allow(dead_code)] UndefinedVariable(String), #[error("")] InvalidConvert(String), } -#[cfg(not(feature = "tarn"))] -impl From for Error { - fn from(e: env::EnvError) -> Self { - match e { - env::EnvError::UndefinedReference(name, candidates) => Error::UndefinedReference(name, candidates), - env::EnvError::AssignToImmutable(name) => Error::AssignToImmutable(name), - env::EnvError::UndefinedVariable(name) => Error::UndefinedVariable(name), - } - } -} - impl Error { #[cold] pub fn to_runtime_error( @@ -9543,11 +9378,6 @@ pub fn eval_builtin( ) -> Result { get_builtin_functions(ident).map_or_else( || { - #[cfg(all(not(feature = "tarn"), not(feature = "sync")))] - let candidates = env.borrow().defined_names(); - #[cfg(all(not(feature = "tarn"), feature = "sync"))] - let candidates = env.read().unwrap().defined_names(); - #[cfg(feature = "tarn")] let candidates = env.defined_names(); Err(Error::NotDefined(ident.to_string(), candidates)) @@ -10164,9 +9994,9 @@ fn repeat(value: &mut RuntimeValue, n: usize) -> Result { } } -#[cfg(all(test, not(feature = "tarn")))] +#[cfg(test)] mod tests { - use std::collections::BTreeMap; + use crate::DictMap; use mq_markdown::Node; use rstest::rstest; @@ -10242,9 +10072,9 @@ mod tests { #[case("div", vec![RuntimeValue::Number(8.0.into()), RuntimeValue::Number(2.0.into())].into(), Ok(RuntimeValue::Number(4.0.into())))] #[case("eq", vec![RuntimeValue::String(Shared::new("test".into())), RuntimeValue::String(Shared::new("test".into()))].into(), Ok(RuntimeValue::Boolean(true)))] #[case("ne", vec![RuntimeValue::String(Shared::new("test".into())), RuntimeValue::String(Shared::new("different".into()))].into(), Ok(RuntimeValue::Boolean(true)))] - #[case("has", vec![BTreeMap::from([(Ident::new("a"), RuntimeValue::Number(1.into())), (Ident::new("b"), RuntimeValue::Number(2.into()))]).into(), RuntimeValue::String(Shared::new("a".into()))].into(), Ok(RuntimeValue::Boolean(true)))] - #[case("has", vec![BTreeMap::from([(Ident::new("a"), RuntimeValue::Number(1.into())), (Ident::new("b"), RuntimeValue::Number(2.into()))]).into(), RuntimeValue::String(Shared::new("c".into()))].into(), Ok(RuntimeValue::Boolean(false)))] - #[case("has", vec![BTreeMap::from([(Ident::new("a"), RuntimeValue::None)]).into(), RuntimeValue::String(Shared::new("a".into()))].into(), Ok(RuntimeValue::Boolean(true)))] + #[case("has", vec![DictMap::from_iter([(Ident::new("a"), RuntimeValue::Number(1.into())), (Ident::new("b"), RuntimeValue::Number(2.into()))]).into(), RuntimeValue::String(Shared::new("a".into()))].into(), Ok(RuntimeValue::Boolean(true)))] + #[case("has", vec![DictMap::from_iter([(Ident::new("a"), RuntimeValue::Number(1.into())), (Ident::new("b"), RuntimeValue::Number(2.into()))]).into(), RuntimeValue::String(Shared::new("c".into()))].into(), Ok(RuntimeValue::Boolean(false)))] + #[case("has", vec![DictMap::from_iter([(Ident::new("a"), RuntimeValue::None)]).into(), RuntimeValue::String(Shared::new("a".into()))].into(), Ok(RuntimeValue::Boolean(true)))] #[case("has", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into())])), RuntimeValue::Number(1.into())].into(), Ok(RuntimeValue::Boolean(true)))] #[case("has", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into())])), RuntimeValue::Number(5.into())].into(), Ok(RuntimeValue::Boolean(false)))] #[case("has", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into())])), RuntimeValue::Number((-1).into())].into(), Ok(RuntimeValue::Boolean(false)))] @@ -10252,12 +10082,7 @@ mod tests { fn test_eval_builtin(#[case] func_name: &str, #[case] args: Args, #[case] expected: Result) { let ident = Ident::new(func_name); assert_eq!( - eval_builtin( - &RuntimeValue::None, - &ident, - args, - &Shared::new(SharedCell::new(Env::default())) - ), + eval_builtin(&RuntimeValue::None, &ident, args, &VmEnv::default()), expected ); } @@ -10272,12 +10097,7 @@ mod tests { Error::Runtime("unicode_normalize: invalid normalization form `bogus`, expected one of \"nfc\", \"nfd\", \"nfkc\", \"nfkd\"".to_string()))] fn test_eval_builtin_errors(#[case] func_name: &str, #[case] args: Args, #[case] expected_error: Error) { let ident = Ident::new(func_name); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args, - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, args, &VmEnv::default()); assert!(result.is_err()); assert_eq!(result.unwrap_err(), expected_error); } @@ -10288,13 +10108,7 @@ mod tests { // format: [year, mon(0-11), mday, hour, min, sec, wday(0=Sun), yday(0-365)] let ident = Ident::new("gmtime"); let args = vec![RuntimeValue::Number(0.into())]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ) - .unwrap(); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap(); assert_eq!( result, RuntimeValue::Array(Shared::new(vec![ @@ -10315,13 +10129,7 @@ mod tests { // 2024-01-01T00:00:00 UTC = 1704067200 seconds let ident = Ident::new("gmtime"); let args = vec![RuntimeValue::Number(1704067200_i64.into())]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ) - .unwrap(); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap(); assert_eq!( result, RuntimeValue::Array(Shared::new(vec![ @@ -10342,7 +10150,7 @@ mod tests { #[case(1704067200_i64, 1704067200_i64)] #[case(1718454645_i64, 1718454645_i64)] fn test_mktime_roundtrip(#[case] secs: i64, #[case] expected: i64) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let gmtime_ident = Ident::new("gmtime"); let mktime_ident = Ident::new("mktime"); @@ -10358,7 +10166,7 @@ mod tests { } fn call_uuid_fn(name: &str) -> String { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); match eval_builtin(&RuntimeValue::None, &Ident::new(name), vec![].into(), &env).unwrap() { RuntimeValue::String(s) => s.to_string(), other => panic!("{name} should return a string, got {other:?}"), @@ -10409,7 +10217,7 @@ mod tests { #[test] fn test_rand_is_in_unit_range() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); for _ in 0..200 { match eval_builtin(&RuntimeValue::None, &Ident::new("rand"), vec![].into(), &env).unwrap() { RuntimeValue::Number(n) => assert!((0.0..1.0).contains(&n.value()), "rand() out of [0, 1)"), @@ -10423,7 +10231,7 @@ mod tests { #[case(-5, 5)] #[case(7, 7)] fn test_rand_int_within_bounds(#[case] min: i64, #[case] max: i64) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); for _ in 0..200 { let result = eval_builtin( &RuntimeValue::None, @@ -10444,7 +10252,7 @@ mod tests { #[test] fn test_rand_seeded_is_deterministic_and_in_unit_range() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let call = |seed: i64| match eval_builtin( &RuntimeValue::None, &Ident::new("rand"), @@ -10463,7 +10271,7 @@ mod tests { #[test] fn test_rand_int_seeded_is_deterministic_and_within_bounds() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let call = |seed: i64| match eval_builtin( &RuntimeValue::None, &Ident::new("rand_int"), @@ -10491,7 +10299,7 @@ mod tests { #[test] fn test_rand_int_invalid_range_errors() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("rand_int"), @@ -10503,7 +10311,7 @@ mod tests { #[test] fn test_random_string_uses_only_charset_chars_and_requested_length() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("random_string"), @@ -10526,7 +10334,7 @@ mod tests { #[test] fn test_random_string_zero_length_is_empty() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("random_string"), @@ -10543,7 +10351,7 @@ mod tests { #[test] fn test_random_string_rejects_lengths_above_the_allocation_limit() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("random_string"), @@ -10559,7 +10367,7 @@ mod tests { #[test] fn test_random_string_empty_charset_errors() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("random_string"), @@ -10578,7 +10386,7 @@ mod tests { #[test] fn test_random_string_seeded_is_deterministic() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let call = |seed: i64| match eval_builtin( &RuntimeValue::None, &Ident::new("random_string"), @@ -10604,7 +10412,7 @@ mod tests { #[test] fn test_random_string_calls_are_unique() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let values: std::collections::HashSet = (0..200) .map(|_| { match eval_builtin( @@ -10633,7 +10441,7 @@ mod tests { #[test] fn test_shuffle_preserves_elements() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect(); let result = eval_builtin( &RuntimeValue::None, @@ -10657,7 +10465,7 @@ mod tests { #[test] fn test_shuffle_seeded_is_deterministic() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect(); let call = |seed: i64| match eval_builtin( &RuntimeValue::None, @@ -10683,7 +10491,7 @@ mod tests { #[test] fn test_sample_returns_subset_without_duplicates() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect(); let result = eval_builtin( &RuntimeValue::None, @@ -10713,7 +10521,7 @@ mod tests { #[test] fn test_sample_seeded_is_deterministic() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect(); let call = |seed: i64| match eval_builtin( &RuntimeValue::None, @@ -10740,7 +10548,7 @@ mod tests { #[test] fn test_sample_n_exceeds_length_errors() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let input: Vec = (1..=3).map(|n| RuntimeValue::Number(n.into())).collect(); let result = eval_builtin( &RuntimeValue::None, @@ -10764,13 +10572,7 @@ mod tests { RuntimeValue::Number(ts.into()), RuntimeValue::String(Shared::new(fmt.into())), ]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ) - .unwrap(); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap(); assert_eq!(result, RuntimeValue::String(Shared::new(expected.into()))); } @@ -10784,13 +10586,7 @@ mod tests { RuntimeValue::String(Shared::new(date_str.into())), RuntimeValue::String(Shared::new(fmt.into())), ]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ) - .unwrap(); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap(); assert_eq!(result, RuntimeValue::Number(expected.into())); } @@ -10801,17 +10597,12 @@ mod tests { RuntimeValue::String(Shared::new("not-a-date".into())), RuntimeValue::String(Shared::new("%Y-%m-%d".into())), ]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()); assert!(result.is_err()); } fn gmtime_array(secs: i64) -> RuntimeValue { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); eval_builtin( &RuntimeValue::None, &Ident::new("gmtime"), @@ -10830,7 +10621,7 @@ mod tests { #[case(1704067200_i64, -1, "days", 1703980800_i64)] #[case(1704067200_i64, 1, "weeks", 1704672000_i64)] fn test_date_add_duration(#[case] base: i64, #[case] n: i64, #[case] unit: &str, #[case] expected_secs: i64) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let arr = gmtime_array(base); let result = eval_builtin( &RuntimeValue::None, @@ -10853,7 +10644,7 @@ mod tests { #[test] fn test_date_add_months_end_of_month() { // 2024-01-31 + 1 month = 2024-02-29 (leap year) - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let arr = gmtime_array(1706659200); // 2024-01-31T00:00:00Z let result = eval_builtin( &RuntimeValue::None, @@ -10875,7 +10666,7 @@ mod tests { #[test] fn test_date_add_years() { // 2024-02-29 + 1 year = 2025-02-28 (non-leap year clamps) - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let arr = gmtime_array(1709164800); // 2024-02-29T00:00:00Z let result = eval_builtin( &RuntimeValue::None, @@ -10896,7 +10687,7 @@ mod tests { #[test] fn test_date_add_invalid_unit() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let arr = gmtime_array(0); let result = eval_builtin( &RuntimeValue::None, @@ -10921,7 +10712,7 @@ mod tests { #[case(1704067200_i64, 1704672000_i64, "weeks", 1_i64)] #[case(1704153600_i64, 1704067200_i64, "seconds", -86400_i64)] fn test_date_diff(#[case] base1: i64, #[case] base2: i64, #[case] unit: &str, #[case] expected: i64) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let arr1 = gmtime_array(base1); let arr2 = gmtime_array(base2); let result = eval_builtin( @@ -10936,7 +10727,7 @@ mod tests { #[test] fn test_date_diff_invalid_unit() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let arr = gmtime_array(0); let result = eval_builtin( &RuntimeValue::None, @@ -10951,12 +10742,7 @@ mod tests { fn test_gmtime_invalid_type() { let ident = Ident::new("gmtime"); let args = vec![RuntimeValue::String(Shared::new("not a number".into()))]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()); assert!(matches!(result, Err(Error::InvalidTypes(_, _)))); } @@ -10964,18 +10750,13 @@ mod tests { fn test_mktime_invalid_input() { let ident = Ident::new("mktime"); let args = vec![RuntimeValue::String(Shared::new("not an array".into()))]; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()); assert!(matches!(result, Err(Error::InvalidTypes(_, _)))); } #[test] fn test_date_add_malformed_array_error_prefix() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let bad_arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("x".into())); 8])); let result = eval_builtin( &RuntimeValue::None, @@ -10996,7 +10777,7 @@ mod tests { #[test] fn test_date_diff_malformed_array_error_prefix() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let bad_arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("x".into())); 8])); let result = eval_builtin( &RuntimeValue::None, @@ -11026,7 +10807,7 @@ mod tests { #[case("next monday", 1705881600_i64)] #[case("last friday", 1705017600_i64)] fn test_date_relative(#[case] input: &str, #[case] expected_secs: i64) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("date_relative"), @@ -11043,7 +10824,7 @@ mod tests { #[test] fn test_date_relative_invalid_expression() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("date_relative"), @@ -11065,7 +10846,7 @@ mod tests { #[test] fn test_date_relative_invalid_types() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin( &RuntimeValue::None, &Ident::new("date_relative"), @@ -11081,12 +10862,7 @@ mod tests { let first_arg = RuntimeValue::String(Shared::new("hello world".into())); let args = vec![RuntimeValue::String(Shared::new("hello".into()))]; - let result = eval_builtin( - &first_arg, - &ident, - args.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&first_arg, &ident, args.into(), &VmEnv::default()); assert_eq!(result, Ok(RuntimeValue::Boolean(true))); } @@ -11433,36 +11209,32 @@ mod tests { // Tests for Dict functions #[rstest] #[case( - BTreeMap::from([("a".into(), RuntimeValue::Number(1.0.into())), ("b".into(), RuntimeValue::Number(2.0.into()))]), - BTreeMap::from([("c".into(), RuntimeValue::Number(3.0.into()))]), - BTreeMap::from([("a".into(), RuntimeValue::Number(1.0.into())), ("b".into(), RuntimeValue::Number(2.0.into())), ("c".into(), RuntimeValue::Number(3.0.into()))]), + DictMap::from_iter([("a".into(), RuntimeValue::Number(1.0.into())), ("b".into(), RuntimeValue::Number(2.0.into()))]), + DictMap::from_iter([("c".into(), RuntimeValue::Number(3.0.into()))]), + DictMap::from_iter([("a".into(), RuntimeValue::Number(1.0.into())), ("b".into(), RuntimeValue::Number(2.0.into())), ("c".into(), RuntimeValue::Number(3.0.into()))]), )] #[case( - BTreeMap::from([("a".into(), RuntimeValue::Number(1.0.into()))]), - BTreeMap::from([("a".into(), RuntimeValue::Number(99.0.into())), ("b".into(), RuntimeValue::Number(2.0.into()))]), - BTreeMap::from([("a".into(), RuntimeValue::Number(99.0.into())), ("b".into(), RuntimeValue::Number(2.0.into()))]), + DictMap::from_iter([("a".into(), RuntimeValue::Number(1.0.into()))]), + DictMap::from_iter([("a".into(), RuntimeValue::Number(99.0.into())), ("b".into(), RuntimeValue::Number(2.0.into()))]), + DictMap::from_iter([("a".into(), RuntimeValue::Number(99.0.into())), ("b".into(), RuntimeValue::Number(2.0.into()))]), )] #[case( - BTreeMap::new(), - BTreeMap::from([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), - BTreeMap::from([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), + DictMap::default(), + DictMap::from_iter([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), + DictMap::from_iter([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), )] #[case( - BTreeMap::from([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), - BTreeMap::new(), - BTreeMap::from([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), - )] - fn test_eval_builtin_add_dict( - #[case] d1: BTreeMap, - #[case] d2: BTreeMap, - #[case] expected: BTreeMap, - ) { + DictMap::from_iter([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), + DictMap::default(), + DictMap::from_iter([("x".into(), RuntimeValue::String(Shared::new("hello".into())))]), + )] + fn test_eval_builtin_add_dict(#[case] d1: DictMap, #[case] d2: DictMap, #[case] expected: DictMap) { let ident = Ident::new("add"); let result = eval_builtin( &RuntimeValue::None, &ident, vec![RuntimeValue::Dict(Shared::new(d1)), RuntimeValue::Dict(Shared::new(d2))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Dict(Shared::new(expected)))); } @@ -11470,12 +11242,7 @@ mod tests { #[test] fn test_eval_builtin_new_dict() { let ident = Ident::new("dict"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![].into(), &VmEnv::default()); assert!(result.is_ok()); let map_val = result.unwrap(); match map_val { @@ -11493,11 +11260,11 @@ mod tests { RuntimeValue::String(Shared::new("value".into())), ]))] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!( result, - Ok(RuntimeValue::Dict(Shared::new(BTreeMap::from([( + Ok(RuntimeValue::Dict(Shared::new(DictMap::from_iter([( "key".into(), RuntimeValue::String(Shared::new("value".into())) )])))) @@ -11514,12 +11281,7 @@ mod tests { RuntimeValue::String(Shared::new("name".into())), RuntimeValue::String(Shared::new("Jules".into())), ]; - let result1 = eval_builtin( - &RuntimeValue::None, - &ident_set, - args1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result1 = eval_builtin(&RuntimeValue::None, &ident_set, args1.into(), &VmEnv::default()); assert!(result1.is_ok()); let map_val1 = result1.unwrap(); match &map_val1 { @@ -11538,12 +11300,7 @@ mod tests { RuntimeValue::String(Shared::new("age".into())), RuntimeValue::Number(30.into()), ]; - let result2 = eval_builtin( - &RuntimeValue::None, - &ident_set, - args2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result2 = eval_builtin(&RuntimeValue::None, &ident_set, args2.into(), &VmEnv::default()); assert!(result2.is_ok()); let map_val2 = result2.unwrap(); match &map_val2 { @@ -11563,12 +11320,7 @@ mod tests { RuntimeValue::String(Shared::new("name".into())), RuntimeValue::String(Shared::new("Vincent".into())), ]; - let result3 = eval_builtin( - &RuntimeValue::None, - &ident_set, - args3.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result3 = eval_builtin(&RuntimeValue::None, &ident_set, args3.into(), &VmEnv::default()); assert!(result3.is_ok()); let map_val3 = result3.unwrap(); match &map_val3 { @@ -11583,7 +11335,7 @@ mod tests { _ => panic!("Expected Dict, got {:?}", map_val3), } - let mut nested_map_data = BTreeMap::default(); + let mut nested_map_data = DictMap::default(); nested_map_data.insert(Ident::new("level"), RuntimeValue::Number(2.into())); let nested_map: RuntimeValue = nested_map_data.into(); let args4 = vec![ @@ -11591,12 +11343,7 @@ mod tests { RuntimeValue::String(Shared::new("nested".into())), nested_map.clone(), ]; - let result4 = eval_builtin( - &RuntimeValue::None, - &ident_set, - args4.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result4 = eval_builtin(&RuntimeValue::None, &ident_set, args4.into(), &VmEnv::default()); assert!(result4.is_ok()); match result4.unwrap() { RuntimeValue::Dict(map) => { @@ -11611,12 +11358,7 @@ mod tests { RuntimeValue::String(Shared::new("key".into())), RuntimeValue::String(Shared::new("value".into())), ]; - let result_err1 = eval_builtin( - &RuntimeValue::None, - &ident_set, - args_err1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err1 = eval_builtin(&RuntimeValue::None, &ident_set, args_err1.into(), &VmEnv::default()); assert_eq!( result_err1, Err(Error::InvalidTypes( @@ -11634,12 +11376,7 @@ mod tests { RuntimeValue::Number(123.into()), RuntimeValue::String(Shared::new("value".into())), ]; - let result_err2 = eval_builtin( - &RuntimeValue::None, - &ident_set, - args_err2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err2 = eval_builtin(&RuntimeValue::None, &ident_set, args_err2.into(), &VmEnv::default()); assert_eq!( result_err2, Err(Error::InvalidTypes( @@ -11656,39 +11393,24 @@ mod tests { #[test] fn test_eval_builtin_get_map() { let ident_get = Ident::new("get"); - let mut map_data = BTreeMap::default(); + let mut map_data = DictMap::default(); map_data.insert("name".into(), RuntimeValue::String(Shared::new("Jules".into()))); map_data.insert("age".into(), RuntimeValue::Number(30.into())); let map_val: RuntimeValue = map_data.into(); let args1 = vec![map_val.clone(), RuntimeValue::String(Shared::new("name".into()))]; - let result1 = eval_builtin( - &RuntimeValue::None, - &ident_get, - args1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result1 = eval_builtin(&RuntimeValue::None, &ident_get, args1.into(), &VmEnv::default()); assert_eq!(result1, Ok(RuntimeValue::String(Shared::new("Jules".into())))); let args2 = vec![map_val.clone(), RuntimeValue::String(Shared::new("location".into()))]; - let result2 = eval_builtin( - &RuntimeValue::None, - &ident_get, - args2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result2 = eval_builtin(&RuntimeValue::None, &ident_get, args2.into(), &VmEnv::default()); assert_eq!(result2, Ok(RuntimeValue::None)); let args_err1 = vec![ RuntimeValue::String(Shared::new("not_a_map".into())), RuntimeValue::String(Shared::new("key".into())), ]; - let result_err1 = eval_builtin( - &RuntimeValue::None, - &ident_get, - args_err1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err1 = eval_builtin(&RuntimeValue::None, &ident_get, args_err1.into(), &VmEnv::default()); assert_eq!( result_err1, Err(Error::InvalidTypes( @@ -11701,12 +11423,7 @@ mod tests { ); let args_err2 = vec![map_val.clone(), RuntimeValue::Number(123.into())]; - let result_err2 = eval_builtin( - &RuntimeValue::None, - &ident_get, - args_err2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err2 = eval_builtin(&RuntimeValue::None, &ident_get, args_err2.into(), &VmEnv::default()); assert_eq!( result_err2, Err(Error::InvalidTypes( @@ -11738,7 +11455,7 @@ mod tests { &RuntimeValue::None, &ident_get, vec![parent.clone(), RuntimeValue::Number(255.into())].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ) .unwrap(); assert_eq!(in_range.markdown_node().unwrap().value(), "child255"); @@ -11750,7 +11467,7 @@ mod tests { &RuntimeValue::None, &ident_get, vec![parent, RuntimeValue::Number(300.into())].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ) .unwrap(); assert_eq!(out_of_range.markdown_node(), None); @@ -11761,25 +11478,15 @@ mod tests { let ident_keys = Ident::new("keys"); let empty_map = RuntimeValue::new_dict(); let args1 = vec![empty_map.clone()]; - let result1 = eval_builtin( - &RuntimeValue::None, - &ident_keys, - args1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result1 = eval_builtin(&RuntimeValue::None, &ident_keys, args1.into(), &VmEnv::default()); assert_eq!(result1, Ok(RuntimeValue::Array(Shared::new(vec![])))); - let mut map_data = BTreeMap::default(); + let mut map_data = DictMap::default(); map_data.insert("name".into(), RuntimeValue::String(Shared::new("Jules".into()))); map_data.insert("age".into(), RuntimeValue::Number(30.into())); let map_val: RuntimeValue = map_data.into(); let args2 = vec![map_val.clone()]; - let result2 = eval_builtin( - &RuntimeValue::None, - &ident_keys, - args2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result2 = eval_builtin(&RuntimeValue::None, &ident_keys, args2.into(), &VmEnv::default()); assert!(result2.is_ok()); match result2.unwrap() { RuntimeValue::Array(keys_array) => { @@ -11797,12 +11504,7 @@ mod tests { } let args_err1 = vec![RuntimeValue::String(Shared::new("not_a_map".into()))]; - let result_err1 = eval_builtin( - &RuntimeValue::None, - &ident_keys, - args_err1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err1 = eval_builtin(&RuntimeValue::None, &ident_keys, args_err1.into(), &VmEnv::default()); assert_eq!( result_err1, Err(Error::InvalidTypes( @@ -11812,12 +11514,7 @@ mod tests { ); let args_err2 = vec![map_val.clone(), RuntimeValue::String(Shared::new("extra".into()))]; - let result_err2 = eval_builtin( - &RuntimeValue::None, - &ident_keys, - args_err2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err2 = eval_builtin(&RuntimeValue::None, &ident_keys, args_err2.into(), &VmEnv::default()); assert_eq!( result_err2, Err(Error::InvalidNumberOfArguments("keys".to_string(), 1, 2)) @@ -11829,25 +11526,15 @@ mod tests { let ident_values = Ident::new("values"); let empty_map = RuntimeValue::new_dict(); let args1 = vec![empty_map.clone()]; - let result1 = eval_builtin( - &RuntimeValue::None, - &ident_values, - args1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result1 = eval_builtin(&RuntimeValue::None, &ident_values, args1.into(), &VmEnv::default()); assert_eq!(result1, Ok(RuntimeValue::Array(Shared::new(vec![])))); - let mut map_data = BTreeMap::default(); + let mut map_data = DictMap::default(); map_data.insert("name".into(), RuntimeValue::String(Shared::new("Jules".into()))); map_data.insert("age".into(), RuntimeValue::Number(30.into())); let map_val: RuntimeValue = map_data.into(); let args2 = vec![map_val.clone()]; - let result2 = eval_builtin( - &RuntimeValue::None, - &ident_values, - args2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result2 = eval_builtin(&RuntimeValue::None, &ident_values, args2.into(), &VmEnv::default()); assert!(result2.is_ok()); match result2.unwrap() { RuntimeValue::Array(values_array) => { @@ -11859,12 +11546,7 @@ mod tests { } let args_err1 = vec![RuntimeValue::String(Shared::new("not_a_map".into()))]; - let result_err1 = eval_builtin( - &RuntimeValue::None, - &ident_values, - args_err1.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err1 = eval_builtin(&RuntimeValue::None, &ident_values, args_err1.into(), &VmEnv::default()); assert_eq!( result_err1, Err(Error::InvalidTypes( @@ -11874,12 +11556,7 @@ mod tests { ); let args_err2 = vec![map_val.clone(), RuntimeValue::String(Shared::new("extra".into()))]; - let result_err2 = eval_builtin( - &RuntimeValue::None, - &ident_values, - args_err2.into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result_err2 = eval_builtin(&RuntimeValue::None, &ident_values, args_err2.into(), &VmEnv::default()); assert_eq!( result_err2, Err(Error::InvalidNumberOfArguments("values".to_string(), 1, 2)) @@ -12028,7 +11705,7 @@ mod tests { #[case::del_array("del", vec![RuntimeValue::empty_array(), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into())])] #[case::del_string("del", vec![RuntimeValue::String(Shared::new("".into())), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into())])] fn collection_mutators_reject_oversized_indices(#[case] name: &str, #[case] args: Vec) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let result = eval_builtin(&RuntimeValue::None, &Ident::new(name), args.into(), &env); assert!(matches!(result, Err(Error::Runtime(message)) if message.contains("index"))); } @@ -12093,7 +11770,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(csv.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12102,10 +11779,10 @@ mod tests { #[case::simple_with_header( "name,age\nAlice,30\nBob,25", { - let mut alice = BTreeMap::new(); + let mut alice = DictMap::default(); alice.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); alice.insert(Ident::new("age"), RuntimeValue::String(Shared::new("30".to_string()))); - let mut bob = BTreeMap::new(); + let mut bob = DictMap::default(); bob.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Bob".to_string()))); bob.insert(Ident::new("age"), RuntimeValue::String(Shared::new("25".to_string()))); Ok(RuntimeValue::Array(Shared::new(vec![ @@ -12117,7 +11794,7 @@ mod tests { #[case::single_row_with_header( "id,value\n1,hello", { - let mut row = BTreeMap::new(); + let mut row = DictMap::default(); row.insert(Ident::new("id"), RuntimeValue::String(Shared::new("1".to_string()))); row.insert(Ident::new("value"), RuntimeValue::String(Shared::new("hello".to_string()))); Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row))]))) @@ -12126,7 +11803,7 @@ mod tests { #[case::quoted_fields_with_header( "name,note\n\"Doe, Jane\",\"says \"\"hi\"\"\"", { - let mut row = BTreeMap::new(); + let mut row = DictMap::default(); row.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Doe, Jane".to_string()))); row.insert(Ident::new("note"), RuntimeValue::String(Shared::new("says \"hi\"".to_string()))); Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row))]))) @@ -12135,11 +11812,11 @@ mod tests { #[case::ragged_rows_with_header( "a,b,c\n1,2\n3,4,5,6", { - let mut short_row = BTreeMap::new(); + let mut short_row = DictMap::default(); short_row.insert(Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string()))); short_row.insert(Ident::new("b"), RuntimeValue::String(Shared::new("2".to_string()))); short_row.insert(Ident::new("c"), RuntimeValue::String(Shared::new("".to_string()))); - let mut long_row = BTreeMap::new(); + let mut long_row = DictMap::default(); long_row.insert(Ident::new("a"), RuntimeValue::String(Shared::new("3".to_string()))); long_row.insert(Ident::new("b"), RuntimeValue::String(Shared::new("4".to_string()))); long_row.insert(Ident::new("c"), RuntimeValue::String(Shared::new("5".to_string()))); @@ -12160,7 +11837,7 @@ mod tests { RuntimeValue::Boolean(true), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12188,7 +11865,7 @@ mod tests { "\t", true, { - let mut row = BTreeMap::new(); + let mut row = DictMap::default(); row.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); row.insert(Ident::new("age"), RuntimeValue::String(Shared::new("30".to_string()))); Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row))]))) @@ -12210,7 +11887,7 @@ mod tests { RuntimeValue::Boolean(has_header), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12220,12 +11897,7 @@ mod tests { #[case::invalid_type_bool(RuntimeValue::Boolean(false))] fn test_csv_parse_invalid_arg_type(#[case] invalid_arg: RuntimeValue) { let ident = Ident::new("_csv_parse"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![invalid_arg].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![invalid_arg].into(), &VmEnv::default()); assert!(result.is_err()); } @@ -12233,7 +11905,7 @@ mod tests { #[case::simple_object( r#"{"key": "value"}"#, { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("key"), RuntimeValue::String(Shared::new("value".to_string()))); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12249,12 +11921,12 @@ mod tests { #[case::nested( r#"{"a": [true, null], "b": {"c": 1.2}}"#, { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("a"), RuntimeValue::Array(Shared::new(vec![ RuntimeValue::Boolean(true), RuntimeValue::NONE, ]))); - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert(Ident::new("c"), RuntimeValue::Number(1.2.into())); map.insert(Ident::new("b"), RuntimeValue::Dict(Shared::new(inner))); Ok(RuntimeValue::Dict(Shared::new(map))) @@ -12270,7 +11942,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(json.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12284,12 +11956,7 @@ mod tests { RuntimeValue::Number(n) => RuntimeValue::Number(n), s => RuntimeValue::String(Shared::new(s.to_string())), }; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![arg].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![arg].into(), &VmEnv::default()); assert!(result.is_err()); } @@ -12297,7 +11964,7 @@ mod tests { #[case::mapping( "key: value", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("key"), RuntimeValue::String(Shared::new("value".to_string()))); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12313,9 +11980,9 @@ mod tests { #[case::nested( "a:\n b: 42", { - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert(Ident::new("b"), RuntimeValue::Number(42.into())); - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(inner))); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12323,7 +11990,7 @@ mod tests { #[case::boolean( "flag: true", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("flag"), RuntimeValue::Boolean(true)); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12331,7 +11998,7 @@ mod tests { #[case::null( "value: null", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("value"), RuntimeValue::NONE); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12339,7 +12006,7 @@ mod tests { #[case::float( "ratio: 1.5", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("ratio"), RuntimeValue::Number(1.5.into())); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12347,9 +12014,9 @@ mod tests { #[case::multi_document( "a: 1\n---\nb: 2\n", { - let mut first = BTreeMap::new(); + let mut first = DictMap::default(); first.insert(Ident::new("a"), RuntimeValue::Number(1.into())); - let mut second = BTreeMap::new(); + let mut second = DictMap::default(); second.insert(Ident::new("b"), RuntimeValue::Number(2.into())); Ok(RuntimeValue::Array(Shared::new(vec![ RuntimeValue::Dict(Shared::new(first)), @@ -12363,7 +12030,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(yaml.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12376,12 +12043,7 @@ mod tests { RuntimeValue::Number(n) => RuntimeValue::Number(n), s => RuntimeValue::String(Shared::new(s.to_string())), }; - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![arg].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![arg].into(), &VmEnv::default()); assert!(result.is_err()); } @@ -12389,7 +12051,7 @@ mod tests { #[case::simple_kv( "a: 1\nb: 2", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("a"), RuntimeValue::Number(1.into())); map.insert(Ident::new("b"), RuntimeValue::Number(2.into())); Ok(RuntimeValue::Dict(Shared::new(map))) @@ -12398,9 +12060,9 @@ mod tests { #[case::nested_indent( "parent:\n child: value", { - let mut child_map = BTreeMap::new(); + let mut child_map = DictMap::default(); child_map.insert(Ident::new("child"), RuntimeValue::String(Shared::new("value".to_string()))); - let mut parent_map = BTreeMap::new(); + let mut parent_map = DictMap::default(); parent_map.insert(Ident::new("parent"), RuntimeValue::Dict(Shared::new(child_map))); Ok(RuntimeValue::Dict(Shared::new(parent_map))) } @@ -12408,13 +12070,13 @@ mod tests { #[case::tabular_data( "hikes[2]{id,name}:\n 1,Blue Lake\n 2,Ridge Trail", { - let mut row1 = BTreeMap::new(); + let mut row1 = DictMap::default(); row1.insert(Ident::new("id"), RuntimeValue::Number(1.into())); row1.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Blue Lake".to_string()))); - let mut row2 = BTreeMap::new(); + let mut row2 = DictMap::default(); row2.insert(Ident::new("id"), RuntimeValue::Number(2.into())); row2.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Ridge Trail".to_string()))); - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("hikes"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row1)), RuntimeValue::Dict(Shared::new(row2))]))); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12422,7 +12084,7 @@ mod tests { #[case::inline_array( "items[3]: 1, 2, 3", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![ RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), @@ -12434,7 +12096,7 @@ mod tests { #[case::expanded_array( "items[2]:\n - 1\n - 2", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![ RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), @@ -12445,7 +12107,7 @@ mod tests { #[case::primitives( "s: \"string\"\nb: true\nn: null\nf: false", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("s"), RuntimeValue::String(Shared::new("string".to_string()))); map.insert(Ident::new("b"), RuntimeValue::TRUE); map.insert(Ident::new("n"), RuntimeValue::NONE); @@ -12459,7 +12121,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(toon.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12476,7 +12138,7 @@ mod tests { #[case::none(RuntimeValue::NONE, "null")] #[case::single_key_dict( { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); RuntimeValue::Dict(Shared::new(map)) }, @@ -12487,7 +12149,7 @@ mod tests { "[2]: 1,2" )] #[case::empty_array(RuntimeValue::Array(Shared::new(vec![])), "[0]:")] - #[case::empty_dict(RuntimeValue::Dict(Shared::new(BTreeMap::new())), "")] + #[case::empty_dict(RuntimeValue::Dict(Shared::new(DictMap::default())), "")] #[case::empty_string_needs_quoting(RuntimeValue::String(Shared::new("".to_string())), "\"\"")] #[case::numeric_like_string_needs_quoting(RuntimeValue::String(Shared::new("123".to_string())), "\"123\"")] #[case::keyword_like_string_needs_quoting(RuntimeValue::String(Shared::new("true".to_string())), "\"true\"")] @@ -12496,17 +12158,12 @@ mod tests { #[case::string_starting_with_dash_needs_quoting(RuntimeValue::String(Shared::new("-x".to_string())), "\"-x\"")] fn test_toon_stringify(#[case] input: RuntimeValue, #[case] expected: &str) { let ident = Ident::new("_toon_stringify"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![input].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default()); assert_eq!(result, Ok(RuntimeValue::String(Shared::new(expected.to_string())))); } fn toon_tabular_row(id: i64, name: &str) -> RuntimeValue { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("id"), RuntimeValue::Number(id.into())); map.insert(Ident::new("name"), RuntimeValue::String(Shared::new(name.to_string()))); RuntimeValue::Dict(Shared::new(map)) @@ -12521,9 +12178,9 @@ mod tests { toon_tabular_row(2, "Ridge Trail"), ])))] #[case::nested_dict({ - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert(Ident::new("inner"), RuntimeValue::Number(1.into())); - let mut outer = BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert(Ident::new("outer"), RuntimeValue::Dict(Shared::new(inner))); RuntimeValue::Dict(Shared::new(outer)) })] @@ -12533,7 +12190,7 @@ mod tests { RuntimeValue::String(Shared::new("text".to_string())), ])))] fn test_toon_stringify_round_trip(#[case] original: RuntimeValue) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let ident_stringify = Ident::new("_toon_stringify"); let stringified = eval_builtin( @@ -12563,7 +12220,7 @@ mod tests { #[case::simple_kv( "name = \"Alice\"\nage = 30", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); map.insert(Ident::new("age"), RuntimeValue::Number(30.into())); Ok(RuntimeValue::Dict(Shared::new(map))) @@ -12572,7 +12229,7 @@ mod tests { #[case::boolean( "enabled = true\ndisabled = false", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("enabled"), RuntimeValue::Boolean(true)); map.insert(Ident::new("disabled"), RuntimeValue::Boolean(false)); Ok(RuntimeValue::Dict(Shared::new(map))) @@ -12581,10 +12238,10 @@ mod tests { #[case::nested_table( "[server]\nhost = \"localhost\"\nport = 8080", { - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert(Ident::new("host"), RuntimeValue::String(Shared::new("localhost".to_string()))); inner.insert(Ident::new("port"), RuntimeValue::Number(8080.into())); - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("server"), RuntimeValue::Dict(Shared::new(inner))); Ok(RuntimeValue::Dict(Shared::new(map))) } @@ -12592,7 +12249,7 @@ mod tests { #[case::array( "tags = [\"rust\", \"toml\"]", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("tags"), RuntimeValue::Array(Shared::new(vec![ RuntimeValue::String(Shared::new("rust".to_string())), RuntimeValue::String(Shared::new("toml".to_string())), @@ -12606,7 +12263,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(toml.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12619,7 +12276,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(input.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -12628,12 +12285,7 @@ mod tests { #[case::invalid_type(RuntimeValue::Number(1.into()))] fn test_toml_parse_invalid_type(#[case] input: RuntimeValue) { let ident = Ident::new("_toml_parse"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![input].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default()); assert!(result.is_err()); } @@ -12642,7 +12294,7 @@ mod tests { // {"name": "Alice", "age": 30} "omRuYW1lZUFsaWNlY2FnZRge", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); map.insert(Ident::new("age"), RuntimeValue::Number(30.into())); Ok(RuntimeValue::Dict(Shared::new(map))) @@ -12654,7 +12306,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(input.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12668,7 +12320,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(input.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -12677,12 +12329,7 @@ mod tests { #[case::invalid_type(RuntimeValue::Number(1.into()))] fn test_cbor_parse_invalid_type(#[case] input: RuntimeValue) { let ident = Ident::new("_cbor_parse"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![input].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default()); assert!(result.is_err()); } @@ -12691,14 +12338,14 @@ mod tests { // {"name": "Alice", "age": 30} encoded as CBOR then base64 "omRuYW1lZUFsaWNlY2FnZRge", { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); map.insert(Ident::new("age"), RuntimeValue::Number(30.into())); Ok(RuntimeValue::Dict(Shared::new(map))) } )] fn test_cbor_stringify_roundtrip(#[case] base64_input: &str, #[case] expected: Result) { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); // parse let ident_parse = Ident::new("_cbor_parse"); @@ -12734,10 +12381,10 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(cbor_bytes.into())].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_ok()); - let mut expected = BTreeMap::new(); + let mut expected = DictMap::default(); expected.insert( Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), @@ -12753,7 +12400,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(bytes.into())].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::String(Shared::new("SGVsbG8=".to_string())))); } @@ -12785,12 +12432,7 @@ mod tests { )] fn test_to_bytes(#[case] input: RuntimeValue, #[case] expected: Result) { let ident = Ident::new("to_bytes"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![input].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default()); assert_eq!(result, expected); } @@ -12804,12 +12446,7 @@ mod tests { #[case::array_with_infinity(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(f64::INFINITY.into())])))] fn test_to_bytes_invalid(#[case] input: RuntimeValue) { let ident = Ident::new("to_bytes"); - let result = eval_builtin( - &RuntimeValue::None, - &ident, - vec![input].into(), - &Shared::new(SharedCell::new(Env::default())), - ); + let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default()); assert!(result.is_err()); } @@ -12824,7 +12461,7 @@ mod tests { RuntimeValue::Bytes(Shared::new(vec![3, 4])), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![1, 2, 3, 4])))); } @@ -12836,7 +12473,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(Shared::new(vec![1, 2, 3]))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![3, 2, 1])))); } @@ -12853,7 +12490,7 @@ mod tests { RuntimeValue::Number(4.into()), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![20, 30, 40])))); } @@ -12865,7 +12502,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!( result, @@ -12882,7 +12519,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!( result, @@ -12899,7 +12536,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new("hello".to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!( result, @@ -12916,7 +12553,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!( result, @@ -12936,7 +12573,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(input.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -12950,7 +12587,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(input.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result.is_err(), is_err); } @@ -12966,14 +12603,14 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(input.into())].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } #[test] fn test_to_hex_roundtrip() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let original = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]; let hex = eval_builtin( &RuntimeValue::None, @@ -13006,7 +12643,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(lhs.into()), RuntimeValue::Bytes(rhs.into())].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Boolean(expected))); } @@ -13018,7 +12655,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::String(Shared::new("hello".to_string())))); } @@ -13030,7 +12667,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::Bytes(Shared::new(vec![0xff, 0xfe]))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -13046,7 +12683,7 @@ mod tests { RuntimeValue::String(Shared::new("shift_jis".to_string())), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::String(Shared::new("あ".to_string())))); } @@ -13062,7 +12699,7 @@ mod tests { RuntimeValue::String(Shared::new("not-a-real-encoding".to_string())), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -13078,7 +12715,7 @@ mod tests { RuntimeValue::String(Shared::new("shift_jis".to_string())), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -13094,14 +12731,14 @@ mod tests { RuntimeValue::String(Shared::new("shift_jis".to_string())), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![0x82, 0xa0])))); } #[test] fn test_encode_decode_roundtrip() { - let env = Shared::new(SharedCell::new(Env::default())); + let env = VmEnv::default(); let encoded = eval_builtin( &RuntimeValue::None, &Ident::new("encode"), @@ -13133,7 +12770,7 @@ mod tests { RuntimeValue::String(Shared::new("shift_jis".to_string())), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -13149,7 +12786,7 @@ mod tests { RuntimeValue::Bytes(Shared::new(vec![0x55, 0x44])), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![0xff, 0xff])))); } @@ -13165,7 +12802,7 @@ mod tests { RuntimeValue::Bytes(Shared::new(vec![0x00, 0x00, 0x00])), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![0x01, 0x02, 0x03])))); } @@ -13181,7 +12818,7 @@ mod tests { RuntimeValue::Bytes(Shared::new(vec![0x01])), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_err()); } @@ -13190,7 +12827,7 @@ mod tests { #[case::simple( "hello", { - let mut root = BTreeMap::new(); + let mut root = DictMap::default(); root.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("root".to_string()))); root.insert(Ident::new("attributes"), RuntimeValue::new_dict()); root.insert(Ident::new("children"), RuntimeValue::empty_array()); @@ -13201,8 +12838,8 @@ mod tests { #[case::with_attributes( "hello", { - let mut root = BTreeMap::new(); - let mut attrs = BTreeMap::new(); + let mut root = DictMap::default(); + let mut attrs = DictMap::default(); attrs.insert(Ident::new("id"), RuntimeValue::String(Shared::new("1".to_string()))); attrs.insert(Ident::new("class"), RuntimeValue::String(Shared::new("main".to_string()))); root.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("root".to_string()))); @@ -13215,17 +12852,17 @@ mod tests { #[case::nested( "helloworld", { - let mut root = BTreeMap::new(); - let mut child1 = BTreeMap::new(); - let mut attrs1 = BTreeMap::new(); + let mut root = DictMap::default(); + let mut child1 = DictMap::default(); + let mut attrs1 = DictMap::default(); attrs1.insert(Ident::new("id"), RuntimeValue::String(Shared::new("1".to_string()))); child1.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("child".to_string()))); child1.insert(Ident::new("attributes"), RuntimeValue::Dict(Shared::new(attrs1))); child1.insert(Ident::new("children"), RuntimeValue::empty_array()); child1.insert(Ident::new("text"), RuntimeValue::String(Shared::new("hello".to_string()))); - let mut child2 = BTreeMap::new(); - let mut attrs2 = BTreeMap::new(); + let mut child2 = DictMap::default(); + let mut attrs2 = DictMap::default(); attrs2.insert(Ident::new("id"), RuntimeValue::String(Shared::new("2".to_string()))); child2.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("child".to_string()))); child2.insert(Ident::new("attributes"), RuntimeValue::Dict(Shared::new(attrs2))); @@ -13245,9 +12882,9 @@ mod tests { #[case::self_closing( "", { - let mut root = BTreeMap::new(); - let mut child = BTreeMap::new(); - let mut attrs = BTreeMap::new(); + let mut root = DictMap::default(); + let mut child = DictMap::default(); + let mut attrs = DictMap::default(); attrs.insert(Ident::new("id"), RuntimeValue::String(Shared::new("1".to_string()))); child.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("child".to_string()))); child.insert(Ident::new("attributes"), RuntimeValue::Dict(Shared::new(attrs))); @@ -13269,7 +12906,7 @@ mod tests { &RuntimeValue::None, &ident, vec![RuntimeValue::String(Shared::new(xml.to_string()))].into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert_eq!(result, expected); } @@ -13285,7 +12922,7 @@ mod tests { RuntimeValue::String(Shared::new("abc ".into())), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_ok()); @@ -13351,7 +12988,7 @@ mod tests { RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into())])), ] .into(), - &Shared::new(SharedCell::new(Env::default())), + &VmEnv::default(), ); assert!(result.is_ok()); @@ -13828,8 +13465,8 @@ mod tests { assert_eq!(!result.is_none(), expected_match); } - fn env() -> Shared> { - Shared::new(SharedCell::new(Env::default())) + fn env() -> VmEnv { + VmEnv::default() } fn call(name: &str, args: Vec) -> Result { @@ -14584,7 +14221,7 @@ mod tests { get(&entries[0], "title"), RuntimeValue::String(Shared::new("World".into())) ); - let mut toml_frontmatter = BTreeMap::new(); + let mut toml_frontmatter = DictMap::default(); toml_frontmatter.insert(Ident::new("title"), RuntimeValue::String(Shared::new("World".into()))); assert_eq!( get(&entries[0], "frontmatter"), @@ -14901,7 +14538,7 @@ mod tests { #[cfg(feature = "file-io")] fn walk_files_options(entries: &[(&str, bool)]) -> RuntimeValue { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); for (key, value) in entries { map.insert(Ident::new(key), RuntimeValue::Boolean(*value)); } @@ -15522,15 +15159,15 @@ mod tests { } let requests = RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Dict(Shared::new(std::collections::BTreeMap::from([( + RuntimeValue::Dict(Shared::new(DictMap::from_iter([( Ident::new("url"), RuntimeValue::String(Shared::new("https://example.invalid/a".into())), )]))), - RuntimeValue::Dict(Shared::new(std::collections::BTreeMap::from([( + RuntimeValue::Dict(Shared::new(DictMap::from_iter([( Ident::new("url"), RuntimeValue::String(Shared::new("https://example.invalid/b".into())), )]))), - RuntimeValue::Dict(Shared::new(std::collections::BTreeMap::from([ + RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("method"), RuntimeValue::Symbol(Ident::new("post"))), ( Ident::new("url"), @@ -15567,9 +15204,9 @@ mod tests { fn test_http_all_rejects_request_without_url() { let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default()).allow_net(true))); - let requests = RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new( - std::collections::BTreeMap::from([(Ident::new("method"), RuntimeValue::Symbol(Ident::new("get")))]), - ))])); + let requests = RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter( + [(Ident::new("method"), RuntimeValue::Symbol(Ident::new("get")))], + )))])); assert!(call("http_all", vec![requests]).is_err()); } diff --git a/crates/mq-lang/src/runtime/builtin/css.rs b/crates/mq-lang/src/runtime/builtin/css.rs index 26cff1ac6..b1b9cd43f 100644 --- a/crates/mq-lang/src/runtime/builtin/css.rs +++ b/crates/mq-lang/src/runtime/builtin/css.rs @@ -10,7 +10,7 @@ //! //! Gated at compile time by the `css-selector` feature. -use std::collections::BTreeMap; +use crate::DictMap; use ego_tree::NodeRef; use scraper::{Html, Node, Selector}; @@ -27,7 +27,7 @@ fn err(msg: impl std::fmt::Display) -> Error { // Same `{tag, attributes, children, text}` shape as `_xml_parse`; whitespace-only text is dropped. fn build_element(node: NodeRef<'_, Node>) -> RuntimeValue { let element = node.value().as_element().expect("node is an element"); - let mut attributes = BTreeMap::new(); + let mut attributes = DictMap::default(); for (name, value) in element.attrs() { attributes.insert(Ident::new(name), RuntimeValue::String(Shared::new(value.to_string()))); @@ -55,7 +55,7 @@ fn build_element(node: NodeRef<'_, Node>) -> RuntimeValue { } } - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert( Ident::new("tag"), RuntimeValue::String(Shared::new(element.name().to_string())), diff --git a/crates/mq-lang/src/runtime/builtin/http.rs b/crates/mq-lang/src/runtime/builtin/http.rs index bf2082cc8..1dfa9d2eb 100644 --- a/crates/mq-lang/src/runtime/builtin/http.rs +++ b/crates/mq-lang/src/runtime/builtin/http.rs @@ -9,11 +9,11 @@ //! resolution filtered to publicly routable addresses so a hostname can't be rebound to //! an internal address after the initial check. -use std::collections::BTreeMap; +use crate::DictMap; use super::Error; use super::io_context; -use crate::{Ident, RuntimeValue}; +use crate::RuntimeValue; /// Builds an `Error::Runtime` with the `http: ` prefix shared by every error in this module. fn err(msg: impl std::fmt::Display) -> Error { @@ -36,7 +36,7 @@ fn parse_method(value: &RuntimeValue) -> Result { } /// Extracts `(name, value)` pairs from `headers`, requiring every value to be a string. -fn extract_headers(headers: Option<&BTreeMap>) -> Result, Error> { +fn extract_headers(headers: Option<&DictMap>) -> Result, Error> { let Some(headers) = headers else { return Ok(Vec::new()); }; @@ -56,7 +56,7 @@ pub(super) fn request( method: &RuntimeValue, url: &str, body: Option<&str>, - headers: Option<&BTreeMap>, + headers: Option<&DictMap>, ) -> Result { let method = parse_method(method)?; let headers = extract_headers(headers)?; @@ -180,7 +180,7 @@ mod tests { &symbol("get"), "https://this-domain-should-not-exist-mq-test.invalid", None, - Some(&BTreeMap::from([( + Some(&DictMap::from_iter([( Ident::new("Authorization"), RuntimeValue::String(Shared::new("Bearer token".into())) )])) @@ -203,7 +203,7 @@ mod tests { #[test] fn test_extract_headers_accepts_string_values() { - let headers = BTreeMap::from([ + let headers = DictMap::from_iter([ (Ident::new("X-Test"), RuntimeValue::String(Shared::new("value".into()))), ( Ident::new("Content-Type"), @@ -217,7 +217,7 @@ mod tests { #[test] fn test_extract_headers_rejects_non_string_values() { - let headers = BTreeMap::from([(Ident::new("X-Test"), RuntimeValue::from(1usize))]); + let headers = DictMap::from_iter([(Ident::new("X-Test"), RuntimeValue::from(1usize))]); assert!(extract_headers(Some(&headers)).is_err()); } diff --git a/crates/mq-lang/src/runtime/builtin/regex.rs b/crates/mq-lang/src/runtime/builtin/regex.rs index cd9c3caaa..813215078 100644 --- a/crates/mq-lang/src/runtime/builtin/regex.rs +++ b/crates/mq-lang/src/runtime/builtin/regex.rs @@ -1,9 +1,9 @@ +use crate::DictMap; use crate::Ident; use crate::Shared; use crate::runtime::runtime_value::RuntimeValue; use regex::{Regex, RegexBuilder}; use rustc_hash::{FxBuildHasher, FxHashMap}; -use std::collections::BTreeMap; use std::sync::{LazyLock, RwLock}; use super::Error; @@ -68,7 +68,7 @@ pub(super) fn is_match_re(input: &str, pattern: &str) -> Result Result { match (re.capture_names(), re.captures(input)) { (names, Some(caps)) => { - let mut result = BTreeMap::new(); + let mut result = DictMap::default(); for name in names.flatten() { if let Some(m) = caps.name(name) { result.insert( diff --git a/crates/mq-lang/src/runtime/debugger.rs b/crates/mq-lang/src/runtime/debugger.rs index 77a76f4ec..578187db0 100644 --- a/crates/mq-lang/src/runtime/debugger.rs +++ b/crates/mq-lang/src/runtime/debugger.rs @@ -1,11 +1,8 @@ use itertools::Itertools; use super::runtime_value::RuntimeValue; -#[cfg(feature = "tarn")] use crate::Ident; use crate::ast::node as ast; -#[cfg(not(feature = "tarn"))] -use crate::runtime::env::Env; use crate::{Shared, SharedCell, Token}; use std::{collections::HashSet, fmt::Debug}; @@ -68,11 +65,8 @@ pub struct DebugContext { pub token: Shared, /// Call stack of AST nodes representing the current execution path pub call_stack: Vec>, - /// Current evaluation environment info for the tree-walker backend. - #[cfg(not(feature = "tarn"))] - pub env: Shared>, /// Live VM bindings for the paused frame. - #[cfg(all(feature = "tarn", feature = "debugger"))] + #[cfg(feature = "debugger")] pub(crate) vm_frame: VmDebugFrame, /// Snapshot of the VM operand stack at the current statement boundary. #[cfg(feature = "debug-trace")] @@ -87,7 +81,7 @@ impl Default for DebugContext { current_value: RuntimeValue::NONE, current_node: Shared::new(ast::Node { token_id: crate::ast::TokenId::new(0), - expr: Shared::new(ast::Expr::Literal(ast::Literal::Number(0.0.into()))), + expr: ast::Expr::Literal(ast::Literal::Number(0.0.into())), }), token: Shared::new(Token { kind: crate::TokenKind::Eof, @@ -95,9 +89,7 @@ impl Default for DebugContext { module_id: crate::ModuleId::new(0), }), call_stack: Vec::new(), - #[cfg(not(feature = "tarn"))] - env: Shared::new(SharedCell::new(Env::default())), - #[cfg(all(feature = "tarn", feature = "debugger"))] + #[cfg(feature = "debugger")] vm_frame: VmDebugFrame::default(), #[cfg(feature = "debug-trace")] operand_stack: Vec::new(), @@ -107,7 +99,7 @@ impl Default for DebugContext { } /// A variable visible in a paused Tarn VM frame. -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] #[derive(Debug, Clone)] pub(crate) struct VmDebugBinding { name: Ident, @@ -115,7 +107,7 @@ pub(crate) struct VmDebugBinding { value: RuntimeValue, } -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] impl VmDebugBinding { pub(crate) fn new(name: Ident, slot: u16, value: RuntimeValue) -> Self { Self { name, slot, value } @@ -124,7 +116,7 @@ impl VmDebugBinding { /// Debugger display metadata for a VM slot. Kept separate from the tree-walker's /// environment representation so Tarn builds do not retain `runtime::env`. -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] #[derive(Debug, Clone, PartialEq)] pub struct VmVariable { pub name: String, @@ -132,7 +124,7 @@ pub struct VmVariable { pub type_field: String, } -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] impl VmVariable { fn from_binding(binding: &VmDebugBinding) -> Self { Self { @@ -143,7 +135,7 @@ impl VmVariable { } } -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] impl std::fmt::Display for VmVariable { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{} = {}, type: {}", self.name, self.value, self.type_field) @@ -151,7 +143,7 @@ impl std::fmt::Display for VmVariable { } /// A pending write to a VM slot requested while execution is stopped. -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] #[derive(Debug, Clone)] pub(crate) struct VmDebugUpdate { pub(crate) is_upvalue: bool, @@ -163,7 +155,7 @@ pub(crate) struct VmDebugUpdate { /// /// The debugger handler blocks the VM while a DAP client inspects the frame, so updates queued /// here can safely be applied by the interpreter immediately before it resumes execution. -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] #[derive(Debug, Clone)] pub(crate) struct VmDebugFrame { locals: Shared>>, @@ -172,7 +164,7 @@ pub(crate) struct VmDebugFrame { active: Shared>, } -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] impl Default for VmDebugFrame { fn default() -> Self { Self { @@ -184,7 +176,7 @@ impl Default for VmDebugFrame { } } -#[cfg(all(feature = "tarn", feature = "debugger"))] +#[cfg(feature = "debugger")] impl VmDebugFrame { pub(crate) fn new(locals: Vec, upvalues: Vec) -> Self { Self { @@ -243,19 +235,13 @@ impl VmDebugFrame { impl DebugContext { /// Returns variables local to the currently paused frame. - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] pub fn local_variables(&self) -> Vec { VmDebugFrame::variables(&self.vm_frame.locals) } - /// Returns variables local to the currently paused tree-walker frame. - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn local_variables(&self) -> Vec { - self.env.read().unwrap().get_local_variables() - } - /// Returns variables captured from the enclosing frame or global scope. - #[cfg(all(feature = "debugger", feature = "tarn"))] + #[cfg(feature = "debugger")] pub fn global_variables(&self) -> Vec { if self.call_stack.is_empty() { VmDebugFrame::variables(&self.vm_frame.locals) @@ -264,14 +250,8 @@ impl DebugContext { } } - /// Returns variables captured from the tree-walker's enclosing frame or global scope. - #[cfg(all(feature = "debugger", not(feature = "tarn")))] - pub fn global_variables(&self) -> Vec { - self.env.read().unwrap().get_global_variables() - } - /// Returns the bindings visible to a paused Tarn VM frame. - #[cfg(all(feature = "tarn", feature = "debugger"))] + #[cfg(feature = "debugger")] pub fn vm_bindings(&self) -> Vec<(Ident, RuntimeValue)> { self.vm_frame.bindings() } @@ -279,14 +259,14 @@ impl DebugContext { /// Queues an update to a local or captured Tarn VM binding. /// /// The update takes effect when the VM debugger hook returns to the interpreter. - #[cfg(all(feature = "tarn", feature = "debugger"))] + #[cfg(feature = "debugger")] pub fn set_vm_variable(&self, name: &str, value: RuntimeValue, prefer_upvalue: bool) -> bool { self.vm_frame .set_variable(Ident::new(name), value, prefer_upvalue && !self.call_stack.is_empty()) } /// Queues an update to any visible Tarn VM binding, preferring a local binding over a capture. - #[cfg(all(feature = "tarn", feature = "debugger"))] + #[cfg(feature = "debugger")] pub fn set_vm_expression(&self, name: &str, value: RuntimeValue) -> bool { self.vm_frame.set_expression(Ident::new(name), value) } @@ -710,7 +690,7 @@ mod tests { fn make_node(token_id: TokenId) -> Shared { Shared::new(ast::Node { token_id, - expr: Shared::new(ast::Expr::Literal(ast::Literal::Number(42.0.into()))), + expr: ast::Expr::Literal(ast::Literal::Number(42.0.into())), }) } @@ -724,9 +704,7 @@ mod tests { current_node: node, token: Shared::clone(&token), call_stack: Vec::new(), - #[cfg(not(feature = "tarn"))] - env: Shared::new(SharedCell::new(Env::default())), - #[cfg(feature = "tarn")] + #[cfg(feature = "debugger")] vm_frame: Default::default(), #[cfg(feature = "debug-trace")] operand_stack: Vec::new(), diff --git a/crates/mq-lang/src/runtime/env.rs b/crates/mq-lang/src/runtime/env.rs deleted file mode 100644 index 6fdd5ae67..000000000 --- a/crates/mq-lang/src/runtime/env.rs +++ /dev/null @@ -1,1267 +0,0 @@ -use super::builtin; -use super::runtime_value::RuntimeValue; -use crate::ast::TokenId; -use crate::error::runtime::RuntimeError; -use crate::{Ident, SharedCell, Token, TokenArena, get_token}; -use rustc_hash::{FxHashMap, FxHashSet}; -use smallvec::SmallVec; -use std::error::Error; -use std::fmt::{self, Debug}; - -#[cfg(not(feature = "sync"))] -type Weak = std::rc::Weak; - -#[cfg(feature = "sync")] -type Weak = std::sync::Weak; - -#[derive(Debug, PartialEq)] -pub enum EnvError { - /// Unresolved identifier, plus the currently-defined names it was compared against - /// (for "did you mean" suggestions). - UndefinedReference(String, Vec), - AssignToImmutable(String), - UndefinedVariable(String), -} - -impl Error for EnvError {} - -impl fmt::Display for EnvError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", self) - } -} - -impl EnvError { - /// Converts this error into a [`RuntimeError`] with the source location resolved from the token arena. - #[cold] - pub fn to_runtime_error(&self, token_id: TokenId, token_arena: TokenArena) -> RuntimeError { - match self { - EnvError::UndefinedReference(def, candidates) => RuntimeError::UndefinedReference( - (*get_token(token_arena, token_id)).clone(), - def.to_string(), - candidates.clone().into(), - ), - EnvError::AssignToImmutable(var) => { - RuntimeError::AssignToImmutable((*get_token(token_arena, token_id)).clone(), var.to_string()) - } - EnvError::UndefinedVariable(var) => { - RuntimeError::UndefinedVariable((*get_token(token_arena, token_id)).clone(), var.to_string()) - } - } - } - - /// Converts this error into a [`RuntimeError`] using the provided token as the source location. - #[cold] - pub fn to_runtime_error_with_token(&self, token: Token) -> RuntimeError { - match self { - EnvError::UndefinedReference(def, candidates) => { - RuntimeError::UndefinedReference(token, def.to_string(), candidates.clone().into()) - } - EnvError::AssignToImmutable(var) => RuntimeError::AssignToImmutable(token, var.to_string()), - EnvError::UndefinedVariable(var) => RuntimeError::UndefinedVariable(token, var.to_string()), - } - } -} - -/// Scopes with this many or more unique entries are promoted from SmallVec to FxHashMap. -/// Below this threshold, linear search over a stack-allocated array is faster than hashing. -const PROMOTE_THRESHOLD: usize = 6; - -/// Maximum capacity of the SmallVec variant of EnvContext. Must be at least PROMOTE_THRESHOLD - 1 to avoid panicking on push. -const ENV_CONTEXT_CAPACITY: usize = PROMOTE_THRESHOLD - 1; - -/// Per-scope variable storage. -/// -/// `Small` is stack-allocated and uses linear search. -/// It is used for child scopes (function parameters, `let`/`var` bindings) where the number of variables is small. -/// -/// `Large` is a heap-allocated hash map. -/// It is used only for the global scope, which accumulates many entries. -/// -/// `Env` is always stored inside `Shared>` (i.e. on the heap), so the large `Small` variant does not cause stack pressure. -#[allow(clippy::large_enum_variant)] -#[derive(Debug, Clone)] -enum EnvContext { - Small(SmallVec<[(Ident, RuntimeValue); ENV_CONTEXT_CAPACITY]>), - Large(Box>), -} - -impl Default for EnvContext { - fn default() -> Self { - EnvContext::Large(Box::default()) - } -} - -impl PartialEq for EnvContext { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (EnvContext::Small(a), EnvContext::Small(b)) => a == b, - (EnvContext::Large(a), EnvContext::Large(b)) => a == b, - _ => false, - } - } -} - -impl EnvContext { - #[inline] - fn new_small() -> Self { - EnvContext::Small(SmallVec::new()) - } - - #[inline] - fn get(&self, ident: Ident) -> Option<&RuntimeValue> { - match self { - EnvContext::Small(v) => v.iter().rev().find(|(k, _)| *k == ident).map(|(_, v)| v), - EnvContext::Large(m) => m.get(&ident), - } - } - - /// Upsert: update an existing binding if present, otherwise push a new one. - #[inline] - fn upsert(&mut self, ident: Ident, value: RuntimeValue) { - match self { - EnvContext::Small(v) => { - if let Some(entry) = v.iter_mut().find(|(k, _)| *k == ident) { - entry.1 = value; - return; - } - v.push((ident, value)); - if v.len() >= PROMOTE_THRESHOLD { - let map: FxHashMap = std::mem::take(v).into_iter().collect(); - *self = EnvContext::Large(Box::new(map)); - } - } - EnvContext::Large(m) => { - m.insert(ident, value); - } - } - } - - #[inline] - fn contains_key(&self, ident: Ident) -> bool { - match self { - EnvContext::Small(v) => v.iter().any(|(k, _)| *k == ident), - EnvContext::Large(m) => m.contains_key(&ident), - } - } - - fn len(&self) -> usize { - match self { - EnvContext::Small(v) => v.len(), - EnvContext::Large(m) => m.len(), - } - } - - fn iter_entries(&self) -> Box + '_> { - match self { - EnvContext::Small(v) => Box::new(v.iter().map(|(k, v)| (*k, v))), - EnvContext::Large(m) => Box::new(m.iter().map(|(k, v)| (*k, v))), - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct Env { - context: EnvContext, - mutable_vars: Option>, - parent: Option>>, -} - -impl PartialEq for Env { - fn eq(&self, other: &Self) -> bool { - self.context == other.context - && self.mutable_vars == other.mutable_vars - && self.parent.as_ref().map(|p| p.as_ptr()) == other.parent.as_ref().map(|p| p.as_ptr()) - } -} - -#[cfg(feature = "debugger")] -#[derive(Debug, Clone, PartialEq)] -pub struct Variable { - pub name: String, - pub value: String, - pub type_field: String, -} - -#[cfg(feature = "debugger")] -impl Variable { - /// Creates debugger display metadata for a runtime binding. - pub fn from(ident: Ident, value: &RuntimeValue) -> Self { - match value { - RuntimeValue::Array(_) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "array".to_string(), - }, - RuntimeValue::Boolean(_) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "bool".to_string(), - }, - RuntimeValue::Dict(_) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "dict".to_string(), - }, - RuntimeValue::String(_) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "string".to_string(), - }, - RuntimeValue::Symbol(_) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "symbol".to_string(), - }, - RuntimeValue::Number(_) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "number".to_string(), - }, - RuntimeValue::Markdown(_, _) => Variable { - name: ident.to_string(), - value: value.to_string(), - type_field: "markdown".to_string(), - }, - RuntimeValue::Function(f) => Variable { - name: ident.to_string(), - value: format!("function/{}", f.params.len()), - type_field: "function".to_string(), - }, - RuntimeValue::NativeFunction(_) => Variable { - name: ident.to_string(), - value: "native function".to_string(), - type_field: "native_function".to_string(), - }, - #[cfg(feature = "tarn")] - RuntimeValue::VmClosure(_) => Variable { - name: ident.to_string(), - value: "function".to_string(), - type_field: "function".to_string(), - }, - RuntimeValue::Module(m) => Variable { - name: m.name().to_string(), - value: format!("module/{}", m.len()), - type_field: "module".to_string(), - }, - RuntimeValue::Bytes(b) => Variable { - name: ident.to_string(), - value: format!("bytes({})", b.len()), - type_field: "bytes".to_string(), - }, - RuntimeValue::None => Variable { - name: ident.to_string(), - value: "None".to_string(), - type_field: "none".to_string(), - }, - } - } -} - -#[cfg(feature = "debugger")] -impl std::fmt::Display for Variable { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{} = {}, type: {}", self.name, self.value, self.type_field) - } -} - -macro_rules! borrow_env { - ($cell:expr) => {{ - #[cfg(not(feature = "sync"))] - { - $cell.borrow() - } - #[cfg(feature = "sync")] - { - $cell.read().unwrap() - } - }}; -} - -macro_rules! borrow_env_mut { - ($cell:expr) => {{ - #[cfg(not(feature = "sync"))] - { - $cell.borrow_mut() - } - #[cfg(feature = "sync")] - { - $cell.write().unwrap() - } - }}; -} - -impl Env { - /// Creates a child scope. Uses `Small` (stack-allocated SmallVec) storage because - /// function/block scopes typically hold only a handful of variables. - pub fn with_parent(parent: Weak>) -> Self { - Self { - context: EnvContext::new_small(), - mutable_vars: None, - parent: Some(parent), - } - } - - /// Collects the names of every binding visible from this scope (this scope plus all - /// ancestors) so a failed lookup can suggest a "did you mean" among user-defined - /// functions/variables, not just builtins. Cold path only - never called on the - /// `resolve()` success path. - #[cold] - pub(crate) fn defined_names(&self) -> Vec { - let mut names: Vec = self - .context - .iter_entries() - .map(|(ident, _)| ident.to_string()) - .collect(); - - let mut current_parent = self.parent.as_ref().and_then(|p| p.upgrade()); - - while let Some(parent_cell) = current_parent { - let parent_env = borrow_env!(parent_cell); - names.extend(parent_env.context.iter_entries().map(|(ident, _)| ident.to_string())); - current_parent = parent_env.parent.as_ref().and_then(|p| p.upgrade()); - } - - names - } - - /// Returns the number of bindings in the current scope, excluding parent scopes. - pub fn len(&self) -> usize { - self.context.len() - } - - /// Defines or overwrites an immutable binding for `ident` in the current scope. - #[inline(always)] - pub fn define(&mut self, ident: Ident, runtime_value: RuntimeValue) { - self.context.upsert(ident, runtime_value); - } - - /// Looks up `ident` in this scope and its ancestors, falling back to built-in functions. - /// - /// Returns [`EnvError::UndefinedReference`] if the identifier is not found anywhere in - /// the scope chain and is not a built-in. - #[inline(always)] - pub fn resolve(&self, ident: Ident) -> Result { - if let Some(o) = self.context.get(ident) { - return Ok(o.clone()); - } - - let mut current_parent = self.parent.as_ref().and_then(|p| p.upgrade()); - - while let Some(parent_cell) = current_parent { - let parent_env = borrow_env!(parent_cell); - - if let Some(o) = parent_env.context.get(ident) { - return Ok(o.clone()); - } - current_parent = parent_env.parent.as_ref().and_then(|p| p.upgrade()); - } - - if builtin::get_builtin_functions(&ident).is_some() { - Ok(RuntimeValue::NativeFunction(ident)) - } else { - Err(EnvError::UndefinedReference(ident.to_string(), self.defined_names())) - } - } - - /// Defines a mutable variable in the current environment - #[inline(always)] - pub fn define_mutable(&mut self, ident: Ident, runtime_value: RuntimeValue) { - self.context.upsert(ident, runtime_value); - self.mutable_vars.get_or_insert_with(FxHashSet::default).insert(ident); - } - - /// Assigns a value to an existing mutable variable - pub fn assign(&mut self, ident: Ident, runtime_value: RuntimeValue) -> Result<(), EnvError> { - if self.context.contains_key(ident) { - if self.mutable_vars.as_ref().is_some_and(|s| s.contains(&ident)) { - self.context.upsert(ident, runtime_value); - return Ok(()); - } else { - return Err(EnvError::AssignToImmutable(ident.to_string())); - } - } - - let mut current_parent = self.parent.as_ref().and_then(|p| p.upgrade()); - - while let Some(parent_cell) = current_parent { - let has_key; - let is_mutable; - let next_parent; - { - let parent_env = borrow_env!(parent_cell); - has_key = parent_env.context.contains_key(ident); - is_mutable = has_key && parent_env.mutable_vars.as_ref().is_some_and(|s| s.contains(&ident)); - next_parent = parent_env.parent.as_ref().and_then(|p| p.upgrade()); - } - - if has_key { - if is_mutable { - borrow_env_mut!(parent_cell).context.upsert(ident, runtime_value); - return Ok(()); - } else { - return Err(EnvError::AssignToImmutable(ident.to_string())); - } - } - current_parent = next_parent; - } - - Err(EnvError::UndefinedVariable(ident.to_string())) - } - - /// Checks if a variable is mutable - pub fn is_mutable(&self, ident: Ident) -> bool { - if self.context.contains_key(ident) { - return self.mutable_vars.as_ref().is_some_and(|s| s.contains(&ident)); - } - - let mut current_parent = self.parent.as_ref().and_then(|p| p.upgrade()); - - while let Some(parent_cell) = current_parent { - let parent_env = borrow_env!(parent_cell); - - if parent_env.context.contains_key(ident) { - return parent_env.mutable_vars.as_ref().is_some_and(|s| s.contains(&ident)); - } - current_parent = parent_env.parent.as_ref().and_then(|p| p.upgrade()); - } - - false - } - - #[cfg(feature = "debugger")] - /// Returns a vector of local variables in the current environment. - pub fn get_local_variables(&self) -> Vec { - match self.parent { - None => vec![], - Some(_) => self - .context - .iter_entries() - .map(|(ident, value)| Variable::from(ident, value)) - .collect(), - } - } - - #[cfg(feature = "debugger")] - /// Returns a vector of global variables in the current environment. - pub fn get_global_variables(&self) -> Vec { - match &self.parent { - None => self - .context - .iter_entries() - .filter_map(|(ident, value)| { - if value.is_function() || value.is_native_function() { - None - } else { - Some(Variable::from(ident, value)) - } - }) - .collect(), - Some(parent_weak) => { - if let Some(parent_env) = parent_weak.upgrade() { - let parent_ref = borrow_env!(parent_env); - parent_ref.get_global_variables() - } else { - self.context - .iter_entries() - .filter_map(|(ident, value)| { - if value.is_function() || value.is_native_function() { - None - } else { - Some(Variable::from(ident, value)) - } - }) - .collect() - } - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::Shared; - use proptest::prelude::*; - use rstest::rstest; - - use super::*; - - fn num(n: f64) -> RuntimeValue { - RuntimeValue::Number(n.into()) - } - - fn child(parent: &Shared>) -> Env { - Env::with_parent(Shared::downgrade(parent)) - } - - fn define_n_unique(env: &mut Env, n: usize) { - for i in 0..n { - env.define(Ident::new(&format!("v{i}")), num(i as f64)); - } - } - - fn make_parent() -> Shared> { - Shared::new(SharedCell::new(Env::default())) - } - - #[test] - fn child_scope_starts_as_small() { - let p = make_parent(); - assert!(matches!(child(&p).context, EnvContext::Small(_))); - } - - #[test] - fn global_scope_starts_as_large() { - assert!(matches!(Env::default().context, EnvContext::Large(_))); - } - - #[rstest] - #[case(1)] - #[case(PROMOTE_THRESHOLD - 1)] - #[case(PROMOTE_THRESHOLD)] - #[case(PROMOTE_THRESHOLD + 3)] - fn define_n_keys_all_resolve_correctly(#[case] n: usize) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - for i in 0..n { - assert_eq!(env.resolve(Ident::new(&format!("v{i}"))).unwrap(), num(i as f64)); - } - } - - #[rstest] - #[case(1)] - #[case(PROMOTE_THRESHOLD - 1)] - #[case(PROMOTE_THRESHOLD + 1)] - fn rebinding_same_key_keeps_len_1(#[case] rebinds: usize) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - for i in 0..rebinds { - env.define(x, num(i as f64)); - } - assert_eq!(env.context.len(), 1); - assert_eq!(env.resolve(x).unwrap(), num((rebinds - 1) as f64)); - } - - #[test] - fn stays_small_below_threshold() { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, PROMOTE_THRESHOLD - 1); - assert!(matches!(env.context, EnvContext::Small(_))); - } - - #[rstest] - #[case(PROMOTE_THRESHOLD)] - #[case(PROMOTE_THRESHOLD + 1)] - #[case(PROMOTE_THRESHOLD + 10)] - fn promotes_at_or_above_threshold(#[case] n: usize) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - assert!(matches!(env.context, EnvContext::Large(_))); - } - - #[rstest] - #[case(PROMOTE_THRESHOLD)] - #[case(PROMOTE_THRESHOLD + 5)] - fn promotion_preserves_all_values(#[case] n: usize) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - assert!(matches!(env.context, EnvContext::Large(_))); - for i in 0..n { - assert_eq!(env.resolve(Ident::new(&format!("v{i}"))).unwrap(), num(i as f64)); - } - } - - #[test] - fn upsert_in_promoted_scope_does_not_grow() { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, PROMOTE_THRESHOLD); - assert!(matches!(env.context, EnvContext::Large(_))); - let len_before = env.context.len(); - env.define(Ident::new("v0"), num(999.0)); // already exists - assert_eq!(env.context.len(), len_before); - assert_eq!(env.resolve(Ident::new("v0")).unwrap(), num(999.0)); - } - - #[rstest] - #[case(false)] // child stays Small - #[case(true)] // child promoted to Large - fn child_finds_parent_value(#[case] promote_child: bool) { - let p = make_parent(); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define(Ident::new("pg"), num(77.0)); - #[cfg(feature = "sync")] - p.write().unwrap().define(Ident::new("pg"), num(77.0)); - } - let mut env = child(&p); - if promote_child { - define_n_unique(&mut env, PROMOTE_THRESHOLD); - } - assert_eq!(env.resolve(Ident::new("pg")).unwrap(), num(77.0)); - } - - #[test] - fn child_does_not_see_siblings_variable() { - let p = make_parent(); - let mut sibling = child(&p); - sibling.define(Ident::new("sib"), num(1.0)); - - let env = child(&p); - assert!(env.resolve(Ident::new("sib")).is_err()); - } - - #[test] - fn three_level_scope_chain() { - let gp = make_parent(); - { - #[cfg(not(feature = "sync"))] - gp.borrow_mut().define(Ident::new("g"), num(1.0)); - #[cfg(feature = "sync")] - gp.write().unwrap().define(Ident::new("g"), num(1.0)); - } - let p = Shared::new(SharedCell::new(child(&gp))); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define(Ident::new("p"), num(2.0)); - #[cfg(feature = "sync")] - p.write().unwrap().define(Ident::new("p"), num(2.0)); - } - let mut env = child(&p); - env.define(Ident::new("c"), num(3.0)); - - assert_eq!(env.resolve(Ident::new("c")).unwrap(), num(3.0)); - assert_eq!(env.resolve(Ident::new("p")).unwrap(), num(2.0)); - assert_eq!(env.resolve(Ident::new("g")).unwrap(), num(1.0)); - } - - #[test] - fn child_shadows_parent_variable() { - let p = make_parent(); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define(Ident::new("x"), num(1.0)); - #[cfg(feature = "sync")] - p.write().unwrap().define(Ident::new("x"), num(1.0)); - } - let mut env = child(&p); - env.define(Ident::new("x"), num(2.0)); - assert_eq!(env.resolve(Ident::new("x")).unwrap(), num(2.0)); - } - - #[test] - fn undefined_var_is_error() { - let p = make_parent(); - assert!(child(&p).resolve(Ident::new("nope")).is_err()); - } - - #[rstest] - #[case(false)] // still Small - #[case(true)] // after promotion to Large - fn mutable_assign_works(#[case] promote: bool) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - env.define_mutable(x, num(10.0)); - if promote { - define_n_unique(&mut env, PROMOTE_THRESHOLD); - } - env.assign(x, num(20.0)).unwrap(); - assert_eq!(env.resolve(x).unwrap(), num(20.0)); - } - - #[rstest] - #[case(false)] - #[case(true)] - fn immutable_assign_is_error(#[case] promote: bool) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - env.define(x, num(1.0)); - if promote { - define_n_unique(&mut env, PROMOTE_THRESHOLD); - } - assert!(env.assign(x, num(2.0)).is_err()); - } - - #[test] - fn assign_walks_to_parent() { - let p = make_parent(); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define_mutable(Ident::new("cnt"), num(0.0)); - #[cfg(feature = "sync")] - p.write().unwrap().define_mutable(Ident::new("cnt"), num(0.0)); - } - let mut env = child(&p); - env.assign(Ident::new("cnt"), num(42.0)).unwrap(); - - #[cfg(not(feature = "sync"))] - let val = p.borrow().resolve(Ident::new("cnt")).unwrap(); - #[cfg(feature = "sync")] - let val = p.read().unwrap().resolve(Ident::new("cnt")).unwrap(); - - assert_eq!(val, num(42.0)); - } - - #[rstest] - #[case("x", true)] - #[case("y", false)] - fn is_mutable_matches_definition(#[case] name: &str, #[case] mutable: bool) { - let p = make_parent(); - let mut env = child(&p); - let id = Ident::new(name); - if mutable { - env.define_mutable(id, num(0.0)); - } else { - env.define(id, num(0.0)); - } - assert_eq!(env.is_mutable(id), mutable); - } - - #[rstest] - #[case(10)] - #[case(100)] - #[case(1000)] - fn foreach_rebind_keeps_len_1(#[case] iters: usize) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - for i in 0..iters { - env.define(x, num(i as f64)); - } - assert_eq!(env.context.len(), 1); - assert_eq!(env.resolve(x).unwrap(), num((iters - 1) as f64)); - } - - #[test] - fn foreach_with_multiple_lets_promotes_and_stays_correct() { - // Simulate: foreach(i, ...): let a=i | let b=a+1 | ... | a+b+c+d+e - let p = make_parent(); - let mut env = child(&p); - let names = ["i", "a", "b", "c", "d", "e"]; - let ids: Vec = names.iter().map(|n| Ident::new(n)).collect(); - - for iter in 0..10usize { - // rebind loop var - env.define(ids[0], num(iter as f64)); - // bind let vars (upsert: first time new, subsequent iterations update) - for (j, id) in ids[1..].iter().enumerate() { - env.define(*id, num((iter + j) as f64)); - } - } - - // After promotion (6 unique vars >= threshold if threshold ≤ 6) - // All values must reflect the last iteration (iter=9) - assert_eq!(env.resolve(ids[0]).unwrap(), num(9.0)); - assert_eq!(env.context.len(), names.len()); - } - - proptest! { - #[test] - fn prop_rebind_keeps_len_1(iters in 1usize..=200) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - for i in 0..iters { - env.define(x, num(i as f64)); - } - prop_assert_eq!(env.context.len(), 1); - prop_assert_eq!(env.resolve(x).unwrap(), num((iters - 1) as f64)); - } - - #[test] - fn prop_unique_keys_len_equals_n(n in 1usize..=30) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - prop_assert_eq!(env.context.len(), n); - } - - #[test] - fn prop_all_values_accessible_after_n_defines(n in 1usize..=30) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - for i in 0..n { - let val = env.resolve(Ident::new(&format!("v{i}"))).unwrap(); - prop_assert_eq!(val, num(i as f64)); - } - } - - #[test] - fn prop_promotion_happens_iff_n_ge_threshold(n in 0usize..=30) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - let is_large = matches!(env.context, EnvContext::Large(_)); - prop_assert_eq!(is_large, n >= PROMOTE_THRESHOLD); - } - - #[test] - fn prop_latest_value_always_returned(updates in 2usize..=50) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - for i in 0..updates { - env.define(x, num(i as f64)); - // After each update, resolve must return the latest value - prop_assert_eq!(env.resolve(x).unwrap(), num(i as f64)); - } - } - - #[test] - fn prop_parent_value_always_accessible( - child_vars in 0usize..=20, - parent_val in 0.0f64..1000.0 - ) { - let p = make_parent(); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define(Ident::new("pv"), num(parent_val)); - #[cfg(feature = "sync")] - p.write().unwrap().define(Ident::new("pv"), num(parent_val)); - } - let mut env = child(&p); - define_n_unique(&mut env, child_vars); - // Regardless of how many child vars exist (including after promotion), - // the parent value must remain accessible. - let resolved = env.resolve(Ident::new("pv")).unwrap(); - prop_assert_eq!(resolved, num(parent_val)); - } - - #[test] - fn prop_mutable_assign_updates_correctly( - initial in 0.0f64..500.0, - updated in 500.0f64..1000.0, - extra_vars in 0usize..=15 - ) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - env.define_mutable(x, num(initial)); - define_n_unique(&mut env, extra_vars); - env.assign(x, num(updated)).unwrap(); - prop_assert_eq!(env.resolve(x).unwrap(), num(updated)); - } - - #[test] - fn prop_len_never_exceeds_unique_key_count( - rebinds in 1usize..=100, - extra_keys in 0usize..=10 - ) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - for i in 0..rebinds { - env.define(x, num(i as f64)); - } - define_n_unique(&mut env, extra_keys); - // len must equal 1 (for x) + extra_keys unique vars - prop_assert_eq!(env.context.len(), 1 + extra_keys); - } - - #[test] - fn prop_contains_key_consistent_with_resolve(n in 1usize..=30, query in 0usize..=35) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - let key = Ident::new(&format!("v{query}")); - let contains = env.context.contains_key(key); - let resolved = env.resolve(key); - // contains_key iff resolve succeeds - prop_assert_eq!(contains, resolved.is_ok()); - } - - #[test] - fn prop_scope_isolation(n in 1usize..=20) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, n); - // Parent must not see child's variables - for i in 0..n { - #[cfg(not(feature = "sync"))] - let result = p.borrow().resolve(Ident::new(&format!("v{i}"))); - #[cfg(feature = "sync")] - let result = p.read().unwrap().resolve(Ident::new(&format!("v{i}"))); - prop_assert!(result.is_err()); - } - } - - #[test] - fn prop_assign_undefined_is_error(extra in 0usize..=15) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, extra); - let result = env.assign(Ident::new("does_not_exist"), num(1.0)); - prop_assert!(result.is_err()); - } - - #[test] - fn prop_global_scope_rebind_keeps_len_1(rebinds in 1usize..=200) { - let mut env = Env::default(); - let x = Ident::new("x"); - for i in 0..rebinds { - env.define(x, num(i as f64)); - } - prop_assert_eq!(env.context.len(), 1); - prop_assert_eq!(env.resolve(x).unwrap(), num((rebinds - 1) as f64)); - } - - /// `is_mutable` predicts whether `assign` will succeed or return `AssignToImmutable`. - #[test] - fn prop_is_mutable_consistent_with_assign( - extra in 0usize..=15, - is_mutable in any::() - ) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("target"); - if is_mutable { - env.define_mutable(x, num(0.0)); - } else { - env.define(x, num(0.0)); - } - define_n_unique(&mut env, extra); - let result = env.assign(x, num(1.0)); - if is_mutable { - prop_assert!(result.is_ok(), "mutable var must accept assign"); - } else { - prop_assert_eq!(result.unwrap_err(), EnvError::AssignToImmutable("target".to_string())); - } - } - - /// Defining x in a child scope must not change the parent's binding of x. - #[test] - fn prop_shadow_preserves_parent_value( - parent_val in 0.0f64..500.0, - child_val in 500.0f64..1000.0, - extra in 0usize..=15 - ) { - let p = make_parent(); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define(Ident::new("x"), num(parent_val)); - #[cfg(feature = "sync")] - p.write().unwrap().define(Ident::new("x"), num(parent_val)); - } - let mut env = child(&p); - env.define(Ident::new("x"), num(child_val)); - define_n_unique(&mut env, extra); - // Parent's x must be unchanged - #[cfg(not(feature = "sync"))] - let pv = p.borrow().resolve(Ident::new("x")).unwrap(); - #[cfg(feature = "sync")] - let pv = p.read().unwrap().resolve(Ident::new("x")).unwrap(); - prop_assert_eq!(pv, num(parent_val)); - // Child's x must be the shadowed value - prop_assert_eq!(env.resolve(Ident::new("x")).unwrap(), num(child_val)); - } - - /// After promotion, rebinding an existing key must not increase len. - #[test] - fn prop_promotion_then_rebind_does_not_grow( - rebinds in 1usize..=50 - ) { - let p = make_parent(); - let mut env = child(&p); - // Trigger promotion - define_n_unique(&mut env, PROMOTE_THRESHOLD); - prop_assert!(matches!(env.context, EnvContext::Large(_))); - let len_at_promotion = env.context.len(); - // Rebind all existing keys multiple times - for _ in 0..rebinds { - for i in 0..PROMOTE_THRESHOLD { - env.define(Ident::new(&format!("v{i}")), num(i as f64 + 1.0)); - } - } - prop_assert_eq!(env.context.len(), len_at_promotion); - } - - /// After assign to a parent mutable var, the child can resolve the new value. - #[test] - fn prop_assign_to_parent_visible_from_child( - initial in 0.0f64..500.0, - updated in 500.0f64..1000.0, - child_extra in 0usize..=10 - ) { - let p = make_parent(); - { - #[cfg(not(feature = "sync"))] - p.borrow_mut().define_mutable(Ident::new("shared"), num(initial)); - #[cfg(feature = "sync")] - p.write().unwrap().define_mutable(Ident::new("shared"), num(initial)); - } - let mut env = child(&p); - define_n_unique(&mut env, child_extra); - // Assign via child walks up and updates parent - env.assign(Ident::new("shared"), num(updated)).unwrap(); - // Child resolve must see the updated value - prop_assert_eq!(env.resolve(Ident::new("shared")).unwrap(), num(updated)); - } - - /// Mixed sequence of rebinds and new-key inserts: len always equals the count - /// of distinct keys that have been defined. - #[test] - fn prop_mixed_operations_len_equals_unique_keys( - unique_keys in 1usize..=20, - rebind_rounds in 0usize..=5 - ) { - let p = make_parent(); - let mut env = child(&p); - // First pass: all unique keys - for i in 0..unique_keys { - env.define(Ident::new(&format!("k{i}")), num(i as f64)); - } - prop_assert_eq!(env.context.len(), unique_keys); - // Additional rebind rounds must not change len - for round in 0..rebind_rounds { - for i in 0..unique_keys { - env.define(Ident::new(&format!("k{i}")), num((round * 100 + i) as f64)); - } - prop_assert_eq!(env.context.len(), unique_keys, - "len must stay at {} after rebind round {}", unique_keys, round); - } - } - - /// If a key is undefined in the entire chain, `assign` returns `UndefinedVariable`, - /// never `AssignToImmutable`. - #[test] - fn prop_undefined_assign_error_kind(extra in 0usize..=10) { - let p = make_parent(); - let mut env = child(&p); - define_n_unique(&mut env, extra); - let err = env.assign(Ident::new("ghost"), num(1.0)).unwrap_err(); - prop_assert_eq!(err, EnvError::UndefinedVariable("ghost".to_string())); - } - - /// `contains_key` returns true for every key inserted, regardless of how many - /// rebinds or whether promotion occurred. - #[test] - fn prop_contains_key_after_rebind_and_promotion( - unique in 1usize..=25, - rebinds in 0usize..=10 - ) { - let p = make_parent(); - let mut env = child(&p); - for i in 0..unique { - env.define(Ident::new(&format!("k{i}")), num(i as f64)); - } - for r in 0..rebinds { - for i in 0..unique { - env.define(Ident::new(&format!("k{i}")), num((r * unique + i) as f64)); - } - } - for i in 0..unique { - let key = Ident::new(&format!("k{i}")); - prop_assert!(env.context.contains_key(key)); - } - } - } - - #[rstest] - #[case(false, false)] - #[case(true, false)] - #[case(false, true)] - #[case(true, true)] - fn is_mutable_correct_after_optional_promotion(#[case] is_mutable: bool, #[case] promote: bool) { - let p = make_parent(); - let mut env = child(&p); - let x = Ident::new("x"); - if is_mutable { - env.define_mutable(x, num(0.0)); - } else { - env.define(x, num(0.0)); - } - if promote { - define_n_unique(&mut env, PROMOTE_THRESHOLD); - } - assert_eq!(env.is_mutable(x), is_mutable); - } - - #[rstest] - #[case(EnvError::UndefinedVariable("no_such".to_string()))] - fn assign_undefined_var_returns_error(#[case] expected: EnvError) { - let p = make_parent(); - let mut env = child(&p); - let err = env.assign(Ident::new("no_such"), num(1.0)).unwrap_err(); - assert_eq!(err, expected); - } - - #[test] - fn cross_variant_partial_eq_is_false() { - let p = make_parent(); - let mut small_env = child(&p); - small_env.define(Ident::new("x"), num(1.0)); - assert!(matches!(small_env.context, EnvContext::Small(_))); - - let mut large_env = Env::default(); // starts as Large - large_env.define(Ident::new("x"), num(1.0)); - assert!(matches!(large_env.context, EnvContext::Large(_))); - - assert_ne!(small_env.context, large_env.context); - } - - #[test] - fn global_scope_rebind_keeps_len_1() { - let mut env = Env::default(); - let x = Ident::new("x"); - for i in 0..100 { - env.define(x, num(i as f64)); - } - assert_eq!(env.context.len(), 1); - assert_eq!(env.resolve(x).unwrap(), num(99.0)); - } - - #[test] - fn assign_unknown_in_chain_is_undefined_error() { - let gp = make_parent(); - let p = Shared::new(SharedCell::new(child(&gp))); - let mut env = child(&p); - let err = env.assign(Ident::new("ghost"), num(1.0)).unwrap_err(); - assert_eq!(err, EnvError::UndefinedVariable("ghost".to_string())); - } - - #[test] - fn test_env_define_and_resolve() { - let mut env = Env::default(); - let ident = Ident::new("x"); - let value = RuntimeValue::Number(42.0.into()); - env.define(ident, value.clone()); - assert_eq!(env.resolve(ident).unwrap(), value); - } - - #[test] - fn test_env_resolve_from_parent() { - let parent_env = make_parent(); - let mut child_env = child(&parent_env); - - let parent_ident = Ident::new("parent_var"); - let parent_value = num(100.0); - - #[cfg(not(feature = "sync"))] - parent_env.borrow_mut().define(parent_ident, parent_value.clone()); - #[cfg(feature = "sync")] - parent_env.write().unwrap().define(parent_ident, parent_value.clone()); - - child_env.define(Ident::new("child_var"), num(200.0)); - - assert_eq!(child_env.resolve(Ident::new("child_var")).unwrap(), num(200.0)); - assert_eq!(child_env.resolve(parent_ident).unwrap(), parent_value); - #[cfg(not(feature = "sync"))] - assert!(parent_env.borrow().resolve(Ident::new("child_var")).is_err()); - #[cfg(feature = "sync")] - assert!(parent_env.read().unwrap().resolve(Ident::new("child_var")).is_err()); - } - - #[cfg(feature = "debugger")] - #[rstest] - #[case( - vec![("a", RuntimeValue::Number(1.0.into())), ("b", RuntimeValue::Boolean(true))], - vec![ - Variable { name: "a".to_string(), value: "1".to_string(), type_field: "number".to_string() }, - Variable { name: "b".to_string(), value: "true".to_string(), type_field: "bool".to_string() } - ] - )] - #[case( - vec![("x", RuntimeValue::String(Shared::new("hello".into()))), ("y", RuntimeValue::None)], - vec![ - Variable { name: "x".to_string(), value: "hello".to_string(), type_field: "string".to_string() }, - Variable { name: "y".to_string(), value: "None".to_string(), type_field: "none".to_string() } - ] - )] - fn test_variable_from_and_display(#[case] vars: Vec<(&str, RuntimeValue)>, #[case] expected: Vec) { - for (i, (name, value)) in vars.iter().enumerate() { - let ident = Ident::new(name); - let var = Variable::from(ident, value); - assert_eq!(var, expected[i]); - let display = format!("{}", var); - assert!(display.contains(&var.name)); - } - } - - #[rstest] - #[case("mutable_var", Some(true), None, None, true)] - #[case("immutable_var", Some(false), None, None, false)] - #[case("non_existent", None, None, None, false)] - #[case("var", None, Some(true), None, true)] - #[case("var", None, Some(false), None, false)] - #[case("var", Some(false), Some(true), None, false)] - #[case("var", None, None, Some(true), true)] - fn test_is_mutable( - #[case] var_name: &str, - #[case] define_in_current: Option, - #[case] define_in_parent: Option, - #[case] define_in_grandparent: Option, - #[case] expected_mutable: bool, - ) { - let grandparent_env = define_in_grandparent.map(|_| make_parent()); - - let parent_env = if define_in_parent.is_some() || grandparent_env.is_some() { - if let Some(ref gp) = grandparent_env { - Some(Shared::new(SharedCell::new(child(gp)))) - } else { - Some(make_parent()) - } - } else { - None - }; - - let mut env = if let Some(ref parent) = parent_env { - child(parent) - } else { - Env::default() - }; - - let var = Ident::new(var_name); - - if let Some(is_mutable) = define_in_grandparent - && let Some(ref gp) = grandparent_env - { - #[cfg(not(feature = "sync"))] - if is_mutable { - gp.borrow_mut().define_mutable(var, num(1.0)); - } else { - gp.borrow_mut().define(var, num(1.0)); - } - #[cfg(feature = "sync")] - if is_mutable { - gp.write().unwrap().define_mutable(var, num(1.0)); - } else { - gp.write().unwrap().define(var, num(1.0)); - } - } - - if let Some(is_mutable) = define_in_parent - && let Some(ref parent) = parent_env - { - #[cfg(not(feature = "sync"))] - if is_mutable { - parent.borrow_mut().define_mutable(var, num(100.0)); - } else { - parent.borrow_mut().define(var, num(100.0)); - } - #[cfg(feature = "sync")] - if is_mutable { - parent.write().unwrap().define_mutable(var, num(100.0)); - } else { - parent.write().unwrap().define(var, num(100.0)); - } - } - - if let Some(is_mutable) = define_in_current { - if is_mutable { - env.define_mutable(var, num(200.0)); - } else { - env.define(var, num(200.0)); - } - } - - assert_eq!(env.is_mutable(var), expected_mutable); - } -} diff --git a/crates/mq-lang/src/runtime/runtime_value.rs b/crates/mq-lang/src/runtime/runtime_value.rs index b6c0805f1..89818adc2 100644 --- a/crates/mq-lang/src/runtime/runtime_value.rs +++ b/crates/mq-lang/src/runtime/runtime_value.rs @@ -1,16 +1,16 @@ -#[cfg(not(feature = "tarn"))] -use super::env::Env; -#[cfg(not(feature = "tarn"))] -use crate::{AstParams, Program, SharedCell}; use crate::{Ident, Shared, number::Number}; +use indexmap::IndexMap; use mq_markdown::Node; +use rustc_hash::FxBuildHasher; use std::{ borrow::Cow, cmp::Ordering, - collections::BTreeMap, ops::{Index, IndexMut}, }; +/// The backing map for [`RuntimeValue::Dict`]: insertion-ordered, `FxHash`-based. +pub type DictMap = IndexMap; + /// Runtime selector for indexing into markdown nodes. #[derive(Debug, Clone, Copy, PartialEq)] pub enum Selector { @@ -37,77 +37,6 @@ impl Selector { } } -/// Represents a module's runtime environment with its exports. -#[derive(Clone, Debug)] -#[cfg(not(feature = "tarn"))] -pub struct ModuleEnv { - name: Ident, - exports: Shared>, -} - -#[cfg(not(feature = "tarn"))] -impl ModuleEnv { - /// Creates a new module environment with the given name and exports. - pub fn new(name: &str, exports: Shared>) -> Self { - Self { - name: Ident::new(name), - exports, - } - } - - /// Returns the name of the module. - pub fn name(&self) -> String { - self.name.as_str() - } - - /// Returns a reference to the module's exports environment. - pub fn exports(&self) -> &Shared> { - &self.exports - } - - /// Returns the number of exports in this module. - pub fn len(&self) -> usize { - #[cfg(not(feature = "sync"))] - { - self.exports.borrow().len() - } - - #[cfg(feature = "sync")] - { - self.exports.read().unwrap().len() - } - } -} - -#[cfg(not(feature = "tarn"))] -impl PartialEq for ModuleEnv { - fn eq(&self, other: &Self) -> bool { - #[cfg(not(feature = "sync"))] - let exports = self.exports().borrow(); - #[cfg(feature = "sync")] - let exports = self.exports().read().unwrap(); - - #[cfg(not(feature = "sync"))] - let other_exports = other.exports().borrow(); - #[cfg(feature = "sync")] - let other_exports = other.exports().read().unwrap(); - - self.name == other.name && std::ptr::eq(&*exports, &*other_exports) - } -} - -/// A user-defined function's parameters, body, and captured environment. -/// -/// Held behind a single [`Shared`] in [`RuntimeValue::Function`] so cloning a function -/// value is one refcount bump rather than three. -#[derive(Debug, Clone)] -#[cfg(not(feature = "tarn"))] -pub(crate) struct FunctionValue { - pub(crate) params: Shared, - pub(crate) body: Shared, - pub(crate) env: Shared>, -} - /// A value in the mq runtime. /// /// This enum represents all possible value types that can exist during @@ -134,35 +63,20 @@ pub enum RuntimeValue { /// /// Same clone-on-write scheme as [`RuntimeValue::Array`]; see [`markdown_mut`]. Markdown(Shared, Option), - /// A user-defined function with parameters, body (program), and captured environment. - /// - /// `Shared`-wrapped as a whole, not per-field, for the same reason as [`VmClosure`]: - /// cloning a `Function` value (e.g. on every `Env` lookup) is one O(1) refcount bump - /// instead of three. - /// - /// [`VmClosure`]: RuntimeValue::VmClosure - #[cfg(not(feature = "tarn"))] - #[allow(private_interfaces)] - Function(Shared), /// A built-in native function identified by name. NativeFunction(Ident), /// A VM closure that has crossed into plain-value territory (stored in an array/dict, - /// passed to `partial`, ...) — see `tarn::value::VmClosureValue`. Only exists behind the - /// `tarn` feature; the tree-walker never creates one. `VmClosureValue` is deliberately - /// `pub(crate)` — this variant is constructible only from within the crate. + /// passed to `partial`, ...) — see `tarn::value::VmClosureValue`. `VmClosureValue` is + /// deliberately `pub(crate)` — this variant is constructible only from within the crate. /// /// `Shared`-wrapped, not inline: `VmClosureValue` is 64 bytes (chunks/upvalues/bound_args), /// which would otherwise force every `RuntimeValue` variant to that size. - #[cfg(feature = "tarn")] #[allow(private_interfaces)] VmClosure(Shared), /// A dictionary mapping identifiers to runtime values. /// /// Same clone-on-write scheme as [`RuntimeValue::Array`]; see [`dict_mut`]. - Dict(Shared>), - /// A module with its exports. - #[cfg(not(feature = "tarn"))] - Module(Shared), + Dict(Shared), /// Raw binary data (e.g. CBOR byte strings). /// /// Same clone-on-write scheme as [`RuntimeValue::Array`]; see [`bytes_mut`]. @@ -172,7 +86,6 @@ pub enum RuntimeValue { None, } -// Custom PartialEq implementation to avoid comparing Env pointers impl PartialEq for RuntimeValue { fn eq(&self, other: &Self) -> bool { match (self, other) { @@ -182,17 +95,11 @@ impl PartialEq for RuntimeValue { (RuntimeValue::Symbol(a), RuntimeValue::Symbol(b)) => a == b, (RuntimeValue::Array(a), RuntimeValue::Array(b)) => a == b, (RuntimeValue::Markdown(a, sa), RuntimeValue::Markdown(b, sb)) => a == b && sa == sb, - #[cfg(not(feature = "tarn"))] - (RuntimeValue::Function(a), RuntimeValue::Function(b)) => a.params == b.params && a.body == b.body, - // Mirrors `Function` above: same compiled body, same bound args, upvalues ignored. - #[cfg(feature = "tarn")] (RuntimeValue::VmClosure(a), RuntimeValue::VmClosure(b)) => { Shared::ptr_eq(&a.chunks, &b.chunks) && a.chunk_index == b.chunk_index && a.bound_args == b.bound_args } (RuntimeValue::NativeFunction(a), RuntimeValue::NativeFunction(b)) => a == b, (RuntimeValue::Dict(a), RuntimeValue::Dict(b)) => a == b, - #[cfg(not(feature = "tarn"))] - (RuntimeValue::Module(a), RuntimeValue::Module(b)) => a == b, (RuntimeValue::Bytes(a), RuntimeValue::Bytes(b)) => a == b, (RuntimeValue::None, RuntimeValue::None) => true, _ => false, @@ -272,8 +179,8 @@ impl From> for RuntimeValue { } } -impl From> for RuntimeValue { - fn from(map: BTreeMap) -> Self { +impl From for RuntimeValue { + fn from(map: DictMap) -> Self { RuntimeValue::Dict(Shared::new(map)) } } @@ -283,7 +190,7 @@ impl From> for RuntimeValue { RuntimeValue::Dict(Shared::new( v.into_iter() .map(|(k, v)| (Ident::new(&k), RuntimeValue::Number(v))) - .collect::>(), + .collect::(), )) } } @@ -318,7 +225,7 @@ impl From for RuntimeValue { RuntimeValue::Array(Shared::new(arr.into_iter().map(RuntimeValue::from).collect())) } yaml_rust2::Yaml::Hash(map) => { - let mut btree = BTreeMap::new(); + let mut btree = DictMap::default(); for (k, v) in map { let key = match k { yaml_rust2::Yaml::String(s) => s, @@ -350,7 +257,7 @@ impl From for RuntimeValue { RuntimeValue::Array(Shared::new(arr.into_iter().map(RuntimeValue::from).collect())) } serde_json::Value::Object(obj) => { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); for (k, v) in obj { map.insert(Ident::new(&k), RuntimeValue::from(v)); } @@ -377,7 +284,7 @@ impl From for RuntimeValue { RuntimeValue::Array(Shared::new(items)) } ciborium::Value::Map(pairs) => { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); for (k, v) in pairs { let key = match k { ciborium::Value::Text(s) => Ident::new(&s), @@ -406,18 +313,9 @@ impl PartialOrd for RuntimeValue { let b = b.to_string(); a.to_string().partial_cmp(&b) } - #[cfg(not(feature = "tarn"))] - (RuntimeValue::Function(a), RuntimeValue::Function(b)) => match a.params.partial_cmp(&b.params) { - Some(Ordering::Equal) => a.body.partial_cmp(&b.body), - Some(Ordering::Greater) => Some(Ordering::Greater), - Some(Ordering::Less) => Some(Ordering::Less), - _ => None, - }, (RuntimeValue::Bytes(a), RuntimeValue::Bytes(b)) => a.partial_cmp(b), (RuntimeValue::Dict(_), _) => None, (_, RuntimeValue::Dict(_)) => None, - #[cfg(not(feature = "tarn"))] - (RuntimeValue::Module(a), RuntimeValue::Module(b)) => a.name.partial_cmp(&b.name), _ => None, } } @@ -433,16 +331,9 @@ impl std::fmt::Display for RuntimeValue { Self::Array(_) => self.string(), Self::Markdown(m, ..) => Cow::Owned(m.to_string()), Self::None => Cow::Borrowed(""), - #[cfg(not(feature = "tarn"))] - #[cfg(not(feature = "tarn"))] - #[cfg(not(feature = "tarn"))] - Self::Function(f) => Cow::Owned(format!("function/{}", f.params.len())), Self::NativeFunction(_) => Cow::Borrowed("native_function"), - #[cfg(feature = "tarn")] Self::VmClosure(_) => Cow::Borrowed("function"), Self::Dict(_) => self.string(), - #[cfg(not(feature = "tarn"))] - Self::Module(module_name) => Cow::Owned(format!(r#"module "{}""#, module_name.name)), Self::Bytes(b) => Cow::Owned(bytes_to_hex(b)), }; write!(f, "{}", value) @@ -481,8 +372,8 @@ pub(crate) fn array_mut(array: &mut Shared>) -> &mut Vec>) -> &mut BTreeMap { - Shared::>::make_mut(map) +pub(crate) fn dict_mut(map: &mut Shared) -> &mut DictMap { + Shared::::make_mut(map) } /// Clone-on-write access to a markdown node; see [`array_mut`]. @@ -522,7 +413,7 @@ impl RuntimeValue { /// Creates a new empty dictionary. #[inline(always)] pub fn new_dict() -> RuntimeValue { - RuntimeValue::Dict(Shared::new(BTreeMap::new())) + RuntimeValue::Dict(Shared::new(DictMap::default())) } /// Creates a new markdown runtime value from the given node. @@ -530,17 +421,6 @@ impl RuntimeValue { RuntimeValue::Markdown(Shared::new(node), None) } - /// Creates a new tree-walker function value from its params, body, and environment. - #[cfg(not(feature = "tarn"))] - #[inline(always)] - pub(crate) fn new_function( - params: Shared, - body: Shared, - env: Shared>, - ) -> RuntimeValue { - RuntimeValue::Function(Shared::new(FunctionValue { params, body, env })) - } - /// Returns the type name of this runtime value as a string. #[inline(always)] pub fn name(&self) -> &str { @@ -552,14 +432,9 @@ impl RuntimeValue { RuntimeValue::Markdown(_, _) => "markdown", RuntimeValue::Array(_) => "array", RuntimeValue::None => "None", - #[cfg(not(feature = "tarn"))] - RuntimeValue::Function(_) => "function", RuntimeValue::NativeFunction(_) => "native_function", - #[cfg(feature = "tarn")] RuntimeValue::VmClosure(_) => "function", RuntimeValue::Dict(_) => "dict", - #[cfg(not(feature = "tarn"))] - RuntimeValue::Module(_) => "module", RuntimeValue::Bytes(_) => "bytes", } } @@ -570,21 +445,10 @@ impl RuntimeValue { matches!(self, RuntimeValue::None) } - /// Returns `true` if this value is a user-defined function (tree-walker or VM). + /// Returns `true` if this value is a user-defined function. #[inline(always)] pub fn is_function(&self) -> bool { - #[cfg(feature = "tarn")] - if matches!(self, RuntimeValue::VmClosure(_)) { - return true; - } - #[cfg(not(feature = "tarn"))] - { - matches!(self, RuntimeValue::Function(_)) - } - #[cfg(feature = "tarn")] - { - false - } + matches!(self, RuntimeValue::VmClosure(_)) } /// Returns `true` if this value is a native (built-in) function. @@ -639,12 +503,7 @@ impl RuntimeValue { None => true, }, RuntimeValue::Symbol(_) | RuntimeValue::NativeFunction(_) | RuntimeValue::Dict(_) => true, - #[cfg(not(feature = "tarn"))] - RuntimeValue::Function(_) => true, - #[cfg(feature = "tarn")] RuntimeValue::VmClosure(_) => true, - #[cfg(not(feature = "tarn"))] - RuntimeValue::Module(_) => true, RuntimeValue::Bytes(b) => !b.is_empty(), RuntimeValue::None => false, } @@ -666,12 +525,7 @@ impl RuntimeValue { RuntimeValue::Dict(m) => m.len(), RuntimeValue::Bytes(b) => b.len(), RuntimeValue::None => 0, - #[cfg(not(feature = "tarn"))] - RuntimeValue::Function(..) => 0, - #[cfg(not(feature = "tarn"))] - RuntimeValue::Module(m) => m.len(), RuntimeValue::NativeFunction(..) => 0, - #[cfg(feature = "tarn")] RuntimeValue::VmClosure(..) => 0, } } @@ -694,10 +548,13 @@ impl RuntimeValue { #[inline(always)] pub fn update_markdown_value(&self, value: &str) -> RuntimeValue { match self { - RuntimeValue::Markdown(n, Some(sel)) => { - RuntimeValue::Markdown(Shared::new(n.with_children_value(value, sel.index_value())), Some(*sel)) + RuntimeValue::Markdown(n, Some(sel)) => RuntimeValue::Markdown( + Shared::new((**n).clone().into_with_children_value(value, sel.index_value())), + Some(*sel), + ), + RuntimeValue::Markdown(n, selector) => { + RuntimeValue::Markdown(Shared::new((**n).clone().into_with_value(value)), *selector) } - RuntimeValue::Markdown(n, selector) => RuntimeValue::Markdown(Shared::new(n.with_value(value)), *selector), _ => RuntimeValue::NONE, } } @@ -744,13 +601,8 @@ impl RuntimeValue { )), Self::Markdown(m, ..) => Cow::Owned(m.to_string()), Self::None => Cow::Borrowed(""), - #[cfg(not(feature = "tarn"))] - Self::Function(f) => Cow::Owned(format!("function/{}", f.params.len())), Self::NativeFunction(_) => Cow::Borrowed("native_function"), - #[cfg(feature = "tarn")] Self::VmClosure(_) => Cow::Borrowed("function"), - #[cfg(not(feature = "tarn"))] - Self::Module(m) => Cow::Owned(format!("module/{}", m.name())), Self::Bytes(b) => Cow::Owned(bytes_to_hex(b)), Self::Dict(map) => { let items = map @@ -909,9 +761,6 @@ impl RuntimeValues { if let RuntimeValue::Markdown(node, _) = ¤t_value { match &updated_value { RuntimeValue::None | RuntimeValue::NativeFunction(_) => current_value.clone(), - #[cfg(not(feature = "tarn"))] - RuntimeValue::Function(_) | RuntimeValue::Module(_) => current_value.clone(), - #[cfg(feature = "tarn")] RuntimeValue::VmClosure(_) => current_value.clone(), RuntimeValue::Markdown(node, _) if node.is_empty() => current_value.clone(), RuntimeValue::Markdown(node, _) => { @@ -947,7 +796,7 @@ impl RuntimeValues { )), RuntimeValue::Bytes(b) => RuntimeValue::new_markdown(node.with_value(bytes_to_hex(b).as_str())), RuntimeValue::Dict(map) => { - let mut new_dict = BTreeMap::new(); + let mut new_dict = DictMap::default(); for (k, v) in map.iter() { if !v.is_none() && !v.is_empty() { new_dict.insert( @@ -967,1094 +816,3 @@ impl RuntimeValues { .into() } } - -#[cfg(all(test, not(feature = "tarn")))] -mod tests { - use crate::ast::node::{IdentWithToken, Param}; - use rstest::rstest; - use smallvec::{SmallVec, smallvec}; - - use super::*; - - #[test] - fn test_runtime_value_from() { - assert_eq!(RuntimeValue::from(true), RuntimeValue::Boolean(true)); - assert_eq!(RuntimeValue::from(false), RuntimeValue::Boolean(false)); - assert_eq!( - RuntimeValue::from(String::from("test")), - RuntimeValue::String(Shared::new(String::from("test"))) - ); - assert_eq!( - RuntimeValue::from(Number::from(42.0)), - RuntimeValue::Number(Number::from(42.0)) - ); - } - - #[rstest] - #[case(RuntimeValue::Number(Number::from(42.0)), "42")] - #[case(RuntimeValue::Boolean(true), "true")] - #[case(RuntimeValue::Boolean(false), "false")] - #[case(RuntimeValue::String(Shared::new("hello".to_string())), r#""hello""#)] - #[case(RuntimeValue::None, "")] - #[case(RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(Number::from(1.0)), - RuntimeValue::String(Shared::new("test".to_string())) - ])), r#"[1, "test"]"#)] - #[case(RuntimeValue::Dict({ - let mut map = BTreeMap::new(); - map.insert(Ident::new("key1"), RuntimeValue::String(Shared::new("value1".to_string()))); - map.insert(Ident::new("key2"), RuntimeValue::Number(Number::from(42.0))); - Shared::new(map) - }), r#"{"key1": "value1", "key2": 42}"#)] - fn test_string_method(#[case] value: RuntimeValue, #[case] expected: &str) { - assert_eq!(value.string(), expected); - } - - #[test] - fn test_runtime_value_display() { - assert_eq!(format!("{}", RuntimeValue::Boolean(true)), "true"); - assert_eq!(format!("{}", RuntimeValue::Number(Number::from(42.0))), "42"); - assert_eq!( - format!("{}", RuntimeValue::String(Shared::new(String::from("test")))), - "test" - ); - assert_eq!(format!("{}", RuntimeValue::None), ""); - let map_val = RuntimeValue::Dict(Shared::new(BTreeMap::default())); - assert_eq!(format!("{}", map_val), "{}"); - } - - #[test] - fn test_runtime_value_debug() { - assert_eq!(format!("{:?}", RuntimeValue::Boolean(true)), "true"); - assert_eq!(format!("{:?}", RuntimeValue::Number(Number::from(42.0))), "42"); - assert_eq!( - format!("{:?}", RuntimeValue::String(Shared::new(String::from("test")))), - "\"test\"" - ); - assert_eq!(format!("{:?}", RuntimeValue::None), "None"); - - let mut map = BTreeMap::default(); - map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("MQ".to_string()))); - map.insert(Ident::new("version"), RuntimeValue::Number(Number::from(1.0))); - let map_val = RuntimeValue::Dict(Shared::new(map)); - let debug_str = format!("{:?}", map_val); - assert!(debug_str == r#"{"name": "MQ", "version": 1}"# || debug_str == r#"{"version": 1, "name": "MQ"}"#); - } - - #[test] - fn test_runtime_value_name() { - assert_eq!(RuntimeValue::Boolean(true).name(), "bool"); - assert_eq!(RuntimeValue::Number(Number::from(42.0)).name(), "number"); - assert_eq!(RuntimeValue::String(Shared::new(String::from("test"))).name(), "string"); - assert_eq!(RuntimeValue::None.name(), "None"); - assert_eq!( - RuntimeValue::new_function( - Shared::new(SmallVec::new()), - Shared::new(Vec::new()), - Shared::new(SharedCell::new(Env::default())) - ) - .name(), - "function" - ); - assert_eq!( - RuntimeValue::NativeFunction(Ident::new("name")).name(), - "native_function" - ); - assert_eq!( - RuntimeValue::Markdown( - Shared::new(mq_markdown::Node::Text(mq_markdown::Text { - value: "".to_string(), - position: None - })), - None - ) - .name(), - "markdown" - ); - assert_eq!(RuntimeValue::Dict(Shared::new(BTreeMap::default())).name(), "dict"); - } - - #[test] - fn test_runtime_value_is_true() { - assert!(RuntimeValue::Boolean(true).is_truthy()); - assert!(!RuntimeValue::Boolean(false).is_truthy()); - assert!(RuntimeValue::Number(Number::from(42.0)).is_truthy()); - assert!(!RuntimeValue::Number(Number::from(0.0)).is_truthy()); - assert!(RuntimeValue::String(Shared::new(String::from("test"))).is_truthy()); - assert!(!RuntimeValue::String(Shared::new(String::from(""))).is_truthy()); - assert!(RuntimeValue::Array(Shared::new(vec!["".to_string().into()])).is_truthy()); - assert!(!RuntimeValue::Array(Shared::new(Vec::new())).is_truthy()); - assert!( - RuntimeValue::Markdown( - Shared::new(mq_markdown::Node::Text(mq_markdown::Text { - value: "".to_string(), - position: None - })), - None - ) - .is_truthy() - ); - assert!( - !RuntimeValue::Markdown( - Shared::new(mq_markdown::Node::Text(mq_markdown::Text { - value: "".to_string(), - position: None - })), - Some(Selector::index(1).unwrap()) - ) - .is_truthy() - ); - assert!(!RuntimeValue::Array(Shared::new(Vec::new())).is_truthy()); - assert!(!RuntimeValue::None.is_truthy()); - assert!(RuntimeValue::NativeFunction(Ident::new("name")).is_truthy()); - assert!( - RuntimeValue::new_function( - Shared::new(SmallVec::new()), - Shared::new(Vec::new()), - Shared::new(SharedCell::new(Env::default())) - ) - .is_truthy() - ); - assert!(RuntimeValue::Dict(Shared::new(BTreeMap::default())).is_truthy()); - } - - #[test] - fn test_runtime_value_partial_ord() { - assert!(RuntimeValue::Number(Number::from(1.0)) < RuntimeValue::Number(Number::from(2.0))); - assert!( - RuntimeValue::String(Shared::new(String::from("a"))) < RuntimeValue::String(Shared::new(String::from("b"))) - ); - assert!( - RuntimeValue::Array(Shared::new(Vec::new())) - < RuntimeValue::Array(Shared::new(vec!["a".to_string().into()])) - ); - assert!( - RuntimeValue::Markdown( - Shared::new(mq_markdown::Node::Text(mq_markdown::Text { - value: "test".to_string(), - position: None - })), - None - ) < RuntimeValue::Markdown( - Shared::new(mq_markdown::Node::Text(mq_markdown::Text { - value: "test2".to_string(), - position: None - })), - None - ) - ); - assert!(RuntimeValue::Boolean(false) < RuntimeValue::Boolean(true)); - assert!( - RuntimeValue::new_function( - Shared::new(SmallVec::new()), - Shared::new(Vec::new()), - Shared::new(SharedCell::new(Env::default())) - ) < RuntimeValue::new_function( - Shared::new(smallvec![Param::new(IdentWithToken::new("test"))]), - Shared::new(Vec::new()), - Shared::new(SharedCell::new(Env::default())) - ) - ); - } - - #[test] - fn test_runtime_value_len() { - assert_eq!(RuntimeValue::Number(Number::from(42.0)).len(), 42); - assert_eq!(RuntimeValue::String(Shared::new(String::from("test"))).len(), 4); - assert_eq!(RuntimeValue::Boolean(true).len(), 1); - assert_eq!(RuntimeValue::Array(Shared::new(vec![RuntimeValue::None])).len(), 1); - assert_eq!( - RuntimeValue::Markdown( - Shared::new(mq_markdown::Node::Text(mq_markdown::Text { - value: "a".to_string(), - position: None - })), - None - ) - .len(), - 1 - ); - let mut map = BTreeMap::default(); - map.insert(Ident::new("a"), RuntimeValue::String(Shared::new("alpha".to_string()))); - map.insert(Ident::new("b"), RuntimeValue::String(Shared::new("beta".to_string()))); - assert_eq!(RuntimeValue::Dict(Shared::new(map)).len(), 2); - } - - #[test] - fn test_negated() { - assert_eq!( - RuntimeValue::Number(Number::from(42.0)).negated(), - RuntimeValue::Number(Number::from(-42.0)) - ); - assert_eq!(RuntimeValue::Boolean(true).negated(), RuntimeValue::Boolean(false)); - assert_eq!(RuntimeValue::Boolean(false).negated(), RuntimeValue::Boolean(true)); - } - - #[test] - fn test_runtime_value_debug_output() { - let array = RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Number(Number::from(1.0)), - RuntimeValue::String(Shared::new("hello".to_string())), - ])); - assert_eq!(format!("{:?}", array), r#"[1, "hello"]"#); - - let node = mq_markdown::Node::Text(mq_markdown::Text { - value: "test markdown".to_string(), - position: None, - }); - let markdown = RuntimeValue::new_markdown(node); - assert_eq!(format!("{:?}", markdown), "test markdown"); - - let function = RuntimeValue::new_function( - Shared::new(SmallVec::new()), - Shared::new(Vec::new()), - Shared::new(SharedCell::new(Env::default())), - ); - assert_eq!(format!("{:?}", function), "function/0"); - - let native_fn = RuntimeValue::NativeFunction(Ident::new("debug")); - assert_eq!(format!("{:?}", native_fn), "native_function"); - - let mut map = BTreeMap::default(); - map.insert(Ident::new("a"), RuntimeValue::String(Shared::new("alpha".to_string()))); - let map_val = RuntimeValue::Dict(Shared::new(map)); - assert_eq!(format!("{:?}", map_val), r#"{"a": "alpha"}"#); - } - - #[test] - fn test_runtime_value_markdown() { - let markdown = RuntimeValue::new_markdown("test markdown".to_string().into()); - assert_eq!(markdown.markdown_node().unwrap().value(), "test markdown"); - - let updated = markdown.update_markdown_value("updated markdown"); - match &updated { - RuntimeValue::Markdown(node, selector) => { - assert_eq!(node.value(), "updated markdown"); - assert_eq!(*selector, None); - } - _ => panic!("Expected Markdown variant"), - } - } - - #[test] - fn test_runtime_value_markdown_with_selector() { - let child1 = mq_markdown::Node::Text(mq_markdown::Text { - value: "child1".to_string(), - position: None, - }); - let child2 = mq_markdown::Node::Text(mq_markdown::Text { - value: "child2".to_string(), - position: None, - }); - - let parent = mq_markdown::Node::Strong(mq_markdown::Strong { - values: vec![child1, child2], - position: None, - }); - - let markdown_with_selector = - RuntimeValue::Markdown(Shared::new(parent.clone()), Some(Selector::index(1).unwrap())); - - let selected = markdown_with_selector.markdown_node(); - assert!(selected.is_some()); - assert_eq!(selected.unwrap().value(), "child2"); - - let updated = markdown_with_selector.update_markdown_value("updated child"); - match &updated { - RuntimeValue::Markdown(node, selector) => { - assert_eq!(selector, &Some(Selector::index(1).unwrap())); - assert_eq!(node.find_at_index(1).unwrap().value(), "updated child"); - } - _ => panic!("Expected Markdown variant"), - } - } - - #[test] - fn test_selector_index_boundary() { - assert!(Selector::index(0).is_some()); - assert!(Selector::index(254).is_some()); - assert!(Selector::index(255).is_some()); - assert!(Selector::index((u32::MAX - 1) as usize).is_some()); - assert!(Selector::index(u32::MAX as usize).is_none()); - assert!(Selector::index(usize::MAX).is_none()); - } - - #[test] - fn test_markdown_node_with_many_children() { - let children: Vec = (0..300) - .map(|i| { - Node::Text(mq_markdown::Text { - value: format!("child-{i}"), - position: None, - }) - }) - .collect(); - let parent = Node::Strong(mq_markdown::Strong { - values: children, - position: None, - }); - - let markdown = RuntimeValue::Markdown(Shared::new(parent), Some(Selector::index(255).unwrap())); - assert_eq!(markdown.markdown_node().unwrap().value(), "child-255"); - - let updated = markdown.update_markdown_value("updated-255"); - match &updated { - RuntimeValue::Markdown(node, selector) => { - assert_eq!(selector, &Some(Selector::index(255).unwrap())); - assert_eq!(node.find_at_index(255).unwrap().value(), "updated-255"); - } - _ => panic!("Expected Markdown variant"), - } - } - - #[test] - fn test_update_markdown_value_non_markdown() { - assert_eq!( - RuntimeValue::Number(Number::from(42.0)).update_markdown_value("test"), - RuntimeValue::NONE - ); - assert_eq!( - RuntimeValue::String(Shared::new("hello".to_string())).update_markdown_value("test"), - RuntimeValue::NONE - ); - assert_eq!( - RuntimeValue::Boolean(true).update_markdown_value("test"), - RuntimeValue::NONE - ); - assert_eq!(RuntimeValue::None.update_markdown_value("test"), RuntimeValue::NONE); - } - - #[test] - fn test_runtime_value_map_creation_and_equality() { - let mut map1_data = BTreeMap::default(); - map1_data.insert(Ident::new("a"), RuntimeValue::Number(Number::from(1.0))); - map1_data.insert(Ident::new("b"), RuntimeValue::String(Shared::new("hello".to_string()))); - let map1 = RuntimeValue::Dict(Shared::new(map1_data)); - - let mut map2_data = BTreeMap::default(); - map2_data.insert(Ident::new("a"), RuntimeValue::Number(Number::from(1.0))); - map2_data.insert(Ident::new("b"), RuntimeValue::String(Shared::new("hello".to_string()))); - let map2 = RuntimeValue::Dict(Shared::new(map2_data)); - - let mut map3_data = BTreeMap::default(); - map3_data.insert(Ident::new("a"), RuntimeValue::Number(Number::from(1.0))); - map3_data.insert(Ident::new("c"), RuntimeValue::String(Shared::new("world".to_string()))); - let map3 = RuntimeValue::Dict(Shared::new(map3_data)); - - assert_eq!(map1, map2); - assert_ne!(map1, map3); - } - - #[test] - fn test_runtime_value_map_is_empty() { - let empty_map = RuntimeValue::Dict(Shared::new(BTreeMap::default())); - assert!(empty_map.is_empty()); - - let mut map_data = BTreeMap::default(); - map_data.insert(Ident::new("a"), RuntimeValue::Number(Number::from(1.0))); - let non_empty_map = RuntimeValue::Dict(Shared::new(map_data)); - assert!(!non_empty_map.is_empty()); - } - - #[test] - fn test_runtime_value_map_partial_ord() { - let mut map1_data = BTreeMap::default(); - map1_data.insert(Ident::new("a"), RuntimeValue::Number(Number::from(1.0))); - let map1 = RuntimeValue::Dict(Shared::new(map1_data)); - - let mut map2_data = BTreeMap::default(); - map2_data.insert(Ident::new("b"), RuntimeValue::Number(Number::from(2.0))); - let map2 = RuntimeValue::Dict(Shared::new(map2_data)); - - assert_eq!(map1.partial_cmp(&map2), None); - assert_eq!(map2.partial_cmp(&map1), None); - assert_eq!(map1.partial_cmp(&map1), None); - - let num_val = RuntimeValue::Number(Number::from(5.0)); - assert_eq!(map1.partial_cmp(&num_val), None); - assert_eq!(num_val.partial_cmp(&map1), None); - } - - #[test] - fn test_bytes_name() { - assert_eq!(RuntimeValue::Bytes(Shared::new(vec![])).name(), "bytes"); - assert_eq!(RuntimeValue::Bytes(Shared::new(vec![1, 2, 3])).name(), "bytes"); - } - - #[test] - fn test_bytes_is_empty() { - assert!(RuntimeValue::Bytes(Shared::new(vec![])).is_empty()); - assert!(!RuntimeValue::Bytes(Shared::new(vec![0])).is_empty()); - } - - #[test] - fn test_bytes_is_truthy() { - assert!(!RuntimeValue::Bytes(Shared::new(vec![])).is_truthy()); - assert!(RuntimeValue::Bytes(Shared::new(vec![0])).is_truthy()); - assert!(RuntimeValue::Bytes(Shared::new(vec![1, 2, 3])).is_truthy()); - } - - #[test] - fn test_bytes_len() { - assert_eq!(RuntimeValue::Bytes(Shared::new(vec![])).len(), 0); - assert_eq!(RuntimeValue::Bytes(Shared::new(vec![1, 2, 3])).len(), 3); - } - - #[test] - fn test_bytes_display() { - assert_eq!( - format!("{}", RuntimeValue::Bytes(Shared::new(vec![0xde, 0xad, 0xbe, 0xef]))), - "deadbeef" - ); - assert_eq!(format!("{}", RuntimeValue::Bytes(Shared::new(vec![]))), ""); - } - - #[test] - fn test_bytes_debug() { - assert_eq!( - format!("{:?}", RuntimeValue::Bytes(Shared::new(vec![0xca, 0xfe]))), - "bytes(cafe)" - ); - } - - #[test] - fn test_bytes_partial_eq() { - assert_eq!( - RuntimeValue::Bytes(Shared::new(vec![1, 2])), - RuntimeValue::Bytes(Shared::new(vec![1, 2])) - ); - assert_ne!( - RuntimeValue::Bytes(Shared::new(vec![1, 2])), - RuntimeValue::Bytes(Shared::new(vec![1, 3])) - ); - assert_ne!( - RuntimeValue::Bytes(Shared::new(vec![1, 2])), - RuntimeValue::String(Shared::new("0102".to_string())) - ); - } - - #[test] - fn test_bytes_partial_ord() { - assert!(RuntimeValue::Bytes(Shared::new(vec![1])) < RuntimeValue::Bytes(Shared::new(vec![2]))); - assert!(RuntimeValue::Bytes(Shared::new(vec![1, 2])) > RuntimeValue::Bytes(Shared::new(vec![1]))); - assert_eq!( - RuntimeValue::Bytes(Shared::new(vec![1])).partial_cmp(&RuntimeValue::Bytes(Shared::new(vec![1]))), - Some(std::cmp::Ordering::Equal) - ); - assert_eq!( - RuntimeValue::Bytes(Shared::new(vec![])).partial_cmp(&RuntimeValue::None), - None - ); - } - - #[rstest] - #[case(RuntimeValue::None, serde_json::Value::Null)] - #[case(RuntimeValue::Boolean(true), serde_json::Value::Bool(true))] - #[case(RuntimeValue::Boolean(false), serde_json::Value::Bool(false))] - #[case(RuntimeValue::String(Shared::new("hi".to_string())), serde_json::Value::String("hi".to_string()))] - #[case(RuntimeValue::Symbol(Ident::new("sym")), serde_json::Value::String("sym".to_string()))] - #[case(RuntimeValue::NativeFunction(Ident::new("f")), serde_json::Value::Null)] - #[case( - RuntimeValue::Number(Number::from(42.0)), - serde_json::Value::Number(serde_json::Number::from(42)) - )] - #[case(RuntimeValue::Number(Number::from(-7.0)), serde_json::Value::Number(serde_json::Number::from(-7)))] - #[case(RuntimeValue::Number(Number::from(1.5)), serde_json::Value::Number(serde_json::Number::from_f64(1.5).unwrap()))] - fn test_to_json_value_scalars(#[case] value: RuntimeValue, #[case] expected: serde_json::Value) { - assert_eq!(value.to_json_value(), expected); - } - - #[test] - fn test_to_json_value_array() { - let arr = RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Boolean(true), - RuntimeValue::String(Shared::new("x".to_string())), - ])); - match arr.to_json_value() { - serde_json::Value::Array(items) => { - assert_eq!(items[0], serde_json::Value::Bool(true)); - assert_eq!(items[1], serde_json::Value::String("x".to_string())); - } - other => panic!("expected Array, got {other:?}"), - } - } - - #[test] - fn test_to_json_value_dict() { - let mut map = BTreeMap::new(); - map.insert(Ident::new("k"), RuntimeValue::Boolean(false)); - let obj = RuntimeValue::Dict(Shared::new(map)).to_json_value(); - match obj { - serde_json::Value::Object(m) => { - assert_eq!(m["k"], serde_json::Value::Bool(false)); - } - other => panic!("expected Object, got {other:?}"), - } - } - - #[test] - fn test_to_json_value_bytes_base64() { - let b = RuntimeValue::Bytes(Shared::new(vec![0x00, 0xff])); - match b.to_json_value() { - serde_json::Value::String(s) => assert!(!s.is_empty()), - other => panic!("expected String, got {other:?}"), - } - } - - #[test] - fn test_to_json_value_markdown() { - let node = Node::Text(mq_markdown::Text { - value: "hi".to_string(), - position: None, - }); - let value = RuntimeValue::Markdown(Shared::new(node), None).to_json_value(); - assert_eq!(value["type"], serde_json::Value::String("Text".to_string())); - assert_eq!(value["value"], serde_json::Value::String("hi".to_string())); - } - - #[test] - fn test_to_json_value_array_of_markdown() { - // Regression test: nested Markdown values inside an Array (e.g. the array - // returned by `from_html()`) must serialize to their node structure, not `null`. - let node = Node::Text(mq_markdown::Text { - value: "hi".to_string(), - position: None, - }); - let arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::Markdown(Shared::new(node), None)])); - match arr.to_json_value() { - serde_json::Value::Array(items) => { - assert_ne!(items[0], serde_json::Value::Null); - assert_eq!(items[0]["type"], serde_json::Value::String("Text".to_string())); - } - other => panic!("expected Array, got {other:?}"), - } - } - - #[rstest] - #[case(RuntimeValue::None, true)] - #[case(RuntimeValue::Boolean(true), false)] - #[case(RuntimeValue::String(Shared::new("".to_string())), true)] - #[case(RuntimeValue::Array(Shared::new(vec![])), true)] - #[case(RuntimeValue::Dict(Shared::new(BTreeMap::new())), true)] - #[case(RuntimeValue::Bytes(Shared::new(vec![])), true)] - #[case(RuntimeValue::Bytes(Shared::new(vec![1])), false)] - fn test_is_empty(#[case] value: RuntimeValue, #[case] expected: bool) { - assert_eq!(value.is_empty(), expected); - } - - #[rstest] - #[case(RuntimeValue::None, false)] - #[case(RuntimeValue::Boolean(true), true)] - #[case(RuntimeValue::Boolean(false), false)] - #[case(RuntimeValue::String(Shared::new("hi".to_string())), true)] - #[case(RuntimeValue::String(Shared::new("".to_string())), false)] - #[case(RuntimeValue::Array(Shared::new(vec![RuntimeValue::None])), true)] - #[case(RuntimeValue::Array(Shared::new(vec![])), false)] - #[case(RuntimeValue::Symbol(Ident::new("s")), true)] - #[case(RuntimeValue::NativeFunction(Ident::new("f")), true)] - fn test_is_truthy_variants(#[case] value: RuntimeValue, #[case] expected: bool) { - assert_eq!(value.is_truthy(), expected); - } - - #[rstest] - #[case(RuntimeValue::Symbol(Ident::new("abc")), 3)] - #[case(RuntimeValue::NativeFunction(Ident::new("f")), 0)] - fn test_len_less_common(#[case] value: RuntimeValue, #[case] expected: usize) { - assert_eq!(value.len(), expected); - } - - #[test] - fn test_is_none_predicate() { - assert!(RuntimeValue::None.is_none()); - assert!(!RuntimeValue::Boolean(false).is_none()); - } - - #[test] - fn test_is_function_native() { - assert!(RuntimeValue::NativeFunction(Ident::new("f")).is_native_function()); - assert!(!RuntimeValue::NativeFunction(Ident::new("f")).is_function()); - assert!(!RuntimeValue::None.is_native_function()); - } - - #[test] - fn test_is_array_dict() { - assert!(RuntimeValue::Array(Shared::new(vec![])).is_array()); - assert!(!RuntimeValue::None.is_array()); - assert!(RuntimeValue::Dict(Shared::new(BTreeMap::new())).is_dict()); - assert!(!RuntimeValue::None.is_dict()); - } - - #[test] - fn test_new_dict_and_new_markdown() { - assert!(RuntimeValue::new_dict().is_dict()); - let node = mq_markdown::Node::Empty; - assert!(matches!( - RuntimeValue::new_markdown(node), - RuntimeValue::Markdown(_, None) - )); - } - - #[test] - fn test_from_vec_runtime_value() { - let arr: RuntimeValue = vec![RuntimeValue::None, RuntimeValue::Boolean(true)].into(); - assert!(arr.is_array()); - assert_eq!(arr.len(), 2); - } - - #[test] - fn test_from_btree_map() { - let mut map = BTreeMap::new(); - map.insert(Ident::new("x"), RuntimeValue::Boolean(true)); - let dict: RuntimeValue = map.into(); - assert!(dict.is_dict()); - } - - #[test] - fn test_from_usize() { - let v: RuntimeValue = 42usize.into(); - assert!(matches!(v, RuntimeValue::Number(_))); - assert_eq!(v.len(), 42); - } - - #[test] - fn test_markdown_node_with_no_selector() { - let node = mq_markdown::Node::Empty; - let v = RuntimeValue::Markdown(Shared::new(node), None); - assert!(v.markdown_node().is_some()); - } - - #[test] - fn test_markdown_node_non_markdown_returns_none() { - assert!(RuntimeValue::None.markdown_node().is_none()); - assert!( - RuntimeValue::String(Shared::new("x".to_string())) - .markdown_node() - .is_none() - ); - } - - #[test] - fn test_runtime_values_index() { - let values: RuntimeValues = vec![ - RuntimeValue::Boolean(true), - RuntimeValue::String(Shared::new("second".to_string())), - ] - .into(); - assert_eq!(values[0], RuntimeValue::Boolean(true)); - assert_eq!(values[1], RuntimeValue::String(Shared::new("second".to_string()))); - } - - #[test] - fn test_runtime_values_index_mut() { - let mut values: RuntimeValues = vec![RuntimeValue::None, RuntimeValue::None].into(); - values[0] = RuntimeValue::Boolean(true); - assert_eq!(values[0], RuntimeValue::Boolean(true)); - } - - #[test] - fn test_runtime_values_is_empty() { - let empty: RuntimeValues = vec![].into(); - assert!(empty.is_empty()); - let non_empty: RuntimeValues = vec![RuntimeValue::None].into(); - assert!(!non_empty.is_empty()); - } - - fn text_node(s: &str) -> mq_markdown::Node { - mq_markdown::Node::Text(mq_markdown::Text { - value: s.to_string(), - position: None, - }) - } - - fn md(s: &str) -> RuntimeValue { - RuntimeValue::new_markdown(text_node(s)) - } - - #[test] - fn test_negated_string_reverses() { - let v = RuntimeValue::String(Shared::new("abc".to_string())).negated(); - assert_eq!(v, RuntimeValue::String(Shared::new("cba".to_string()))); - } - - #[test] - fn test_negated_none_returns_self() { - assert_eq!(RuntimeValue::None.negated(), RuntimeValue::None); - } - - #[test] - fn test_negated_array_returns_self() { - let arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into())])); - assert_eq!(arr.clone().negated(), arr); - } - - #[test] - fn test_position_non_markdown_returns_none() { - assert!(RuntimeValue::None.position().is_none()); - assert!(RuntimeValue::Number(1.into()).position().is_none()); - assert!(RuntimeValue::String(Shared::new("x".to_string())).position().is_none()); - } - - #[test] - fn test_set_position_non_markdown_is_noop() { - let mut v = RuntimeValue::Number(1.into()); - v.set_position(None); // should not panic - assert_eq!(v, RuntimeValue::Number(1.into())); - } - - #[test] - fn test_to_cbor_value_scalars() { - assert_eq!(RuntimeValue::None.to_cbor_value(), ciborium::Value::Null); - assert_eq!(RuntimeValue::Boolean(true).to_cbor_value(), ciborium::Value::Bool(true)); - assert_eq!( - RuntimeValue::Number(1.5.into()).to_cbor_value(), - ciborium::Value::Float(1.5) - ); - assert_eq!( - RuntimeValue::String(Shared::new("hi".to_string())).to_cbor_value(), - ciborium::Value::Text("hi".to_string()) - ); - assert_eq!( - RuntimeValue::Symbol(Ident::new("s")).to_cbor_value(), - ciborium::Value::Text("s".to_string()) - ); - assert_eq!( - RuntimeValue::Bytes(Shared::new(vec![0x01, 0x02])).to_cbor_value(), - ciborium::Value::Bytes(vec![0x01, 0x02]) - ); - } - - #[test] - fn test_to_cbor_value_array() { - let arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::Boolean(false)])); - match arr.to_cbor_value() { - ciborium::Value::Array(items) => { - assert_eq!(items[0], ciborium::Value::Bool(false)); - } - other => panic!("expected Array, got {other:?}"), - } - } - - #[test] - fn test_to_cbor_value_dict() { - let mut map = BTreeMap::new(); - map.insert(Ident::new("k"), RuntimeValue::Boolean(true)); - let obj = RuntimeValue::Dict(Shared::new(map)).to_cbor_value(); - match obj { - ciborium::Value::Map(pairs) => { - assert_eq!(pairs[0].0, ciborium::Value::Text("k".to_string())); - assert_eq!(pairs[0].1, ciborium::Value::Bool(true)); - } - other => panic!("expected Map, got {other:?}"), - } - } - - #[test] - fn test_to_cbor_value_other_is_null() { - let native = RuntimeValue::NativeFunction(Ident::new("f")).to_cbor_value(); - assert_eq!(native, ciborium::Value::Null); - } - - #[test] - fn test_from_yaml_scalars() { - assert_eq!(RuntimeValue::from(yaml_rust2::Yaml::Null), RuntimeValue::NONE); - assert_eq!( - RuntimeValue::from(yaml_rust2::Yaml::Boolean(true)), - RuntimeValue::Boolean(true) - ); - assert_eq!( - RuntimeValue::from(yaml_rust2::Yaml::Integer(42)), - RuntimeValue::Number((42.0_f64).into()) - ); - assert_eq!( - RuntimeValue::from(yaml_rust2::Yaml::String("hi".to_string())), - RuntimeValue::String(Shared::new("hi".to_string())) - ); - assert_eq!(RuntimeValue::from(yaml_rust2::Yaml::BadValue), RuntimeValue::NONE); - } - - #[test] - fn test_from_yaml_real() { - let v = RuntimeValue::from(yaml_rust2::Yaml::Real("3.14".to_string())); - assert!(matches!(v, RuntimeValue::Number(_))); - } - - #[test] - fn test_from_yaml_array() { - let yaml_arr = yaml_rust2::Yaml::Array(vec![yaml_rust2::Yaml::Integer(1), yaml_rust2::Yaml::Integer(2)]); - let v = RuntimeValue::from(yaml_arr); - assert!(matches!(v, RuntimeValue::Array(_))); - if let RuntimeValue::Array(items) = v { - assert_eq!(items.len(), 2); - } - } - - #[test] - fn test_from_yaml_hash() { - let mut hash = yaml_rust2::yaml::Hash::new(); - hash.insert( - yaml_rust2::Yaml::String("key".to_string()), - yaml_rust2::Yaml::Integer(99), - ); - let v = RuntimeValue::from(yaml_rust2::Yaml::Hash(hash)); - assert!(matches!(v, RuntimeValue::Dict(_))); - } - - #[test] - fn test_from_yaml_alias() { - let v = RuntimeValue::from(yaml_rust2::Yaml::Alias(0)); - assert_eq!(v, RuntimeValue::NONE); - } - - #[test] - fn test_from_ciborium_tag_unwraps_inner() { - let inner = Box::new(ciborium::Value::Bool(true)); - let tagged = ciborium::Value::Tag(1, inner); - let v = RuntimeValue::from(tagged); - assert_eq!(v, RuntimeValue::Boolean(true)); - } - - #[test] - fn test_from_ciborium_integer() { - let v = RuntimeValue::from(ciborium::Value::Integer(42.into())); - assert!(matches!(v, RuntimeValue::Number(_))); - } - - #[test] - fn test_from_ciborium_null_and_unknowns() { - assert_eq!(RuntimeValue::from(ciborium::Value::Null), RuntimeValue::NONE); - } - - #[test] - fn test_from_ciborium_map() { - let pairs = vec![(ciborium::Value::Text("k".to_string()), ciborium::Value::Bool(false))]; - let v = RuntimeValue::from(ciborium::Value::Map(pairs)); - assert!(matches!(v, RuntimeValue::Dict(_))); - } - - #[test] - fn test_from_ciborium_map_non_text_key() { - let pairs = vec![(ciborium::Value::Integer(1.into()), ciborium::Value::Bool(true))]; - let v = RuntimeValue::from(ciborium::Value::Map(pairs)); - assert!(matches!(v, RuntimeValue::Dict(_))); - } - - #[rstest] - #[case(mq_markdown::AttrValue::String("s".to_string()), RuntimeValue::String(Shared::new("s".to_string())))] - #[case(mq_markdown::AttrValue::Number(1.0), RuntimeValue::Number(1.0.into()))] - #[case(mq_markdown::AttrValue::Boolean(true), RuntimeValue::Boolean(true))] - #[case(mq_markdown::AttrValue::Null, RuntimeValue::NONE)] - fn test_from_attr_value(#[case] attr: mq_markdown::AttrValue, #[case] expected: RuntimeValue) { - assert_eq!(RuntimeValue::from(attr), expected); - } - - #[test] - fn test_from_attr_value_integer() { - let v = RuntimeValue::from(mq_markdown::AttrValue::Integer(42)); - assert!(matches!(v, RuntimeValue::Number(_))); - } - - #[test] - fn test_from_attr_value_array() { - let arr = mq_markdown::AttrValue::Array(vec![text_node("item")]); - let v = RuntimeValue::from(arr); - assert!(matches!(v, RuntimeValue::Array(_))); - } - - #[test] - fn test_from_serde_json_number_f64() { - let n = serde_json::Number::from_f64(1.5).unwrap(); - let v = RuntimeValue::from(serde_json::Value::Number(n)); - assert!(matches!(v, RuntimeValue::Number(_))); - } - - #[test] - fn test_from_serde_json_object() { - let mut obj = serde_json::Map::new(); - obj.insert("x".to_string(), serde_json::Value::Bool(true)); - let v = RuntimeValue::from(serde_json::Value::Object(obj)); - assert!(matches!(v, RuntimeValue::Dict(_))); - } - - #[test] - fn test_from_vec_tuple_number() { - let v = RuntimeValue::from(vec![("count".to_string(), Number::from(7.0))]); - if let RuntimeValue::Dict(map) = v { - assert_eq!(map.get(&Ident::new("count")), Some(&RuntimeValue::Number(7.0.into()))); - } else { - panic!("expected dict"); - } - } - - #[test] - fn test_update_with_non_markdown_returns_updated() { - let orig: RuntimeValues = vec![RuntimeValue::Number(1.into())].into(); - let updated: RuntimeValues = vec![RuntimeValue::Number(99.into())].into(); - let result = orig.update_with(updated); - assert_eq!(result[0], RuntimeValue::Number(99.into())); - } - - #[test] - fn test_update_with_markdown_to_none_returns_original() { - let orig: RuntimeValues = vec![md("original")].into(); - let updated: RuntimeValues = vec![RuntimeValue::None].into(); - let result = orig.update_with(updated); - assert_eq!(result[0], md("original")); - } - - #[test] - fn test_update_with_markdown_to_string() { - let orig: RuntimeValues = vec![md("old")].into(); - let updated: RuntimeValues = vec![RuntimeValue::String(Shared::new("new".to_string()))].into(); - let result = orig.update_with(updated); - assert_eq!(result[0].markdown_node().unwrap().value(), "new"); - } - - #[test] - fn test_update_with_markdown_to_number() { - let orig: RuntimeValues = vec![md("0")].into(); - let updated: RuntimeValues = vec![RuntimeValue::Number(42.into())].into(); - let result = orig.update_with(updated); - assert_eq!(result[0].markdown_node().unwrap().value(), "42"); - } - - #[test] - fn test_update_with_markdown_to_boolean() { - let orig: RuntimeValues = vec![md("false")].into(); - let updated: RuntimeValues = vec![RuntimeValue::Boolean(true)].into(); - let result = orig.update_with(updated); - assert_eq!(result[0].markdown_node().unwrap().value(), "true"); - } - - #[test] - fn test_update_with_markdown_to_symbol() { - let orig: RuntimeValues = vec![md("sym")].into(); - let updated: RuntimeValues = vec![RuntimeValue::Symbol(Ident::new("hello"))].into(); - let result = orig.update_with(updated); - assert_eq!(result[0].markdown_node().unwrap().value(), "hello"); - } - - #[test] - fn test_update_with_markdown_to_bytes() { - let orig: RuntimeValues = vec![md("bytes")].into(); - let updated: RuntimeValues = vec![RuntimeValue::Bytes(Shared::new(vec![0xff]))].into(); - let result = orig.update_with(updated); - assert_eq!(result[0].markdown_node().unwrap().value(), "ff"); - } - - #[test] - fn test_update_with_markdown_to_array_with_none_filtered() { - let orig: RuntimeValues = vec![md("item")].into(); - let updated: RuntimeValues = vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::String(Shared::new("a".to_string())), - RuntimeValue::None, - RuntimeValue::String(Shared::new("b".to_string())), - ]))] - .into(); - let result = orig.update_with(updated); - if let RuntimeValue::Array(items) = &result[0] { - assert_eq!(items.len(), 2); - } else { - panic!("expected Array"); - } - } - - #[test] - fn test_update_with_markdown_to_dict() { - let orig: RuntimeValues = vec![md("d")].into(); - let mut map = BTreeMap::new(); - map.insert(Ident::new("a"), RuntimeValue::String(Shared::new("val".to_string()))); - map.insert(Ident::new("b"), RuntimeValue::None); - let updated: RuntimeValues = vec![RuntimeValue::Dict(Shared::new(map))].into(); - let result = orig.update_with(updated); - if let RuntimeValue::Dict(m) = &result[0] { - assert!(m.contains_key(&Ident::new("a"))); - assert!(!m.contains_key(&Ident::new("b"))); // None filtered out - } else { - panic!("expected Dict"); - } - } - - #[test] - fn test_update_with_markdown_to_native_function_returns_original() { - let orig: RuntimeValues = vec![md("orig")].into(); - let updated: RuntimeValues = vec![RuntimeValue::NativeFunction(Ident::new("f"))].into(); - let result = orig.update_with(updated); - assert_eq!(result[0], md("orig")); - } - - #[test] - fn test_update_with_markdown_to_empty_markdown_returns_original() { - let orig: RuntimeValues = vec![md("orig")].into(); - let updated: RuntimeValues = vec![RuntimeValue::Markdown(Shared::new(mq_markdown::Node::Empty), None)].into(); - let result = orig.update_with(updated); - assert_eq!(result[0], md("orig")); - } - - #[test] - fn test_update_with_markdown_to_non_empty_markdown_returns_updated() { - let orig: RuntimeValues = vec![md("old")].into(); - let updated: RuntimeValues = vec![md("new")].into(); - let result = orig.update_with(updated); - assert_eq!(result[0].markdown_node().unwrap().value(), "new"); - } - - #[test] - fn test_runtime_values_compact() { - let values: RuntimeValues = vec![ - RuntimeValue::Number(1.into()), - RuntimeValue::None, - RuntimeValue::String(Shared::new("".to_string())), - RuntimeValue::String(Shared::new("x".to_string())), - ] - .into(); - let compact = values.compact(); - assert_eq!(compact.len(), 2); // only Number(1) and String("x") survive - } - - #[test] - fn test_runtime_values_values() { - let vals = vec![RuntimeValue::Boolean(true), RuntimeValue::None]; - let rv: RuntimeValues = vals.clone().into(); - assert_eq!(rv.values(), &vals); - } - - #[test] - fn test_runtime_values_into_iter() { - let items: RuntimeValues = vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())].into(); - let sum: Vec<_> = items.into_iter().collect(); - assert_eq!(sum.len(), 2); - } - - #[test] - fn test_module_env_name_and_len() { - use crate::SharedCell; - let env = Shared::new(SharedCell::new(Env::default())); - let m = ModuleEnv::new("mymod", env); - assert_eq!(m.name(), "mymod"); - assert_eq!(m.len(), 0); - } - - #[test] - fn test_module_partial_cmp() { - use crate::SharedCell; - let e1 = Shared::new(SharedCell::new(Env::default())); - let e2 = Shared::new(SharedCell::new(Env::default())); - let m1 = RuntimeValue::Module(Shared::new(ModuleEnv::new("alpha", e1))); - let m2 = RuntimeValue::Module(Shared::new(ModuleEnv::new("beta", e2))); - // `ModuleEnv::name` is an interned `Ident`, so ordering follows intern order rather - // than lexicographic order; compare against the `Ident` ordering directly instead of - // assuming "alpha" < "beta" (which isn't guaranteed and would make this test flaky - // depending on what else has been interned by the time it runs). - assert_eq!( - m1.partial_cmp(&m2), - Ident::new("alpha").partial_cmp(&Ident::new("beta")) - ); - } - - #[test] - fn test_cross_type_partial_cmp_is_none() { - let n = RuntimeValue::Number(1.into()); - let s = RuntimeValue::String(Shared::new("a".to_string())); - assert_eq!(n.partial_cmp(&s), None); - } -} diff --git a/crates/mq-lang/src/tarn.rs b/crates/mq-lang/src/tarn.rs index d0a25543d..b85db66d7 100644 --- a/crates/mq-lang/src/tarn.rs +++ b/crates/mq-lang/src/tarn.rs @@ -39,8 +39,6 @@ use crate::engine; use crate::error; use crate::io::{Io, NativeIo, SandboxedIo}; use crate::module::resolver::DefaultModuleResolver; -#[cfg(test)] -use crate::module::resolver::std_resolver::StdModuleResolver; use crate::runtime::host::HostFunctions; use crate::runtime::runtime_value::RuntimeValue; use crate::{ModuleLoader, ModuleResolver}; @@ -63,7 +61,9 @@ pub(crate) struct Options { impl Default for Options { fn default() -> Self { Self { - max_call_stack_depth: if cfg!(debug_assertions) { 40 } else { 192 }, + // Keep debug builds eager to expose accidental recursion, while the heap-backed VM + // can safely accommodate practical non-tail recursion in release builds. + max_call_stack_depth: if cfg!(debug_assertions) { 256 } else { 10_000 }, timeout: None, } } @@ -169,44 +169,6 @@ pub(crate) fn vm_error_to_runtime_error( err.to_runtime_error(token, token_id, token_arena) } -#[cfg(test)] -pub(crate) fn compile_and_run(program: &Program, token_arena: TokenArena) -> Result { - compile_and_run_full( - program, - RuntimeValue::None, - &HostFunctions::default(), - None, - token_arena, - ) -} - -#[cfg(test)] -pub(crate) fn compile_and_run_with_input( - program: &Program, - input: RuntimeValue, - token_arena: TokenArena, -) -> Result { - compile_and_run_full(program, input, &HostFunctions::default(), None, token_arena) -} - -#[cfg(test)] -pub(crate) fn compile_and_run_full( - program: &Program, - input: RuntimeValue, - host_functions: &HostFunctions, - timeout: Option, - token_arena: TokenArena, -) -> Result { - let compiled = compiler::compile_program(program, token_arena, ModuleLoader::new(StdModuleResolver))?; - Ok(interpreter::run( - &compiled, - input, - host_functions, - timeout, - Options::default().max_call_stack_depth, - )?) -} - fn run_for_input(input: RuntimeValue, mut run_one: F) -> Result where F: FnMut(RuntimeValue) -> Result, @@ -215,10 +177,15 @@ where // `input` owns its shared node. In the common case it is uniquely held, so move the // Markdown tree into the transform instead of cloning it before walking every value. RuntimeValue::Markdown(node, _) => Shared::unwrap_or_clone(node) - .map_values_into( - &mut |child_node: &mq_markdown::Node| -> Result { - let value = run_one(RuntimeValue::new_markdown(child_node.clone()))?; - Ok(markdown_child_result(value, child_node)) + .map_values_into_owned( + &mut |child_node: mq_markdown::Node| -> Result { + // The VM receives one shared reference and this fallback keeps the other. + // Read-only work and no-match results move the same node back out without a + // deep clone, while mutations naturally take the existing copy-on-write path. + let child_node = Shared::new(child_node); + let fallback = Shared::clone(&child_node); + let value = run_one(RuntimeValue::Markdown(child_node, None))?; + Ok(markdown_child_result(value, fallback)) }, ) .map(RuntimeValue::new_markdown), @@ -226,9 +193,9 @@ where } } -fn markdown_child_result(value: RuntimeValue, child_node: &mq_markdown::Node) -> mq_markdown::Node { +fn markdown_child_result(value: RuntimeValue, fallback: Shared) -> mq_markdown::Node { match value { - RuntimeValue::None => child_node.to_fragment(), + RuntimeValue::None => Shared::unwrap_or_clone(fallback).into_fragment(), RuntimeValue::NativeFunction(_) => mq_markdown::Node::Empty, RuntimeValue::VmClosure(_) => mq_markdown::Node::Empty, RuntimeValue::Array(arr) => arr @@ -243,7 +210,12 @@ fn markdown_child_result(value: RuntimeValue, child_node: &mq_markdown::Node) -> | RuntimeValue::String(_) | RuntimeValue::Bytes(_) => value.to_string().into(), RuntimeValue::Symbol(i) => i.as_str().into(), - RuntimeValue::Markdown(node, _) => Shared::unwrap_or_clone(node), + RuntimeValue::Markdown(node, _) => { + // `node` can be the shared VM input. Drop the unmatched fallback first so the + // result is uniquely owned again and can move out without cloning. + drop(fallback); + Shared::unwrap_or_clone(node) + } } } @@ -526,7 +498,7 @@ fn collect_module_prelude_targets( inline_modules: &mut Vec<(ast::IdentWithToken, Program, Program)>, ) { for (index, node) in program.iter().enumerate() { - match &*node.expr { + match &node.expr { Expr::Include(Literal::String(path)) => { if !paths.iter().any(|existing| existing == path) { paths.push(path.clone()); @@ -550,7 +522,7 @@ fn collect_module_prelude_targets( /// as separate probes. fn collect_module_paths(program: &Program, paths: &mut Vec) { for node in program { - match &*node.expr { + match &node.expr { Expr::Include(Literal::String(path)) | Expr::Import(Literal::String(path), _) => { if !paths.iter().any(|existing| existing == path) { paths.push(path.clone()); @@ -616,7 +588,7 @@ fn resolve_external_module_prelude( let directive_program: Program = vec![Shared::new(Node { token_id: crate::ast::TokenId::new(0), - expr: Shared::new(Expr::Include(Literal::String(path.to_string()))), + expr: Expr::Include(Literal::String(path.to_string())), })]; let compiled = compiler::compile_program_for_engine( &directive_program, @@ -656,7 +628,7 @@ fn collect_inline_module_vars( let mut path = parent_path.to_vec(); path.push(module.clone()); for node in body { - match &*node.expr { + match &node.expr { Expr::Let(Pattern::Ident(_), _) => vars.push((path.clone(), Shared::clone(node))), Expr::Module(nested_module, nested_body) => { collect_inline_module_vars(nested_module, nested_body, &path, vars); @@ -711,26 +683,23 @@ fn resolve_module_prelude_globals( let probe_args: ast::Args = module_vars .iter() .map(|(path, node)| { - let Expr::Let(Pattern::Ident(let_ident), _) = &*node.expr else { + let Expr::Let(Pattern::Ident(let_ident), _) = &node.expr else { unreachable!("filtered above"); }; Shared::new(Node { token_id: crate::ast::TokenId::new(0), - expr: Shared::new(Expr::QualifiedAccess( - path.clone(), - AccessTarget::Ident(let_ident.clone()), - )), + expr: Expr::QualifiedAccess(path.clone(), AccessTarget::Ident(let_ident.clone())), }) }) .collect(); let mut probe_program = prefix; probe_program.push(Shared::new(Node { token_id: crate::ast::TokenId::new(0), - expr: Shared::new(Expr::Module(ident.clone(), body)), + expr: Expr::Module(ident.clone(), body), })); probe_program.push(Shared::new(Node { token_id: crate::ast::TokenId::new(0), - expr: Shared::new(Expr::Call(ast::IdentWithToken::new("array"), probe_args)), + expr: Expr::Call(ast::IdentWithToken::new("array"), probe_args), })); let probed: Result, Error> = (|| { diff --git a/crates/mq-lang/src/tarn/bytecode.rs b/crates/mq-lang/src/tarn/bytecode.rs index ea61e1c18..a8b9588ef 100644 --- a/crates/mq-lang/src/tarn/bytecode.rs +++ b/crates/mq-lang/src/tarn/bytecode.rs @@ -13,6 +13,17 @@ use std::fmt; /// The implicit pipeline value (`.` / `self`) slot. pub(crate) const SELF_SLOT: u16 = 0; +/// Compile-time frame metadata for a capture-free static call with a common exact arity. +/// +/// The dedicated call opcodes carrying this target avoid indexing the chunk table before a +/// callee frame starts. Chunks whose locals are captured retain the generic call path, because +/// their per-slot cell layout cannot be represented by this compact payload. +#[derive(Debug, Clone, Copy)] +pub(crate) struct StaticExactCallTarget { + pub(crate) chunk_index: u16, + pub(crate) local_count: u16, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// A captured value's source slot. pub(crate) enum UpvalueSource { @@ -37,6 +48,13 @@ pub(crate) enum BinaryOp { Ge, } +impl BinaryOp { + /// Whether this op yields a boolean, making it eligible for compare-and-jump fusion. + pub(crate) fn is_comparison(self) -> bool { + matches!(self, Self::Eq | Self::Ne | Self::Lt | Self::Le | Self::Gt | Self::Ge) + } +} + /// Compact argument-free node selector. #[repr(u8)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -239,6 +257,36 @@ pub(crate) enum OpCode { local: u16, constant: u16, }, + /// Applies a binary operation between a local and a constant, then stores the result back + /// into that same local without materializing the value on the operand stack. + UpdateLocalConst { + op: BinaryOp, + local: u16, + constant: u16, + }, + /// Updates a local from another local without using the operand stack. + UpdateLocalLocal { + op: BinaryOp, + local: u16, + value: u16, + }, + /// Fuses a local/local comparison directly into its branch: computes `left op right` and + /// jumps without ever materializing the boolean on the operand stack. Produced by the + /// bytecode optimizer from a `BinaryLocalLocal` comparison immediately followed by + /// `JumpIfFalse`, the shape every `if`/`while`/`until` condition compiles to. + JumpIfFalseLocalLocal { + op: BinaryOp, + left: u16, + right: u16, + offset: i32, + }, + /// Same fusion as [`Self::JumpIfFalseLocalLocal`] for a local/constant comparison. + JumpIfFalseLocalConst { + op: BinaryOp, + local: u16, + constant: u16, + offset: i32, + }, Neg, Not, ArrayNew, @@ -279,7 +327,33 @@ pub(crate) enum OpCode { SelectorMatchHeading(u8), SelectorMatchWithArgs(Box<(Selector, u16)>), CallBuiltin(Ident, u16), + /// Calls a capture-free fixed-arity chunk through the checked fallback path. + CallStatic(u16, u16), + /// Calls a capture-free fixed-arity chunk with exactly its declared arguments. + CallStaticExact(u16, u16), + /// Calls a capture-free zero-argument chunk with its frame metadata embedded. + CallStaticExact0(StaticExactCallTarget), + /// Calls a capture-free one-argument chunk with its frame metadata embedded. + CallStaticExact1(StaticExactCallTarget), + /// Calls a capture-free two-argument chunk with its frame metadata embedded. + CallStaticExact2(StaticExactCallTarget), + /// Calls a capture-free fixed-arity chunk with the pipeline value as its first argument. + CallStaticImplicitSelf(u16, u16), + /// Recursively calls the current fixed-arity chunk through the checked fallback path. + CallSelf(u16), + /// Recursively calls the current chunk with exactly its declared arguments. + CallSelfExact(u16), + /// Recursively calls the current zero-argument chunk. + CallSelfExact0, + /// Recursively calls the current one-argument chunk. + CallSelfExact1, + /// Recursively calls the current two-argument chunk. + CallSelfExact2, + /// Recursively calls the current chunk with the pipeline value as its first argument. + CallSelfImplicitSelf(u16), CallLocal(u16, u16), + /// Calls an immutable upvalue without first placing its closure on the operand stack. + CallUpvalue(u16, u16), CallValue(u16), /// Invokes a pipeline value only when it is callable without explicit arguments. MaybeAutoCall, @@ -293,6 +367,109 @@ pub(crate) enum OpCode { Return, } +#[cfg(feature = "vm-profile")] +impl OpCode { + /// Returns a stable opcode name for execution-count profiling. + pub(crate) fn profile_name(&self) -> &'static str { + match self { + #[cfg(feature = "debugger")] + Self::StmtBoundary(_) => "StmtBoundary", + #[cfg(feature = "debugger")] + Self::Breakpoint(_) => "Breakpoint", + Self::Const(_) => "Const", + Self::PushNone => "PushNone", + Self::GetLocal(_) => "GetLocal", + Self::SetLocal(_) => "SetLocal", + Self::TeeLocal(_) => "TeeLocal", + Self::CopyLocal { .. } => "CopyLocal", + Self::GetUpvalue(_) => "GetUpvalue", + Self::SetUpvalue(_) => "SetUpvalue", + Self::MakeClosure(_) => "MakeClosure", + Self::MakeStaticClosure(_) => "MakeStaticClosure", + Self::Pop => "Pop", + Self::Dup => "Dup", + Self::Jump(_) => "Jump", + Self::JumpIfFalse(_) => "JumpIfFalse", + Self::Add => "Add", + Self::Sub => "Sub", + Self::Mul => "Mul", + Self::Div => "Div", + Self::Mod => "Mod", + Self::Eq => "Eq", + Self::Ne => "Ne", + Self::Lt => "Lt", + Self::Le => "Le", + Self::Gt => "Gt", + Self::Ge => "Ge", + Self::BinaryLocalLocal { .. } => "BinaryLocalLocal", + Self::BinaryLocalConst { .. } => "BinaryLocalConst", + Self::UpdateLocalConst { .. } => "UpdateLocalConst", + Self::UpdateLocalLocal { .. } => "UpdateLocalLocal", + Self::JumpIfFalseLocalLocal { .. } => "JumpIfFalseLocalLocal", + Self::JumpIfFalseLocalConst { .. } => "JumpIfFalseLocalConst", + Self::Neg => "Neg", + Self::Not => "Not", + Self::ArrayNew => "ArrayNew", + Self::ArrayPush => "ArrayPush", + Self::ArraySpread => "ArraySpread", + Self::DictSpread => "DictSpread", + Self::ToForeachIterable => "ToForeachIterable", + Self::ArrayLen => "ArrayLen", + Self::ArrayGetAt => "ArrayGetAt", + Self::ArrayLenLocal(_) => "ArrayLenLocal", + Self::ArrayGetLocalAt { .. } => "ArrayGetLocalAt", + Self::ForeachNext { .. } => "ForeachNext", + Self::ForeachCollect(_) => "ForeachCollect", + Self::ArraySliceFrom => "ArraySliceFrom", + Self::DictGetLocalOrFail { .. } => "DictGetLocalOrFail", + Self::TypeCheck(_) => "TypeCheck", + Self::GetEnvVar(_) => "GetEnvVar", + Self::GetExternalGlobal(_) => "GetExternalGlobal", + Self::InterpString(_) => "InterpString", + Self::SelectorMatch(_) => "SelectorMatch", + Self::SelectorMatchKind(_) => "SelectorMatchKind", + Self::SelectorMatchHeading(_) => "SelectorMatchHeading", + Self::SelectorMatchWithArgs(_) => "SelectorMatchWithArgs", + Self::CallBuiltin(_, _) => "CallBuiltin", + Self::CallStatic(_, _) => "CallStatic", + Self::CallStaticExact(_, _) => "CallStaticExact", + Self::CallStaticExact0(_) => "CallStaticExact0", + Self::CallStaticExact1(_) => "CallStaticExact1", + Self::CallStaticExact2(_) => "CallStaticExact2", + Self::CallStaticImplicitSelf(_, _) => "CallStaticImplicitSelf", + Self::CallSelf(_) => "CallSelf", + Self::CallSelfExact(_) => "CallSelfExact", + Self::CallSelfExact0 => "CallSelfExact0", + Self::CallSelfExact1 => "CallSelfExact1", + Self::CallSelfExact2 => "CallSelfExact2", + Self::CallSelfImplicitSelf(_) => "CallSelfImplicitSelf", + Self::CallLocal(_, _) => "CallLocal", + Self::CallUpvalue(_, _) => "CallUpvalue", + Self::CallValue(_) => "CallValue", + Self::MaybeAutoCall => "MaybeAutoCall", + Self::TryCatch(_) => "TryCatch", + Self::FlowBreak(_) => "FlowBreak", + Self::FlowContinue => "FlowContinue", + Self::RaiseDestructuringFailed => "RaiseDestructuringFailed", + Self::Return => "Return", + } + } + + /// Returns whether this instruction represents user-program execution rather than a + /// debugger-only boundary. Debugger builds inject boundaries that normal mq builds do not + /// execute, so profiling excludes them to keep opcode proportions actionable. + pub(crate) fn is_profiled_instruction(&self) -> bool { + #[cfg(feature = "debugger")] + { + !matches!(self, Self::StmtBoundary(_) | Self::Breakpoint(_)) + } + #[cfg(not(feature = "debugger"))] + { + true + } + } +} + /// Payload for [`OpCode::TryCatch`]. #[derive(Debug, Clone)] /// `try`/`catch` instruction metadata. @@ -303,31 +480,6 @@ pub(crate) struct TryCatchInfo { pub(crate) continue_offset: Option, } -/// A build-dependent memoized boolean. -#[derive(Debug, Default)] -struct BoolCache( - #[cfg(not(feature = "sync"))] std::cell::Cell>, - #[cfg(feature = "sync")] std::sync::OnceLock, -); - -impl BoolCache { - fn get_or_init(&self, f: impl FnOnce() -> bool) -> bool { - #[cfg(not(feature = "sync"))] - { - if let Some(value) = self.0.get() { - return value; - } - let value = f(); - self.0.set(Some(value)); - value - } - #[cfg(feature = "sync")] - { - *self.0.get_or_init(f) - } - } -} - /// A run of instructions attributed to one source token. #[derive(Debug, Clone, Copy)] pub(crate) struct LineEntry { @@ -351,7 +503,9 @@ pub(crate) struct Chunk { #[cfg(feature = "debugger")] pub(crate) debug_symbols: DebugSymbolTable, pub(crate) param_shape: ParamShape, - captures_local_slots_cache: BoolCache, + /// Sorted local slots whose cells are captured by a nested closure or default expression. + /// All remaining slots can stay as direct values in the interpreter frame. + captured_local_slots: Vec, } impl Chunk { @@ -359,28 +513,51 @@ impl Chunk { pub(crate) fn push_static_closure(&mut self, target_chunk: u16) -> u16 { self.static_closures.push(Shared::new(Closure { chunk_index: target_chunk, - upvalues: Vec::new(), + upvalues: None, })); (self.static_closures.len() - 1) as u16 } - /// Returns whether locals can outlive the current frame. + /// Computes the local slots that a closure or default expression captures. + /// + /// This runs after bytecode optimization, so the interpreter can choose its local storage + /// layout without rescanning instructions each time a frame is entered. + pub(crate) fn refresh_captured_local_slots(&mut self) { + let mut captured = vec![false; self.local_count as usize]; + let mut mark_sources = |sources: &[UpvalueSource]| { + for source in sources { + if let UpvalueSource::Local(slot) = source + && let Some(captured) = captured.get_mut(*slot as usize) + { + *captured = true; + } + } + }; + for opcode in &self.code { + if let OpCode::MakeClosure(payload) = opcode { + mark_sources(&payload.1); + } + } + for binding in &self.param_shape.bindings { + if let ParamBinding::Optional(_, _, sources) = binding { + mark_sources(sources); + } + } + self.captured_local_slots = captured + .into_iter() + .enumerate() + .filter_map(|(slot, is_captured)| is_captured.then_some(slot as u16)) + .collect(); + } + + /// Returns whether any local can outlive the current frame. pub(crate) fn captures_local_slots(&self) -> bool { - self.captures_local_slots_cache.get_or_init(|| { - self.code.iter().any(|op| { - matches!( - op, - OpCode::MakeClosure(payload) - if payload.1.iter().any(|source| matches!(source, UpvalueSource::Local(_))) - ) - }) || self.param_shape.bindings.iter().any(|binding| { - matches!( - binding, - ParamBinding::Optional(_, _, sources) - if sources.iter().any(|source| matches!(source, UpvalueSource::Local(_))) - ) - }) - }) + !self.captured_local_slots.is_empty() + } + + /// Returns the finalized list of local slots that need independently shared cells. + pub(crate) fn captured_local_slots(&self) -> &[u16] { + &self.captured_local_slots } /// Adds a constant and returns its index. @@ -501,6 +678,11 @@ pub(crate) enum BytecodeError { expected: usize, actual: usize, }, + StaticCallTargetInvalid { + chunk: usize, + pc: usize, + target: u16, + }, } impl fmt::Display for BytecodeError { @@ -554,6 +736,9 @@ impl fmt::Display for BytecodeError { "chunk {chunk} pc {pc} makes a closure over chunk {target} with {actual} captures, but it expects {expected}" ) } + Self::StaticCallTargetInvalid { chunk, pc, target } => { + write!(f, "chunk {chunk} pc {pc} directly calls invalid static chunk {target}") + } } } } @@ -567,19 +752,54 @@ pub(crate) fn optimize_chunks(chunks: &mut [Chunk]) { } } +/// Rewrites common capture-free exact static calls after local capture metadata is finalized. +pub(crate) fn specialize_static_exact_calls(chunks: &mut [Chunk]) { + let targets: Vec> = chunks + .iter() + .enumerate() + .map(|(chunk_index, chunk)| { + (!chunk.captures_local_slots()).then_some(StaticExactCallTarget { + chunk_index: chunk_index as u16, + local_count: chunk.local_count, + }) + }) + .collect(); + + for chunk in chunks { + for op in &mut chunk.code { + let OpCode::CallStaticExact(chunk_index, argc) = op else { + continue; + }; + let Some(target) = targets.get(*chunk_index as usize).copied().flatten() else { + continue; + }; + *op = match *argc { + 0 => OpCode::CallStaticExact0(target), + 1 => OpCode::CallStaticExact1(target), + 2 => OpCode::CallStaticExact2(target), + _ => continue, + }; + } + } +} + fn optimize_chunk(chunk: &mut Chunk) { if chunk.code.is_empty() { return; } let has_rewrite = chunk.code.iter().enumerate().any(|(pc, op)| { - matches!( - (op, chunk.code.get(pc + 1)), - (OpCode::Const(_), Some(OpCode::Pop)) - | (OpCode::GetLocal(_), Some(OpCode::SetLocal(_))) - | (OpCode::SetLocal(_), Some(OpCode::GetLocal(_))) - | (OpCode::Jump(0), _) - ) + is_fusable_compare_jump(op, chunk.code.get(pc + 1)) || { + matches!( + (op, chunk.code.get(pc + 1)), + (OpCode::Const(_), Some(OpCode::Pop)) + | (OpCode::GetLocal(_), Some(OpCode::SetLocal(_))) + | (OpCode::SetLocal(_), Some(OpCode::GetLocal(_))) + | (OpCode::BinaryLocalConst { .. }, Some(OpCode::SetLocal(_))) + | (OpCode::BinaryLocalLocal { .. }, Some(OpCode::SetLocal(_))) + | (OpCode::Jump(0), _) + ) + } }); if !has_rewrite { return; @@ -623,10 +843,58 @@ fn optimize_chunk(chunk: &mut Chunk) { keep[pc + 1] = false; pc += 2; } + (OpCode::BinaryLocalConst { op, local, constant }, Some(OpCode::SetLocal(destination))) + if local == destination && !targets.contains(&pc) && !targets.contains(&(pc + 1)) => + { + old_code[pc] = OpCode::UpdateLocalConst { + op: *op, + local: *local, + constant: *constant, + }; + keep[pc + 1] = false; + pc += 2; + } + (OpCode::BinaryLocalLocal { op, left, right }, Some(OpCode::SetLocal(destination))) + if left == destination && !targets.contains(&pc) && !targets.contains(&(pc + 1)) => + { + old_code[pc] = OpCode::UpdateLocalLocal { + op: *op, + local: *left, + value: *right, + }; + keep[pc + 1] = false; + pc += 2; + } (OpCode::Jump(0), _) => { keep[pc] = false; pc += 1; } + (OpCode::BinaryLocalLocal { op, left, right }, Some(OpCode::JumpIfFalse(offset))) + if op.is_comparison() && !targets.contains(&pc) && !targets.contains(&(pc + 1)) => + { + old_code[pc] = OpCode::JumpIfFalseLocalLocal { + op: *op, + left: *left, + right: *right, + // The fused op keeps the `BinaryLocalLocal`'s old pc, one slot earlier than + // the `JumpIfFalse` this offset was written for; +1 keeps the same target. + offset: *offset + 1, + }; + keep[pc + 1] = false; + pc += 2; + } + (OpCode::BinaryLocalConst { op, local, constant }, Some(OpCode::JumpIfFalse(offset))) + if op.is_comparison() && !targets.contains(&pc) && !targets.contains(&(pc + 1)) => + { + old_code[pc] = OpCode::JumpIfFalseLocalConst { + op: *op, + local: *local, + constant: *constant, + offset: *offset + 1, + }; + keep[pc + 1] = false; + pc += 2; + } _ => pc += 1, } } @@ -652,11 +920,27 @@ fn optimize_chunk(chunk: &mut Chunk) { chunk.lines = new_lines; } +/// Whether `op` immediately followed by `next` is a comparison feeding a plain `JumpIfFalse` — +/// the shape every `if`/`while`/`until` condition compiles to — and so can fuse into a single +/// compare-and-branch instruction with no boolean ever pushed to the operand stack. +fn is_fusable_compare_jump(op: &OpCode, next: Option<&OpCode>) -> bool { + let Some(OpCode::JumpIfFalse(_)) = next else { + return false; + }; + match op { + OpCode::BinaryLocalLocal { op, .. } | OpCode::BinaryLocalConst { op, .. } => op.is_comparison(), + _ => false, + } +} + fn jump_targets(code: &[OpCode]) -> std::collections::BTreeSet { let mut targets = std::collections::BTreeSet::new(); for (pc, op) in code.iter().enumerate() { match op { - OpCode::Jump(offset) | OpCode::JumpIfFalse(offset) => { + OpCode::Jump(offset) + | OpCode::JumpIfFalse(offset) + | OpCode::JumpIfFalseLocalLocal { offset, .. } + | OpCode::JumpIfFalseLocalConst { offset, .. } => { if let Some(target) = jump_target(pc, *offset) { targets.insert(target); } @@ -713,6 +997,28 @@ fn rewrite_targets(op: OpCode, old_pc: usize, new_pc: usize, map: &[usize]) -> O match op { OpCode::Jump(offset) => OpCode::Jump(rewrite(offset)), OpCode::JumpIfFalse(offset) => OpCode::JumpIfFalse(rewrite(offset)), + OpCode::JumpIfFalseLocalLocal { + op, + left, + right, + offset, + } => OpCode::JumpIfFalseLocalLocal { + op, + left, + right, + offset: rewrite(offset), + }, + OpCode::JumpIfFalseLocalConst { + op, + local, + constant, + offset, + } => OpCode::JumpIfFalseLocalConst { + op, + local, + constant, + offset: rewrite(offset), + }, OpCode::ForeachNext { array_slot, index_slot, @@ -821,7 +1127,18 @@ pub(crate) fn verify_chunks(chunks: &[Chunk]) -> Result<(), BytecodeError> { } } } - OpCode::BinaryLocalConst { local, constant, .. } => { + OpCode::UpdateLocalLocal { local, value, .. } => { + for slot in [local, value] { + if *slot >= chunk.local_count { + return Err(BytecodeError::LocalOutOfBounds { + chunk: chunk_index, + pc, + slot: *slot, + }); + } + } + } + OpCode::BinaryLocalConst { local, constant, .. } | OpCode::UpdateLocalConst { local, constant, .. } => { if *local >= chunk.local_count { return Err(BytecodeError::LocalOutOfBounds { chunk: chunk_index, @@ -837,6 +1154,42 @@ pub(crate) fn verify_chunks(chunks: &[Chunk]) -> Result<(), BytecodeError> { }); } } + OpCode::JumpIfFalseLocalLocal { + left, right, offset, .. + } => { + for slot in [left, right] { + if *slot >= chunk.local_count { + return Err(BytecodeError::LocalOutOfBounds { + chunk: chunk_index, + pc, + slot: *slot, + }); + } + } + verify_jump_target(chunk, chunk_index, pc, *offset)?; + } + OpCode::JumpIfFalseLocalConst { + local, + constant, + offset, + .. + } => { + if *local >= chunk.local_count { + return Err(BytecodeError::LocalOutOfBounds { + chunk: chunk_index, + pc, + slot: *local, + }); + } + if *constant as usize >= chunk.constants.len() { + return Err(BytecodeError::ConstantOutOfBounds { + chunk: chunk_index, + pc, + index: *constant, + }); + } + verify_jump_target(chunk, chunk_index, pc, *offset)?; + } OpCode::ArrayGetLocalAt { array_slot, index_slot } => { for slot in [array_slot, index_slot] { if *slot >= chunk.local_count { @@ -889,6 +1242,15 @@ pub(crate) fn verify_chunks(chunks: &[Chunk]) -> Result<(), BytecodeError> { }); } } + OpCode::CallUpvalue(index, _) => { + if *index as usize >= chunk.upvalue_names.len() { + return Err(BytecodeError::UpvalueOutOfBounds { + chunk: chunk_index, + pc, + index: *index, + }); + } + } OpCode::MakeClosure(payload) => { let (target, sources) = payload.as_ref(); verify_chunk_target(chunks, chunk_index, pc, *target)?; @@ -904,7 +1266,119 @@ pub(crate) fn verify_chunks(chunks: &[Chunk]) -> Result<(), BytecodeError> { }); }; verify_chunk_target(chunks, chunk_index, pc, closure.chunk_index)?; - verify_closure_capture_count(chunks, chunk_index, pc, closure.chunk_index, closure.upvalues.len())?; + verify_closure_capture_count( + chunks, + chunk_index, + pc, + closure.chunk_index, + closure.upvalues.as_ref().map_or(0, |upvalues| upvalues.len()), + )?; + } + OpCode::CallStatic(target, _) + | OpCode::CallStaticExact(target, _) + | OpCode::CallStaticImplicitSelf(target, _) => { + verify_chunk_target(chunks, chunk_index, pc, *target)?; + let callee = &chunks[*target as usize]; + if !callee.upvalue_names.is_empty() || callee.param_shape.fixed_required_arity().is_none() { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: *target, + }); + } + let arity = callee.param_shape.required; + match op { + OpCode::CallStaticExact(_, argc) if arity != *argc as usize => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: *target, + }); + } + OpCode::CallStaticImplicitSelf(_, argc) if arity == 0 || arity != *argc as usize + 1 => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: *target, + }); + } + _ => {} + } + } + OpCode::CallStaticExact0(target) + | OpCode::CallStaticExact1(target) + | OpCode::CallStaticExact2(target) => { + verify_chunk_target(chunks, chunk_index, pc, target.chunk_index)?; + let callee = &chunks[target.chunk_index as usize]; + let expected_arity = match op { + OpCode::CallStaticExact0(_) => 0, + OpCode::CallStaticExact1(_) => 1, + OpCode::CallStaticExact2(_) => 2, + _ => unreachable!("the outer match limits the opcode variants"), + }; + if !callee.upvalue_names.is_empty() + || callee.param_shape.fixed_required_arity() != Some(expected_arity) + || callee.captures_local_slots() + || callee.local_count != target.local_count + { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: target.chunk_index, + }); + } + } + OpCode::CallSelf(_) + | OpCode::CallSelfExact(_) + | OpCode::CallSelfExact0 + | OpCode::CallSelfExact1 + | OpCode::CallSelfExact2 + | OpCode::CallSelfImplicitSelf(_) => { + let Some(arity) = chunk.param_shape.fixed_required_arity() else { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: chunk_index as u16, + }); + }; + match op { + OpCode::CallSelfExact(argc) if arity != *argc as usize => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: chunk_index as u16, + }); + } + OpCode::CallSelfImplicitSelf(argc) if arity == 0 || arity != *argc as usize + 1 => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: chunk_index as u16, + }); + } + OpCode::CallSelfExact0 if arity != 0 => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: chunk_index as u16, + }); + } + OpCode::CallSelfExact1 if arity != 1 => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: chunk_index as u16, + }); + } + OpCode::CallSelfExact2 if arity != 2 => { + return Err(BytecodeError::StaticCallTargetInvalid { + chunk: chunk_index, + pc, + target: chunk_index as u16, + }); + } + _ => {} + } } OpCode::Jump(offset) | OpCode::JumpIfFalse(offset) => { verify_jump_target(chunk, chunk_index, pc, *offset)?; @@ -1182,6 +1656,23 @@ mod tests { ); } + #[test] + fn captured_local_metadata_contains_only_closure_and_default_sources() { + let mut chunk = Chunk { + local_count: 5, + code: vec![OpCode::MakeClosure(Box::new((0, vec![UpvalueSource::Local(3)])))], + param_shape: ParamShape { + bindings: vec![ParamBinding::Optional(1, 0, vec![UpvalueSource::Local(1)])], + ..Default::default() + }, + ..Default::default() + }; + + chunk.refresh_captured_local_slots(); + + assert_eq!(chunk.captured_local_slots(), &[1, 3]); + } + #[test] fn verifier_rejects_invalid_constant_and_jump_targets() { let invalid_constant = Chunk { @@ -1231,11 +1722,19 @@ mod tests { OpCode::Pop, OpCode::Return, ])] + #[case::update_local_local(vec![ + OpCode::UpdateLocalLocal { op: BinaryOp::Add, local: 0, value: 0 }, + OpCode::Return, + ])] #[case::binary_local_const(vec![ OpCode::BinaryLocalConst { op: BinaryOp::Add, local: 0, constant: 0 }, OpCode::Pop, OpCode::Return, ])] + #[case::update_local_const(vec![ + OpCode::UpdateLocalConst { op: BinaryOp::Add, local: 0, constant: 0 }, + OpCode::Return, + ])] #[case::array_get_local_at(vec![ OpCode::ArrayGetLocalAt { array_slot: 0, index_slot: 0 }, OpCode::Pop, @@ -1283,6 +1782,10 @@ mod tests { OpCode::Pop, OpCode::Return, ])] + #[case::update_local_const(vec![ + OpCode::UpdateLocalConst { op: BinaryOp::Add, local: 0, constant: 0 }, + OpCode::Return, + ])] fn verifier_rejects_out_of_bounds_constant_index(#[case] code: Vec) { let chunk = Chunk { code, @@ -1428,7 +1931,7 @@ mod tests { code: vec![OpCode::MakeStaticClosure(0), OpCode::Return], static_closures: vec![Shared::new(Closure { chunk_index: 1, - upvalues: Vec::new(), + upvalues: None, })], ..Default::default() }; diff --git a/crates/mq-lang/src/tarn/compiler.rs b/crates/mq-lang/src/tarn/compiler.rs index 514f08a20..a71b93a01 100644 --- a/crates/mq-lang/src/tarn/compiler.rs +++ b/crates/mq-lang/src/tarn/compiler.rs @@ -68,6 +68,14 @@ enum Resolved { Upvalue { index: u16, immutable: bool }, } +/// The parameter-binding path selected for a compile-time known fixed-arity call. +#[derive(Clone, Copy)] +enum FixedCallForm { + Exact, + ImplicitSelf, + Fallback, +} + struct LoopCtx { continue_target: usize, break_jumps: Vec, @@ -111,6 +119,8 @@ struct PatternState { struct Compiler { chunks: Vec, scopes: Vec, + /// The directly enclosing named function and its fixed arity, if it has one. + function_names: Vec)>>, current: usize, loops: Vec, current_token_id: crate::ast::TokenId, @@ -361,7 +371,7 @@ fn builtin_dependency_graph(module: &crate::Module) -> Option Some((ident.name, soft_builtin_names_in_program(&vec![Shared::clone(node)]))), _ => None, }) @@ -386,7 +396,7 @@ fn soft_builtin_names_in_program_with_shadowed( ) -> FxHashSet { let mut names = FxHashSet::default(); let mut shadowed = inherited_shadowed.clone(); - shadowed.extend(program.iter().filter_map(|node| match &*node.expr { + shadowed.extend(program.iter().filter_map(|node| match &node.expr { Expr::Def(ident, _, _) => Some(ident.name), _ => None, })); @@ -401,7 +411,7 @@ fn collect_soft_builtin_names( shadowed: &FxHashSet, names: &mut FxHashSet, ) { - match &*node.expr { + match &node.expr { Expr::As(_, value) | Expr::Let(_, value) | Expr::Var(_, value) @@ -512,7 +522,7 @@ fn referenced_names_in_program(program: &Program) -> FxHashSet { } fn collect_referenced_names(node: &Shared, names: &mut FxHashSet) { - match &*node.expr { + match &node.expr { Expr::As(_, value) | Expr::Let(_, value) | Expr::Var(_, value) @@ -632,6 +642,7 @@ fn compile_program_impl( let mut compiler = Compiler { chunks: vec![Chunk::default()], scopes: vec![scope], + function_names: vec![None], current: 0, loops: Vec::new(), current_token_id: crate::ast::TokenId::new(0), @@ -667,6 +678,10 @@ fn compile_program_impl( compiler.chunks[0].local_mutable = compiler.scopes[0].local_mutable(); compiler.chunks[0].upvalue_names = compiler.scopes[0].upvalue_names(); bytecode::optimize_chunks(&mut compiler.chunks); + for chunk in &mut compiler.chunks { + chunk.refresh_captured_local_slots(); + } + bytecode::specialize_static_exact_calls(&mut compiler.chunks); bytecode::verify_chunks(&compiler.chunks).map_err(|error| CompileError::InvalidBytecode(error.to_string()))?; #[cfg(feature = "debugger")] { @@ -748,7 +763,7 @@ impl Compiler { let mut defs: Program = Vec::new(); for node in program { self.current_token_id = node.token_id; - match &*node.expr { + match &node.expr { Expr::Def(_, _, _) => defs.push(Shared::clone(node)), Expr::Let(Pattern::Ident(ident), _) | Expr::Var(Pattern::Ident(ident), _) => { self.scope_mut().declare_or_reuse(ident.name); @@ -812,17 +827,17 @@ impl Compiler { for item in init { match item { #[cfg(not(feature = "debugger"))] - Deferred::Statement(node) if matches!(&*node.expr, Expr::Let(..) | Expr::Var(..)) => { - let (Expr::Let(pattern, value) | Expr::Var(pattern, value)) = &*node.expr else { + Deferred::Statement(node) if matches!(&node.expr, Expr::Let(..) | Expr::Var(..)) => { + let (Expr::Let(pattern, value) | Expr::Var(pattern, value)) = &node.expr else { unreachable!("guarded above"); }; self.current_token_id = node.token_id; self.take_pending_pattern_override(pattern); - self.compile_let_or_var_binding(pattern, value, matches!(&*node.expr, Expr::Var(..)))?; + self.compile_let_or_var_binding(pattern, value, matches!(&node.expr, Expr::Var(..)))?; continue; } Deferred::Statement(node) => { - if let Expr::Let(pattern, _) | Expr::Var(pattern, _) = &*node.expr { + if let Expr::Let(pattern, _) | Expr::Var(pattern, _) = &node.expr { self.take_pending_pattern_override(pattern); } self.compile_expr(node)?; @@ -843,7 +858,7 @@ impl Compiler { } match last { Deferred::Statement(node) => { - if let Expr::Let(pattern, _) | Expr::Var(pattern, _) = &*node.expr { + if let Expr::Let(pattern, _) | Expr::Var(pattern, _) = &node.expr { self.take_pending_pattern_override(pattern); } self.compile_expr(node)?; @@ -899,18 +914,36 @@ impl Compiler { self.chunk_mut().emit(op, token_id) } - fn emit_closure(&mut self, chunk_index: u16, upvalues: Vec) { + /// Emits a closure value and returns whether it is capture-free. + fn emit_closure(&mut self, chunk_index: u16, upvalues: Vec) -> bool { if upvalues.is_empty() { let closure_index = self.chunk_mut().push_static_closure(chunk_index); self.emit(OpCode::MakeStaticClosure(closure_index)); + true } else { self.emit(OpCode::MakeClosure(Box::new((chunk_index, upvalues)))); + false + } + } + + /// Records a direct-call target when it needs neither captured state nor the general + /// optional/variadic parameter binder. + fn register_static_function(&mut self, slot: u16, chunk_index: u16, capture_free: bool) { + if capture_free + && self.chunks[chunk_index as usize] + .param_shape + .fixed_required_arity() + .is_some() + { + self.scope_mut().set_static_function(slot, chunk_index); + } else { + self.scope_mut().clear_static_function(slot); } } fn compile_body(&mut self, body: &Program) -> CompileResult<()> { for node in body { - if let Expr::Def(ident, _, _) = &*node.expr { + if let Expr::Def(ident, _, _) = &node.expr { self.scope_mut().declare(ident.name); } } @@ -922,9 +955,9 @@ impl Compiler { for node in init { // See the matching case in `compile_top_level`. #[cfg(not(feature = "debugger"))] - if let Expr::Let(pattern, value) | Expr::Var(pattern, value) = &*node.expr { + if let Expr::Let(pattern, value) | Expr::Var(pattern, value) = &node.expr { self.current_token_id = node.token_id; - self.compile_let_or_var_binding(pattern, value, matches!(&*node.expr, Expr::Var(..)))?; + self.compile_let_or_var_binding(pattern, value, matches!(&node.expr, Expr::Var(..)))?; continue; } self.compile_expr(node)?; @@ -942,7 +975,7 @@ impl Compiler { fn is_auto_call_candidate(node: &Node) -> bool { matches!( - &*node.expr, + &node.expr, Expr::Ident(_) | Expr::QualifiedAccess(_, AccessTarget::Ident(_)) ) } @@ -972,6 +1005,9 @@ impl Compiler { } let param_slots: Vec = params.iter().map(|param| scope.declare(param.ident.name)).collect(); self.scopes.push(scope); + let is_fixed_arity = params.iter().all(|param| !param.is_variadic && param.default.is_none()); + self.function_names + .push(name_for_shadow.map(|name| (name, is_fixed_arity.then_some(params.len())))); let mut bindings = Vec::with_capacity(params.len()); let mut required = 0usize; @@ -995,6 +1031,7 @@ impl Compiler { self.emit(OpCode::Return); let finished = self.scopes.pop().expect("scope pushed above"); + self.function_names.pop().expect("function name pushed above"); self.chunks[new_index as usize].local_count = finished.local_count(); self.chunks[new_index as usize].local_names = finished.local_names(); self.chunks[new_index as usize].local_mutable = finished.local_mutable(); @@ -1071,8 +1108,8 @@ impl Compiler { mutable: bool, ) -> CompileResult<()> { match pattern { - Pattern::Ident(ident) if matches!(&*value.expr, Expr::Fn(_, _)) => { - let Expr::Fn(params, body) = &*value.expr else { + Pattern::Ident(ident) if matches!(&value.expr, Expr::Fn(_, _)) => { + let Expr::Fn(params, body) = &value.expr else { unreachable!("guarded above"); }; let slot = self.scope_mut().declare_or_reuse(ident.name); @@ -1082,8 +1119,13 @@ impl Compiler { self.scope_mut().mark_immutable(slot); } let (chunk_idx, upvalues) = self.compile_function(params, body, Some(ident.name))?; - self.emit_closure(chunk_idx, upvalues); + let capture_free = self.emit_closure(chunk_idx, upvalues); self.emit(OpCode::SetLocal(slot)); + if mutable { + self.scope_mut().clear_static_function(slot); + } else { + self.register_static_function(slot, chunk_idx, capture_free); + } } Pattern::Ident(ident) => { self.compile_expr(value)?; @@ -1093,6 +1135,7 @@ impl Compiler { } else { self.scope_mut().mark_immutable(slot); } + self.scope_mut().clear_static_function(slot); self.emit(OpCode::SetLocal(slot)); } _ => { @@ -1404,7 +1447,7 @@ impl Compiler { fn predeclare_module_var_slots(&mut self, vars: &Program) { for node in vars { - if let Expr::Let(Pattern::Ident(ident), _) = &*node.expr { + if let Expr::Let(Pattern::Ident(ident), _) = &node.expr { self.scope_mut().declare(ident.name); } } @@ -1433,7 +1476,7 @@ impl Compiler { module .vars .iter() - .filter_map(|node| match &*node.expr { + .filter_map(|node| match &node.expr { Expr::Let(Pattern::Ident(ident), _) => self .scope_mut() .resolve_local(ident.name) @@ -1471,7 +1514,7 @@ impl Compiler { ) -> CompileResult<()> { for node in vars { self.current_token_id = node.token_id; - let Expr::Let(pattern, value) = &*node.expr else { + let Expr::Let(pattern, value) = &node.expr else { self.compile_expr(node)?; self.emit(OpCode::Pop); continue; @@ -1512,16 +1555,16 @@ impl Compiler { parent_module_path: &[crate::Ident], ) -> CompileResult<()> { for node in nodes { - if let Expr::Module(ident, program) = &*node.expr { + if let Expr::Module(ident, program) = &node.expr { self.current_token_id = node.token_id; self.compile_module(ident, program, parent_module_path)?; self.emit(OpCode::Pop); continue; } #[cfg(not(feature = "debugger"))] - if let Expr::Let(pattern, value) | Expr::Var(pattern, value) = &*node.expr { + if let Expr::Let(pattern, value) | Expr::Var(pattern, value) = &node.expr { self.current_token_id = node.token_id; - self.compile_let_or_var_binding(pattern, value, matches!(&*node.expr, Expr::Var(..)))?; + self.compile_let_or_var_binding(pattern, value, matches!(&node.expr, Expr::Var(..)))?; continue; } self.compile_expr(node)?; @@ -1560,7 +1603,7 @@ impl Compiler { let definitions: FxHashMap> = module .functions .iter() - .filter_map(|node| match &*node.expr { + .filter_map(|node| match &node.expr { Expr::Def(ident, _, _) => Some((ident.name, node)), _ => None, }) @@ -1590,7 +1633,7 @@ impl Compiler { module .functions .iter() - .filter(|node| matches!(&*node.expr, Expr::Def(ident, _, _) if required.contains(&ident.name))) + .filter(|node| matches!(&node.expr, Expr::Def(ident, _, _) if required.contains(&ident.name))) .cloned() .collect() } @@ -1622,7 +1665,7 @@ impl Compiler { let functions = module .functions .iter() - .filter(|node| matches!(&*node.expr, Expr::Def(ident, _, _) if required.contains(&ident.name))) + .filter(|node| matches!(&node.expr, Expr::Def(ident, _, _) if required.contains(&ident.name))) .cloned() .collect(); self.compile_functions_with_forward_refs(&functions) @@ -1631,7 +1674,7 @@ impl Compiler { fn compile_functions_with_forward_refs(&mut self, nodes: &Program) -> CompileResult<()> { let mut slots = Vec::with_capacity(nodes.len()); for node in nodes { - let Expr::Def(ident, _, _) = &*node.expr else { + let Expr::Def(ident, _, _) = &node.expr else { return Err(CompileError::Unsupported( "module top-level statement is not a def", self.current_token_id, @@ -1643,12 +1686,13 @@ impl Compiler { } for (node, slot) in nodes.iter().zip(slots) { self.current_token_id = node.token_id; - let Expr::Def(ident, params, body) = &*node.expr else { + let Expr::Def(ident, params, body) = &node.expr else { unreachable!("validated as Def above"); }; let (chunk_idx, upvalues) = self.compile_function(params, body, Some(ident.name))?; - self.emit_closure(chunk_idx, upvalues); + let capture_free = self.emit_closure(chunk_idx, upvalues); self.emit(OpCode::SetLocal(slot)); + self.register_static_function(slot, chunk_idx, capture_free); } Ok(()) } @@ -1691,7 +1735,7 @@ impl Compiler { let depth = self.scopes.len() - 1; let mut slots = Vec::with_capacity(functions.len()); for node in &functions { - let Expr::Def(ident, _, _) = &*node.expr else { + let Expr::Def(ident, _, _) = &node.expr else { return Err(CompileError::Unsupported( "module function is not a def", self.current_token_id, @@ -1703,12 +1747,13 @@ impl Compiler { } for (node, slot) in functions.iter().zip(&slots) { self.current_token_id = node.token_id; - let Expr::Def(ident, params, body) = &*node.expr else { + let Expr::Def(ident, params, body) = &node.expr else { unreachable!("validated as Def above"); }; let (chunk_idx, upvalues) = self.compile_function(params, body, Some(ident.name))?; - self.emit_closure(chunk_idx, upvalues); + let capture_free = self.emit_closure(chunk_idx, upvalues); self.emit(OpCode::SetLocal(*slot)); + self.register_static_function(*slot, chunk_idx, capture_free); self.insert_qualified_binding(&[module_alias], ident.name, QualifiedSlot { depth, slot: *slot }); } // Only qualified names remain visible after compilation. @@ -1741,7 +1786,7 @@ impl Compiler { let mut def_slots = FxHashMap::default(); let mut let_slots = FxHashMap::default(); for node in program { - match &*node.expr { + match &node.expr { Expr::Def(def_ident, _, _) => { let slot = self.scope_mut().declare(def_ident.name); self.scope_mut().mark_immutable(slot); @@ -1763,12 +1808,13 @@ impl Compiler { let mut rest = Program::new(); for node in program { self.current_token_id = node.token_id; - match &*node.expr { + match &node.expr { Expr::Def(def_ident, params, body) => { let slot = def_slots[&def_ident.name]; let (chunk_idx, upvalues) = self.compile_function(params, body, Some(def_ident.name))?; - self.emit_closure(chunk_idx, upvalues); + let capture_free = self.emit_closure(chunk_idx, upvalues); self.emit(OpCode::SetLocal(slot)); + self.register_static_function(slot, chunk_idx, capture_free); self.insert_qualified_binding(module_path, def_ident.name, QualifiedSlot { depth, slot }); } Expr::Include(_) | Expr::Let(_, _) | Expr::Var(_, _) | Expr::Import(_, _) | Expr::Module(_, _) => { @@ -1796,7 +1842,7 @@ impl Compiler { ) -> CompileResult<()> { for node in rest { self.current_token_id = node.token_id; - match &*node.expr { + match &node.expr { Expr::Include(literal) => { // See `compile_discarding`: skip the discarded trailing self-value. let Literal::String(path) = literal else { @@ -1810,7 +1856,7 @@ impl Compiler { self.compile_module_vars_binding(&path, &module, None)?; } Expr::Let(pattern, value) | Expr::Var(pattern, value) => { - let mutable = matches!(&*node.expr, Expr::Var(..)); + let mutable = matches!(&node.expr, Expr::Var(..)); let mut names = Vec::new(); collect_pattern_idents(pattern, &mut names); names.sort(); @@ -1991,7 +2037,7 @@ impl Compiler { self.chunk_mut().debug_nodes.push((node.token_id, Shared::clone(node))); self.emit(OpCode::StmtBoundary(node.token_id)); } - match &*node.expr { + match &node.expr { Expr::Literal(lit) => { let value = literal_to_runtime_value(lit); let idx = self.chunk_mut().push_const(value); @@ -2037,8 +2083,9 @@ impl Compiler { let slot = self.scope_mut().declare_or_reuse(ident.name); self.scope_mut().mark_immutable(slot); let (chunk_idx, upvalues) = self.compile_function(params, body, Some(ident.name))?; - self.emit_closure(chunk_idx, upvalues); + let capture_free = self.emit_closure(chunk_idx, upvalues); self.emit(OpCode::SetLocal(slot)); + self.register_static_function(slot, chunk_idx, capture_free); self.emit(OpCode::GetLocal(slot)); Ok(()) } @@ -2051,6 +2098,7 @@ impl Compiler { self.compile_expr(value)?; let slot = self.scope_mut().declare_or_reuse(ident.name); self.scope_mut().mark_immutable(slot); + self.scope_mut().clear_static_function(slot); self.emit(OpCode::SetLocal(slot)); self.emit(OpCode::GetLocal(SELF_SLOT)); Ok(()) @@ -2208,6 +2256,34 @@ impl Compiler { let shadowed = self.scope_mut().shadowed_builtin == Some(ident); + // A named function normally resolves its own name through an upvalue, which would make + // every recursive call load and clone the closure. At this lexical depth the active + // frame already supplies the correct captured environment, so call the current chunk + // directly. A same-named parameter/local still takes precedence. + let direct_self_arity = self + .function_names + .last() + .and_then(|entry| *entry) + .and_then(|(name, arity)| (name == ident).then_some(arity).flatten()) + .filter(|_| { + self.scopes + .last() + .is_some_and(|scope| scope.resolve_local(ident).is_none()) + }); + if !shadowed && let Some(arity) = direct_self_arity { + for arg in args { + self.compile_expr(arg)?; + } + self.current_token_id = call_token_id; + let argc = self.arg_count(args.len())?; + self.emit(match Self::fixed_call_form(arity, argc) { + FixedCallForm::Exact => Self::self_exact_call_opcode(argc), + FixedCallForm::ImplicitSelf => OpCode::CallSelfImplicitSelf(argc), + FixedCallForm::Fallback => OpCode::CallSelf(argc), + }); + return Ok(()); + } + if !shadowed && let Some(resolved) = self.resolve(ident) { let local_call = match resolved { Resolved::Local(slot) if self.scopes.last().is_some_and(|scope| scope.is_immutable(slot)) => Some(slot), @@ -2219,7 +2295,24 @@ impl Compiler { } self.current_token_id = call_token_id; let argc = self.arg_count(args.len())?; - self.emit(OpCode::CallLocal(slot, argc)); + if let Some(chunk) = self.scopes.last().and_then(|scope| scope.static_function(slot)) { + let arity = self.chunks[chunk as usize].param_shape.required; + self.emit(match Self::fixed_call_form(arity, argc) { + FixedCallForm::Exact => OpCode::CallStaticExact(chunk, argc), + FixedCallForm::ImplicitSelf => OpCode::CallStaticImplicitSelf(chunk, argc), + FixedCallForm::Fallback => OpCode::CallStatic(chunk, argc), + }); + } else { + self.emit(OpCode::CallLocal(slot, argc)); + } + return Ok(()); + } + if let Resolved::Upvalue { index, immutable: true } = resolved { + for arg in args { + self.compile_expr(arg)?; + } + self.current_token_id = call_token_id; + self.emit(OpCode::CallUpvalue(index, self.arg_count(args.len())?)); return Ok(()); } match resolved { @@ -2293,12 +2386,34 @@ impl Compiler { Ok(()) } + /// Classifies a statically known fixed-arity call without changing the fallback's runtime + /// arity-error behavior. + fn fixed_call_form(arity: usize, argc: u16) -> FixedCallForm { + if argc as usize == arity { + FixedCallForm::Exact + } else if arity > 0 && argc as usize + 1 == arity { + FixedCallForm::ImplicitSelf + } else { + FixedCallForm::Fallback + } + } + + /// Selects a dedicated self-call opcode for the three most common exact arities. + fn self_exact_call_opcode(argc: u16) -> OpCode { + match argc { + 0 => OpCode::CallSelfExact0, + 1 => OpCode::CallSelfExact1, + 2 => OpCode::CallSelfExact2, + _ => OpCode::CallSelfExact(argc), + } + } + fn compile_local_binary(&mut self, op: BinaryOp, args: &ast::Args) -> bool { let Some(left_slot) = self.current_local_slot(&args[0]) else { return false; }; - match &*args[1].expr { + match &args[1].expr { Expr::Ident(_) => { let Some(right_slot) = self.current_local_slot(&args[1]) else { return false; @@ -2324,20 +2439,20 @@ impl Compiler { } fn current_local_slot(&self, node: &Shared) -> Option { - let Expr::Ident(ident) = &*node.expr else { + let Expr::Ident(ident) = &node.expr else { return None; }; self.scopes.last().and_then(|scope| scope.resolve_local(ident.name)) } fn is_spread(arg: &Node) -> bool { - matches!(&*arg.expr, Expr::Call(spread_ident, _) if spread_ident.name == builtins::SPREAD.into()) + matches!(&arg.expr, Expr::Call(spread_ident, _) if spread_ident.name == builtins::SPREAD.into()) } fn compile_array_call(&mut self, args: &ast::Args) -> CompileResult<()> { self.emit(OpCode::ArrayNew); for arg in args { - if let Expr::Call(spread_ident, spread_args) = &*arg.expr + if let Expr::Call(spread_ident, spread_args) = &arg.expr && spread_ident.name == builtins::SPREAD.into() { self.compile_expr(&spread_args[0])?; @@ -2362,7 +2477,7 @@ impl Compiler { } self.emit(OpCode::ArrayNew); for arg in args { - if let Expr::Call(spread_ident, spread_args) = &*arg.expr + if let Expr::Call(spread_ident, spread_args) = &arg.expr && spread_ident.name == builtins::SPREAD.into() { self.compile_expr(&spread_args[0])?; @@ -2716,7 +2831,7 @@ pub(super) fn collect_pattern_idents(pattern: &Pattern, out: &mut Vec Vec { vars.iter() - .filter_map(|node| match &*node.expr { + .filter_map(|node| match &node.expr { Expr::Let(Pattern::Ident(ident), _) => Some(ident.name), _ => None, }) diff --git a/crates/mq-lang/src/tarn/disasm.rs b/crates/mq-lang/src/tarn/disasm.rs index 83262a0f8..5bbb82bf1 100644 --- a/crates/mq-lang/src/tarn/disasm.rs +++ b/crates/mq-lang/src/tarn/disasm.rs @@ -182,6 +182,45 @@ fn format_opcode(opcode: &bytecode::OpCode, chunk: &bytecode::Chunk, pc: usize) } => { format!("BinaryLocalConst {op:?} {}, const {constant}", local(*slot)) } + bytecode::OpCode::UpdateLocalConst { + op, + local: slot, + constant, + } => { + format!("UpdateLocalConst {op:?} {}, const {constant}", local(*slot)) + } + bytecode::OpCode::UpdateLocalLocal { + op, + local: destination, + value, + } => { + format!("UpdateLocalLocal {op:?} {}, {}", local(*destination), local(*value)) + } + bytecode::OpCode::JumpIfFalseLocalLocal { + op, + left, + right, + offset, + } => { + format!( + "JumpIfFalseLocalLocal {op:?} {}, {} -> {}", + local(*left), + local(*right), + jump_ref(pc, *offset) + ) + } + bytecode::OpCode::JumpIfFalseLocalConst { + op, + local: slot, + constant, + offset, + } => { + format!( + "JumpIfFalseLocalConst {op:?} {}, const {constant} -> {}", + local(*slot), + jump_ref(pc, *offset) + ) + } bytecode::OpCode::Neg => "Neg".to_string(), bytecode::OpCode::Not => "Not".to_string(), bytecode::OpCode::ArrayNew => "ArrayNew".to_string(), @@ -229,7 +268,30 @@ fn format_opcode(opcode: &bytecode::OpCode, chunk: &bytecode::Chunk, pc: usize) format!("SelectorMatchWithArgs {:?}, argc={}", payload.0, payload.1) } bytecode::OpCode::CallBuiltin(name, argc) => format!("CallBuiltin {name}, argc={argc}"), + bytecode::OpCode::CallStatic(chunk, argc) => format!("CallStatic chunk {chunk}, argc={argc}"), + bytecode::OpCode::CallStaticExact(chunk, argc) => { + format!("CallStaticExact chunk {chunk}, argc={argc}") + } + bytecode::OpCode::CallStaticExact0(target) => { + format!("CallStaticExact0 chunk {}", target.chunk_index) + } + bytecode::OpCode::CallStaticExact1(target) => { + format!("CallStaticExact1 chunk {}", target.chunk_index) + } + bytecode::OpCode::CallStaticExact2(target) => { + format!("CallStaticExact2 chunk {}", target.chunk_index) + } + bytecode::OpCode::CallStaticImplicitSelf(chunk, argc) => { + format!("CallStaticImplicitSelf chunk {chunk}, argc={argc}") + } + bytecode::OpCode::CallSelf(argc) => format!("CallSelf argc={argc}"), + bytecode::OpCode::CallSelfExact(argc) => format!("CallSelfExact argc={argc}"), + bytecode::OpCode::CallSelfExact0 => "CallSelfExact0".to_string(), + bytecode::OpCode::CallSelfExact1 => "CallSelfExact1".to_string(), + bytecode::OpCode::CallSelfExact2 => "CallSelfExact2".to_string(), + bytecode::OpCode::CallSelfImplicitSelf(argc) => format!("CallSelfImplicitSelf argc={argc}"), bytecode::OpCode::CallLocal(slot, argc) => format!("CallLocal {}, argc={argc}", local(*slot)), + bytecode::OpCode::CallUpvalue(slot, argc) => format!("CallUpvalue {}, argc={argc}", upvalue(*slot)), bytecode::OpCode::CallValue(argc) => format!("CallValue argc={argc}"), bytecode::OpCode::MaybeAutoCall => "MaybeAutoCall".to_string(), bytecode::OpCode::TryCatch(info) => { diff --git a/crates/mq-lang/src/tarn/interpreter.rs b/crates/mq-lang/src/tarn/interpreter.rs index 69207af40..6bdcd1c10 100644 --- a/crates/mq-lang/src/tarn/interpreter.rs +++ b/crates/mq-lang/src/tarn/interpreter.rs @@ -1,5 +1,5 @@ -//! Tarn's bytecode dispatch loop: `run_chunk_inner_impl` and its opcode handlers, plus the -//! public `run_*` entry points that set up a top-level frame and call into it. +//! Tarn's bytecode dispatch loop: `run_frame_slice` and its opcode handlers, `run_frames`'s +//! explicit-frame-stack trampoline driving it, and the public `run_*` entry points. //! //! `errors` (the `VmError` type), `frame` (deadline/call-depth tracking and the `Locals`/stack //! pools), `calls` (binding arguments and invoking a callee), and `selectors` (applying a @@ -10,14 +10,18 @@ mod frame; mod selectors; use self::calls::{ - CallSite, FixedClosureCall, call_builtin, call_builtin_args, call_fixed_closure_from_stack, call_stack_value, - capture_upvalues, negate_ident, + CallSite, CallStep, ExactCallTarget, FixedClosureCall, KnownFixedChunkCall, apply_pending, call_builtin, + call_builtin_args, call_exact_fixed_chunk_0, call_exact_fixed_chunk_1, call_exact_fixed_chunk_2, + call_fixed_closure_from_stack, call_known_fixed_chunk_from_stack, call_self_chunk_from_stack, call_stack_value, + call_static_chunk_from_stack, capture_upvalues, negate_ident, }; use self::selectors::{eval_compact_selector_expr, eval_selector_expr, eval_selector_expr_with_args, type_check}; -use super::bytecode::{BinaryOp, Chunk, OpCode, SELF_SLOT}; +use super::bytecode::{BinaryOp, Chunk, OpCode, SELF_SLOT, TryCatchInfo}; use super::compiler::CompiledProgram; +#[cfg(feature = "debugger")] +use super::value::Cell; use super::value::VmClosureValue; -use super::value::{Cell, Closure, Locals, StackValue, read_cell, write_cell}; +use super::value::{Closure, Locals, StackValue, read_cell, write_cell}; #[cfg(feature = "debugger")] use crate::ast::TokenId; use crate::ast::constants::builtins; @@ -27,11 +31,13 @@ use crate::runtime::host::HostFunctions; use crate::runtime::runtime_value::{self, RuntimeValue}; use crate::selector::Selector; use crate::tarn::VmEnv; +#[cfg(feature = "vm-profile")] +use crate::vm_profile; use crate::{Ident, Shared}; pub(crate) use errors::VmError; use errors::{VmResult, error_dict, flow_break_value, flow_continue, locate}; pub(crate) use frame::ExecutionPools; -use frame::{ExecutionContext, ExecutionLimits}; +use frame::{Continuation, ExecutionContext, ExecutionLimits, Frame, TryBody}; use std::sync::LazyLock; use std::time::Duration; @@ -103,18 +109,6 @@ pub(crate) fn capture_slots(chunk: &Chunk, names: &[Ident]) -> Vec .collect() } -/// Runs a compiled program. -#[cfg(test)] -pub(crate) fn run( - compiled: &CompiledProgram, - input: RuntimeValue, - host_functions: &HostFunctions, - timeout: Option, - max_call_stack_depth: u32, -) -> VmResult { - run_with_globals(compiled, input, host_functions, timeout, max_call_stack_depth, &[]) -} - /// Runs a compiled program with Engine-defined globals. pub(crate) fn run_with_globals( compiled: &CompiledProgram, @@ -396,7 +390,7 @@ fn run_impl_with_bindings( let mut limits = ExecutionLimits::new(options.timeout, options.max_call_stack_depth, pools); let top_level_chunk = &compiled.chunks[0]; let captures_local_slots = top_level_chunk.captures_local_slots(); - let locals = limits.take_locals(top_level_chunk.local_count, captures_local_slots); + let mut locals = limits.take_locals(top_level_chunk.local_count, top_level_chunk.captured_local_slots()); locals.set(SELF_SLOT, StackValue::Value(input)); if initial_bindings.len() + 1 > locals.len() { if !captures_local_slots { @@ -419,7 +413,6 @@ fn run_impl_with_bindings( 0, &compiled.chunks, locals, - &[], &mut execution, #[cfg(feature = "debugger")] debug, @@ -445,7 +438,7 @@ fn run_impl_capturing_locals_with_env( let chunks = &compiled.chunks; let top_level_chunk = &chunks[0]; let reusable_locals = !top_level_chunk.captures_local_slots(); - let locals = limits.take_locals(top_level_chunk.local_count, top_level_chunk.captures_local_slots()); + let mut locals = limits.take_locals(top_level_chunk.local_count, top_level_chunk.captured_local_slots()); locals.set(SELF_SLOT, StackValue::Value(input)); if bindings.len() + 1 > locals.len() { if reusable_locals { @@ -461,18 +454,15 @@ fn run_impl_capturing_locals_with_env( locals.set(slot as u16 + 1, StackValue::Value(value)); } - let mut stack = limits.take_stack(); let mut execution = ExecutionContext { env, limits: &mut limits, host_functions: options.host_functions, }; - let raw_result = run_chunk_inner( - 0, + let initial = Frame::new(0, None, locals, None, reusable_locals, Continuation::Push); + let (raw_result, locals) = run_frames( + initial, chunks, - &locals, - &[], - &mut stack, &mut execution, #[cfg(feature = "debugger")] debug, @@ -485,7 +475,6 @@ fn run_impl_capturing_locals_with_env( .map(|value| (*name, into_runtime_value(value, chunks))) }) .collect(); - execution.limits.recycle_stack(stack); if reusable_locals { execution.limits.recycle_locals(locals); } @@ -545,7 +534,7 @@ struct DebugBindings { } #[cfg(feature = "debugger")] -fn apply_debug_updates(frame: &VmDebugFrame, locals: &Locals, upvalues: &[Cell]) { +fn apply_debug_updates(frame: &VmDebugFrame, locals: &mut Locals, upvalues: &[Cell]) { for update in frame.take_pending_updates() { if update.is_upvalue { if let Some(cell) = upvalues.get(update.slot as usize) { @@ -557,22 +546,19 @@ fn apply_debug_updates(frame: &VmDebugFrame, locals: &Locals, upvalues: &[Cell]) } } +/// For callers that don't need the bottom frame's `Locals` back afterward. fn run_chunk( chunk_index: u16, chunks: &Shared>, locals: Locals, - upvalues: &[Cell], execution: &mut ExecutionContext<'_>, #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, ) -> VmResult { let reusable_locals = !chunks[chunk_index as usize].captures_local_slots(); - let mut stack = execution.limits.take_stack(); - let result = run_chunk_inner( - chunk_index, + let initial = Frame::new(chunk_index, None, locals, None, reusable_locals, Continuation::Push); + let (result, locals) = run_frames( + initial, chunks, - &locals, - upvalues, - &mut stack, execution, #[cfg(feature = "debugger")] debug, @@ -580,62 +566,314 @@ fn run_chunk( if reusable_locals { execution.limits.recycle_locals(locals); } - execution.limits.recycle_stack(stack); result } -fn run_chunk_inner( - chunk_index: u16, - chunks: &Shared>, - locals: &Locals, - upvalues: &[Cell], - stack: &mut Vec, +enum FrameOutcome { + Enter(Frame), + /// A call followed immediately by `Return`; the callee can replace this frame. + TailEnter(Frame), + Complete(StackValue), +} + +/// The trampoline: an explicit `Vec` replaces Rust's own call stack, so mq call depth is +/// decoupled from Rust stack depth. Returns the bottom frame's `Locals` unrecycled. +fn run_frames( + initial: Frame, + root_chunks: &Shared>, execution: &mut ExecutionContext<'_>, #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, -) -> VmResult { - if execution.limits.has_deadline() { - run_chunk_inner_impl::( - chunk_index, - chunks, - locals, - upvalues, - stack, +) -> (VmResult, Locals) { + let mut operand_stack = execution.limits.take_stack(); + let mut frames = execution.limits.take_frame_stack(); + let result = if execution.limits.has_deadline() { + run_frames_impl::( + initial, + root_chunks, + &mut frames, + &mut operand_stack, execution, #[cfg(feature = "debugger")] debug, ) } else { - run_chunk_inner_impl::( - chunk_index, - chunks, - locals, - upvalues, - stack, + run_frames_impl::( + initial, + root_chunks, + &mut frames, + &mut operand_stack, execution, #[cfg(feature = "debugger")] debug, ) + }; + execution.limits.recycle_frame_stack(frames); + execution.limits.recycle_stack(operand_stack); + result +} + +fn run_frames_impl( + initial: Frame, + root_chunks: &Shared>, + frames: &mut Vec, + operand_stack: &mut Vec, + execution: &mut ExecutionContext<'_>, + #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, +) -> (VmResult, Locals) { + frames.push(initial); + + 'frames: loop { + let frame = frames.last_mut().expect("the frame stack is never empty here"); + let outcome = run_frame_slice::( + frame, + root_chunks, + operand_stack, + execution, + #[cfg(feature = "debugger")] + debug, + ); + + let value = match outcome { + Ok(FrameOutcome::Enter(mut new_frame)) => { + new_frame.stack_base = operand_stack.len(); + if let Err(e) = execution.limits.push_frame( + frames, + new_frame, + #[cfg(feature = "debugger")] + debug, + ) { + let e = locate_at_top(frames, root_chunks, e); + match unwind( + e, + frames, + root_chunks, + operand_stack, + execution, + #[cfg(feature = "debugger")] + debug, + ) { + Ok(()) => continue 'frames, + Err((e, locals)) => return (Err(e), locals), + } + } + continue 'frames; + } + Ok(FrameOutcome::TailEnter(mut new_frame)) => { + let caller = frames.last().expect("the frame stack is never empty here"); + new_frame.stack_base = caller.stack_base; + execution.limits.replace_top_frame( + frames, + new_frame, + #[cfg(feature = "debugger")] + debug, + ); + continue 'frames; + } + Ok(FrameOutcome::Complete(value)) => value, + Err(e) => match unwind( + e, + frames, + root_chunks, + operand_stack, + execution, + #[cfg(feature = "debugger")] + debug, + ) { + Ok(()) => continue 'frames, + Err((e, locals)) => return (Err(e), locals), + }, + }; + + if frames.len() == 1 { + let finished = frames.pop().expect("just checked len() == 1"); + return (Ok(value), finished.locals); + } + let continuation = execution + .limits + .pop_frame( + frames, + #[cfg(feature = "debugger")] + debug, + ) + .expect("just checked len() > 1"); + match continuation { + Continuation::Push | Continuation::TryBody(_) => { + operand_stack.push(value); + } + Continuation::ResumeBindParams(pending) => { + let next = match apply_pending(*pending, value, execution) { + Ok(next) => next, + Err(e) => { + let e = locate_at_top(frames, root_chunks, e); + match unwind( + e, + frames, + root_chunks, + operand_stack, + execution, + #[cfg(feature = "debugger")] + debug, + ) { + Ok(()) => continue 'frames, + Err((e, locals)) => return (Err(e), locals), + } + } + }; + let mut next = next; + next.stack_base = operand_stack.len(); + if let Err(e) = execution.limits.push_frame( + frames, + next, + #[cfg(feature = "debugger")] + debug, + ) { + let e = locate_at_top(frames, root_chunks, e); + match unwind( + e, + frames, + root_chunks, + operand_stack, + execution, + #[cfg(feature = "debugger")] + debug, + ) { + Ok(()) => continue 'frames, + Err((e, locals)) => return (Err(e), locals), + } + } + } + } } } -fn run_chunk_inner_impl( - chunk_index: u16, - chunks: &Shared>, - locals: &Locals, - upvalues: &[Cell], +/// `locate`s `e` at the still-suspended calling frame's own chunk/`ip`. +fn locate_at_top(frames: &[Frame], root_chunks: &Shared>, e: VmError) -> VmError { + let caller = frames.last().expect("the frame stack is never empty here"); + let chunks = caller.chunks.as_ref().unwrap_or(root_chunks); + locate(&chunks[caller.chunk_index as usize], caller.ip, e) +} + +/// Pops frames until a `try` body catches `e` (`Ok(())`) or the stack empties (`Err`). +#[allow( + clippy::ptr_arg, + reason = "unwinding may truncate and push onto the shared operand stack" +)] +fn unwind( + mut e: VmError, + frames: &mut Vec, + root_chunks: &Shared>, + operand_stack: &mut Vec, + execution: &mut ExecutionContext<'_>, + #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, +) -> Result<(), (VmError, Locals)> { + loop { + if frames.len() == 1 { + let finished = frames.pop().expect("just checked len() == 1"); + return Err((e, finished.locals)); + } + let failed_stack_base = frames.last().expect("just checked len() > 1").stack_base; + let continuation = execution + .limits + .pop_frame( + frames, + #[cfg(feature = "debugger")] + debug, + ) + .expect("just checked len() > 1"); + // Discard partial operands the failed frame left above its own stack_base (e.g. an + // in-progress array/dict literal), matching the truncation `run_frame_slice` does + // on success so a catch frame, or the next frame up the chain, starts clean. + operand_stack.truncate(failed_stack_base); + match continuation { + Continuation::Push => continue, + Continuation::ResumeBindParams(pending) => { + execution.limits.recycle_pending_locals(*pending); + continue; + } + Continuation::TryBody(body) => { + let TryBody { + catch_closure, + has_binder, + break_acc_slot, + break_offset, + continue_offset, + } = *body; + if let Some(value) = flow_break_value(&e) { + let (Some(acc_slot), Some(offset)) = (break_acc_slot, break_offset) else { + continue; + }; + let parent = frames.last_mut().expect("just checked len() > 1"); + if let Some(value) = value { + parent.locals.set(acc_slot, StackValue::Value(value)); + } + parent.ip = (parent.ip as i64 + offset as i64) as usize; + return Ok(()); + } + if flow_continue(&e) { + let Some(offset) = continue_offset else { + continue; + }; + let parent = frames.last_mut().expect("just checked len() > 1"); + parent.ip = (parent.ip as i64 + offset as i64) as usize; + return Ok(()); + } + let parent = frames.last().expect("just checked len() > 1"); + let catch_chunks = parent.chunks.as_ref().unwrap_or(root_chunks); + let catch_chunk = &catch_chunks[catch_closure.chunk_index as usize]; + let mut catch_locals = execution + .limits + .take_locals(catch_chunk.local_count, catch_chunk.captured_local_slots()); + catch_locals.set(SELF_SLOT, parent.locals.get(SELF_SLOT)); + if has_binder { + catch_locals.set(1, StackValue::Value(error_dict(&e))); + } + let mut catch_frame = Frame::new( + catch_closure.chunk_index, + parent.chunks.clone(), + catch_locals, + catch_closure.upvalues.clone(), + !catch_chunk.captures_local_slots(), + Continuation::Push, + ); + catch_frame.stack_base = operand_stack.len(); + match execution.limits.push_frame( + frames, + catch_frame, + #[cfg(feature = "debugger")] + debug, + ) { + Ok(()) => return Ok(()), + Err(new_e) => { + e = locate_at_top(&*frames, root_chunks, new_e); + continue; + } + } + } + } + } +} + +fn run_frame_slice( + frame: &mut Frame, + root_chunks: &Shared>, stack: &mut Vec, execution: &mut ExecutionContext<'_>, #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, -) -> VmResult { - let chunk = &chunks[chunk_index as usize]; - let mut ip: usize = 0; +) -> VmResult { + let chunks = frame.chunks.as_ref().unwrap_or(root_chunks); + let chunk = &chunks[frame.chunk_index as usize]; + let locals = &mut frame.locals; + let upvalues = frame.upvalues.as_deref().map_or_else(|| &[][..], Vec::as_slice); + let mut ip = frame.ip; macro_rules! pop { - () => { - stack - .pop() - .ok_or_else(|| locate(chunk, ip, VmError::Corrupt("stack underflow")))? - }; + () => {{ + if stack.len() <= frame.stack_base { + return Err(locate(chunk, ip, VmError::Corrupt("stack underflow"))); + } + // SAFETY: the length check above proves the stack is non-empty. + unsafe { stack.pop().unwrap_unchecked() } + }}; } macro_rules! pop_value { () => {{ into_runtime_value(pop!(), chunks) }}; @@ -646,12 +884,25 @@ fn run_chunk_inner_impl( }; } - while ip < chunk.code.len() { + let outcome = 'dispatch: loop { + if ip >= chunk.code.len() { + let value = if stack.len() > frame.stack_base { + // SAFETY: the length check above proves the stack is non-empty. + unsafe { stack.pop().unwrap_unchecked() } + } else { + StackValue::Value(RuntimeValue::None) + }; + break 'dispatch FrameOutcome::Complete(value); + } if CHECK_TIMEOUT { execution.limits.check().map_err(|e| locate(chunk, ip, e))?; } let op = &chunk.code[ip]; ip += 1; + #[cfg(feature = "vm-profile")] + if op.is_profiled_instruction() { + vm_profile::record_opcode(op.profile_name()); + } match op { #[cfg(feature = "debugger")] @@ -755,7 +1006,7 @@ fn run_chunk_inner_impl( let captured = capture_upvalues(sources, locals, upvalues); stack.push(StackValue::Closure(Shared::new(Closure { chunk_index: *target_chunk, - upvalues: captured, + upvalues: (!captured.is_empty()).then(|| Shared::new(captured)), }))); } OpCode::MakeStaticClosure(index) => { @@ -823,6 +1074,52 @@ fn run_chunk_inner_impl( .map_err(|e| locate(chunk, ip, e))?, )); } + OpCode::UpdateLocalConst { op, local, constant } => { + let a = local_runtime_value(locals, *local, chunks)?; + // SAFETY: `verify_chunks` validates every constant index before execution. + let b = unsafe { chunk.constants.get_unchecked(*constant as usize) }.clone(); + let value = eval_binary_op(*op, a, b, locals, chunks, execution.env, execution.host_functions) + .map_err(|e| locate(chunk, ip, e))?; + // SAFETY: `verify_chunks` validates every local slot before execution. + unsafe { locals.set_unchecked(*local, StackValue::Value(value)) }; + } + OpCode::UpdateLocalLocal { op, local, value } => { + let a = local_runtime_value(locals, *local, chunks)?; + let b = local_runtime_value(locals, *value, chunks)?; + let result = eval_binary_op(*op, a, b, locals, chunks, execution.env, execution.host_functions) + .map_err(|e| locate(chunk, ip, e))?; + // SAFETY: `verify_chunks` validates every local slot before execution. + unsafe { locals.set_unchecked(*local, StackValue::Value(result)) }; + } + OpCode::JumpIfFalseLocalLocal { + op, + left, + right, + offset, + } => { + let a = local_runtime_value(locals, *left, chunks)?; + let b = local_runtime_value(locals, *right, chunks)?; + let cond = eval_binary_op(*op, a, b, locals, chunks, execution.env, execution.host_functions) + .map_err(|e| locate(chunk, ip, e))?; + if !cond.is_truthy() { + ip = (ip as i64 + *offset as i64) as usize; + } + } + OpCode::JumpIfFalseLocalConst { + op, + local, + constant, + offset, + } => { + let a = local_runtime_value(locals, *local, chunks)?; + // SAFETY: `verify_chunks` validates every constant index before execution. + let b = unsafe { chunk.constants.get_unchecked(*constant as usize) }.clone(); + let cond = eval_binary_op(*op, a, b, locals, chunks, execution.env, execution.host_functions) + .map_err(|e| locate(chunk, ip, e))?; + if !cond.is_truthy() { + ip = (ip as i64 + *offset as i64) as usize; + } + } OpCode::Neg => { let a = pop_value!(); stack.push(StackValue::Value(match a { @@ -922,25 +1219,13 @@ fn run_chunk_inner_impl( value_slot, exit_offset, } => { - let index = locals.get(*index_slot); - let StackValue::Value(RuntimeValue::Number(index)) = index else { - bail!(VmError::Corrupt("ForeachNext has invalid loop state")); - }; - let index_value = index.value(); - let (array_len, value) = locals - .array_len_and_element_at(*array_slot, index_value as usize) + // SAFETY: `verify_chunks` validates every local slot before execution. + let value = unsafe { locals.foreach_next(*array_slot, *index_slot, *value_slot, SELF_SLOT) } .map_err(|e| locate(chunk, ip, VmError::Corrupt(e)))?; - if index_value >= array_len as f64 { + if value.is_none() { ip = (ip as i64 + *exit_offset as i64) as usize; continue; } - let value = value.unwrap_or(RuntimeValue::None); - locals.set( - *index_slot, - StackValue::Value(RuntimeValue::Number(Number::new(index_value + 1.0))), - ); - locals.set(*value_slot, StackValue::Value(value.clone())); - locals.set(SELF_SLOT, StackValue::Value(value)); } OpCode::ForeachCollect(slot) => { let value = pop_value!(); @@ -997,16 +1282,41 @@ fn run_chunk_inner_impl( .len() .checked_sub(*n as usize) .ok_or_else(|| locate(chunk, ip, VmError::Corrupt("stack underflow in InterpString")))?; + if start < frame.stack_base { + bail!(VmError::Corrupt("stack underflow in InterpString")); + } let value = interp_string(&stack[start..], chunks); stack.truncate(start); stack.push(StackValue::Value(value)); } OpCode::CallBuiltin(ident, argc) => { - let mut args = Args::with_capacity(*argc as usize); - for _ in 0..*argc { - args.push(pop_value!()); - } - args.reverse(); + // Most direct builtin calls have at most two arguments, which fit in `Args`' + // inline storage. Construct those in evaluation order directly; larger calls + // retain the compact generic pop-and-reverse path. + let args = match *argc { + 0 => Args::new(), + 1 => { + let mut args = Args::new(); + args.push(pop_value!()); + args + } + 2 => { + let second = pop_value!(); + let first = pop_value!(); + let mut args = Args::new(); + args.push(first); + args.push(second); + args + } + _ => { + let mut args = Args::with_capacity(*argc as usize); + for _ in 0..*argc { + args.push(pop_value!()); + } + args.reverse(); + args + } + }; let result = call_builtin_args( ident, args, @@ -1017,6 +1327,193 @@ fn run_chunk_inner_impl( .map_err(|e| locate(chunk, ip, e))?; stack.push(StackValue::Value(result)); } + OpCode::CallStatic(chunk_index, argc) => { + let new_frame = call_static_chunk_from_stack( + *chunk_index, + *argc, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + chunks, + execution, + )?; + break 'dispatch FrameOutcome::Enter(new_frame); + } + OpCode::CallStaticExact0(target) => { + let new_frame = call_exact_fixed_chunk_0( + ExactCallTarget { + chunk_index: target.chunk_index, + local_count: target.local_count, + captured_local_slots: &[], + }, + None, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + execution, + ); + break 'dispatch FrameOutcome::Enter(new_frame); + } + OpCode::CallStaticExact1(target) => { + let new_frame = call_exact_fixed_chunk_1( + ExactCallTarget { + chunk_index: target.chunk_index, + local_count: target.local_count, + captured_local_slots: &[], + }, + None, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + execution, + )?; + break 'dispatch FrameOutcome::Enter(new_frame); + } + OpCode::CallStaticExact2(target) => { + let new_frame = call_exact_fixed_chunk_2( + ExactCallTarget { + chunk_index: target.chunk_index, + local_count: target.local_count, + captured_local_slots: &[], + }, + None, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + execution, + )?; + break 'dispatch FrameOutcome::Enter(new_frame); + } + OpCode::CallStaticExact(chunk_index, argc) | OpCode::CallStaticImplicitSelf(chunk_index, argc) => { + let new_frame = call_known_fixed_chunk_from_stack( + KnownFixedChunkCall { + chunk_index: *chunk_index, + upvalues: None, + argc: *argc, + uses_implicit_self: matches!(op, OpCode::CallStaticImplicitSelf(..)), + remove_callee: false, + }, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + chunks, + execution, + )?; + break 'dispatch FrameOutcome::Enter(new_frame); + } + OpCode::CallSelf(argc) => { + let new_frame = call_self_chunk_from_stack( + frame.chunk_index, + frame.upvalues.clone(), + *argc, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + chunks, + execution, + )?; + break 'dispatch tail_call_outcome(chunk, ip, new_frame); + } + OpCode::CallSelfExact0 => { + let new_frame = call_exact_fixed_chunk_0( + ExactCallTarget { + chunk_index: frame.chunk_index, + local_count: chunk.local_count, + captured_local_slots: chunk.captured_local_slots(), + }, + frame.upvalues.clone(), + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + execution, + ); + break 'dispatch tail_call_outcome(chunk, ip, new_frame); + } + OpCode::CallSelfExact1 => { + let new_frame = call_exact_fixed_chunk_1( + ExactCallTarget { + chunk_index: frame.chunk_index, + local_count: chunk.local_count, + captured_local_slots: chunk.captured_local_slots(), + }, + frame.upvalues.clone(), + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + execution, + )?; + break 'dispatch tail_call_outcome(chunk, ip, new_frame); + } + OpCode::CallSelfExact2 => { + let new_frame = call_exact_fixed_chunk_2( + ExactCallTarget { + chunk_index: frame.chunk_index, + local_count: chunk.local_count, + captured_local_slots: chunk.captured_local_slots(), + }, + frame.upvalues.clone(), + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + execution, + )?; + break 'dispatch tail_call_outcome(chunk, ip, new_frame); + } + OpCode::CallSelfExact(argc) | OpCode::CallSelfImplicitSelf(argc) => { + let new_frame = call_known_fixed_chunk_from_stack( + KnownFixedChunkCall { + chunk_index: frame.chunk_index, + upvalues: frame.upvalues.clone(), + argc: *argc, + uses_implicit_self: matches!(op, OpCode::CallSelfImplicitSelf(..)), + remove_callee: false, + }, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + chunks, + execution, + )?; + break 'dispatch tail_call_outcome(chunk, ip, new_frame); + } OpCode::CallLocal(slot, argc) => { let callee = locals.get(*slot); if let StackValue::Closure(closure) = &callee @@ -1025,21 +1522,23 @@ fn run_chunk_inner_impl( .fixed_required_arity() .is_some() { - let result = call_fixed_closure_from_stack( + let new_frame = call_fixed_closure_from_stack( FixedClosureCall { closure, argc: *argc, remove_callee: false, }, stack, - CallSite { locals, chunk, ip }, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, chunks, execution, - #[cfg(feature = "debugger")] - debug, )?; - stack.push(result); - continue; + break 'dispatch FrameOutcome::Enter(new_frame); } // Pooled, not `Vec::with_capacity`: this path (non-fixed-arity callees — // variadic/optional params, `partial`-bound closures) runs often enough in @@ -1049,23 +1548,82 @@ fn run_chunk_inner_impl( args.push(pop!()); } args.reverse(); - let call_result = call_stack_value( + let step = call_stack_value( callee, &mut args, - CallSite { locals, chunk, ip }, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + chunks, + execution, + ); + execution.limits.recycle_stack(args); + match step? { + CallStep::Value(v) => stack.push(v), + CallStep::Enter(new_frame) => break 'dispatch FrameOutcome::Enter(new_frame), + } + } + OpCode::CallUpvalue(index, argc) => { + // SAFETY: `verify_chunks` validates every upvalue index before execution. + let callee = read_cell(unsafe { upvalues.get_unchecked(*index as usize) }); + if let StackValue::Closure(closure) = &callee + && chunks[closure.chunk_index as usize] + .param_shape + .fixed_required_arity() + .is_some() + { + let new_frame = call_fixed_closure_from_stack( + FixedClosureCall { + closure, + argc: *argc, + remove_callee: false, + }, + stack, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, + chunks, + execution, + )?; + break 'dispatch FrameOutcome::Enter(new_frame); + } + let mut args = execution.limits.take_stack(); + for _ in 0..*argc { + args.push(pop!()); + } + args.reverse(); + let step = call_stack_value( + callee, + &mut args, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, chunks, execution, - #[cfg(feature = "debugger")] - debug, ); execution.limits.recycle_stack(args); - stack.push(call_result?); + match step? { + CallStep::Value(v) => stack.push(v), + CallStep::Enter(new_frame) => break 'dispatch FrameOutcome::Enter(new_frame), + } } OpCode::CallValue(argc) => { let callee_index = stack .len() .checked_sub(*argc as usize + 1) .ok_or_else(|| locate(chunk, ip, VmError::Corrupt("stack underflow in CallValue")))?; + if callee_index < frame.stack_base { + bail!(VmError::Corrupt("stack underflow in CallValue")); + } if let StackValue::Closure(closure) = &stack[callee_index] && chunks[closure.chunk_index as usize] .param_shape @@ -1076,21 +1634,23 @@ fn run_chunk_inner_impl( // popped them. This avoids `Vec::remove(callee_index)`, which shifts // every argument and is especially costly for large calls. let closure = Shared::clone(closure); - let result = call_fixed_closure_from_stack( + let new_frame = call_fixed_closure_from_stack( FixedClosureCall { closure: &closure, argc: *argc, remove_callee: true, }, stack, - CallSite { locals, chunk, ip }, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, chunks, execution, - #[cfg(feature = "debugger")] - debug, )?; - stack.push(result); - continue; + break 'dispatch FrameOutcome::Enter(new_frame); } // See the `CallLocal` non-fixed-arity path above for why this is pooled. let mut args = execution.limits.take_stack(); @@ -1099,17 +1659,23 @@ fn run_chunk_inner_impl( } args.reverse(); let callee = pop!(); - let call_result = call_stack_value( + let step = call_stack_value( callee, &mut args, - CallSite { locals, chunk, ip }, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, chunks, execution, - #[cfg(feature = "debugger")] - debug, ); execution.limits.recycle_stack(args); - stack.push(call_result?); + match step? { + CallStep::Value(v) => stack.push(v), + CallStep::Enter(new_frame) => break 'dispatch FrameOutcome::Enter(new_frame), + } } OpCode::MaybeAutoCall => { let value = pop!(); @@ -1127,16 +1693,21 @@ fn run_chunk_inner_impl( _ => false, }; if eligible { - let result = call_stack_value( + match call_stack_value( value, &mut Vec::new(), - CallSite { locals, chunk, ip }, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), + }, chunks, execution, - #[cfg(feature = "debugger")] - debug, - )?; - stack.push(result); + )? { + CallStep::Value(v) => stack.push(v), + CallStep::Enter(new_frame) => break 'dispatch FrameOutcome::Enter(new_frame), + } } else { stack.push(value); } @@ -1144,27 +1715,20 @@ fn run_chunk_inner_impl( OpCode::TryCatch(info) => { let catch_closure = pop!(); let try_closure = pop!(); - match handle_try_catch( - TryCatchArgs { - has_binder: info.has_binder, - break_acc_slot: info.break_acc_slot, - break_offset: info.break_offset, - continue_offset: info.continue_offset, - catch_closure, - try_closure, + let new_frame = begin_try_catch( + info, + catch_closure, + try_closure, + CallSite { + locals, + chunk, + ip, + frame_chunks: frame.chunks.clone(), }, - CallSite { locals, chunk, ip }, chunks, execution, - #[cfg(feature = "debugger")] - debug, - )? { - TryCatchOutcome::Value(value) => stack.push(value), - TryCatchOutcome::JumpTo(offset) => { - ip = (ip as i64 + offset as i64) as usize; - continue; - } - } + )?; + break 'dispatch FrameOutcome::Enter(new_frame); } OpCode::FlowBreak(has_value) => { let value = if *has_value { Some(pop_value!()) } else { None }; @@ -1175,50 +1739,58 @@ fn run_chunk_inner_impl( bail!(VmError::DestructuringFailed); } OpCode::Return => { - return Ok(pop!()); + let v = pop!(); + break 'dispatch FrameOutcome::Complete(v); } } + }; + frame.ip = ip; + if let FrameOutcome::Complete(_) = &outcome { + stack.truncate(frame.stack_base); } - - Ok(stack.pop().unwrap_or(StackValue::Value(RuntimeValue::None))) + Ok(outcome) } -struct TryCatchArgs { - has_binder: bool, - break_acc_slot: Option, - break_offset: Option, - continue_offset: Option, - catch_closure: StackValue, - try_closure: StackValue, -} - -enum TryCatchOutcome { - Value(StackValue), - /// Loop control (`break`/`continue`) raised inside the try chunk bypasses the catch - /// and jumps to the enclosing loop's patched target — signaled back to the dispatch - /// loop instead of jumping directly, since this function doesn't own `ip`. - JumpTo(i32), +/// Marks a self call immediately followed by `Return` as a tail call. +/// +/// Only self calls use the frame-replacement path: other calls can require parameter binding +/// or preserve observable call-depth behavior. Keeping this check at self-call dispatch avoids +/// reclassifying every ordinary VM instruction in the hot loop. +#[inline(always)] +fn tail_call_outcome(chunk: &Chunk, next_ip: usize, frame: Frame) -> FrameOutcome { + if matches!(chunk.code.get(next_ip), Some(OpCode::Return)) { + FrameOutcome::TailEnter(frame) + } else { + FrameOutcome::Enter(frame) + } } -/// `try`/`catch` is rare and large; kept out of `run_chunk_inner_impl` to keep it small. +/// `try`/`catch` is rare; kept out of `run_frame_slice`. Builds the try body's `Frame` — `unwind` +/// handles the rest (routing success, or dispatching to `catch`/a loop jump on error). #[cold] #[inline(never)] -fn handle_try_catch( - args: TryCatchArgs, +fn begin_try_catch( + info: &TryCatchInfo, + catch_closure: StackValue, + try_closure: StackValue, call_site: CallSite<'_>, chunks: &Shared>, execution: &mut ExecutionContext<'_>, - #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, -) -> VmResult { - let CallSite { locals, chunk, ip } = call_site; - let StackValue::Closure(catch_closure) = args.catch_closure else { +) -> VmResult { + let CallSite { + locals, + chunk, + ip, + frame_chunks, + } = call_site; + let StackValue::Closure(catch_closure) = catch_closure else { return Err(locate( chunk, ip, VmError::Corrupt("TryCatch catch operand is not a closure"), )); }; - let StackValue::Closure(try_closure) = args.try_closure else { + let StackValue::Closure(try_closure) = try_closure else { return Err(locate( chunk, ip, @@ -1226,74 +1798,27 @@ fn handle_try_catch( )); }; let try_chunk = &chunks[try_closure.chunk_index as usize]; - let try_locals = execution + let mut try_locals = execution .limits - .take_locals(try_chunk.local_count, try_chunk.captures_local_slots()); + .take_locals(try_chunk.local_count, try_chunk.captured_local_slots()); try_locals.set(SELF_SLOT, locals.get(SELF_SLOT)); - if let Err(error) = execution.limits.enter_call() { - if !try_chunk.captures_local_slots() { - execution.limits.recycle_locals(try_locals); - } - return Err(locate(chunk, ip, error)); - } - let try_result = run_chunk( + Ok(Frame::new( try_closure.chunk_index, - chunks, + frame_chunks, try_locals, - &try_closure.upvalues, - execution, - #[cfg(feature = "debugger")] - debug, - ); - execution.limits.exit_call(); - match try_result { - Ok(value) => Ok(TryCatchOutcome::Value(value)), - Err(e) => { - if let Some(value) = flow_break_value(&e) { - let (Some(acc_slot), Some(offset)) = (args.break_acc_slot, args.break_offset) else { - return Err(e); - }; - if let Some(value) = value { - locals.set(acc_slot, StackValue::Value(value)); - } - return Ok(TryCatchOutcome::JumpTo(offset)); - } - if flow_continue(&e) { - let Some(offset) = args.continue_offset else { - return Err(e); - }; - return Ok(TryCatchOutcome::JumpTo(offset)); - } - let catch_chunk = &chunks[catch_closure.chunk_index as usize]; - let catch_locals = execution - .limits - .take_locals(catch_chunk.local_count, catch_chunk.captures_local_slots()); - catch_locals.set(SELF_SLOT, locals.get(SELF_SLOT)); - if args.has_binder { - catch_locals.set(1, StackValue::Value(error_dict(&e))); - } - if let Err(error) = execution.limits.enter_call() { - if !catch_chunk.captures_local_slots() { - execution.limits.recycle_locals(catch_locals); - } - return Err(locate(chunk, ip, error)); - } - let catch_result = run_chunk( - catch_closure.chunk_index, - chunks, - catch_locals, - &catch_closure.upvalues, - execution, - #[cfg(feature = "debugger")] - debug, - ); - execution.limits.exit_call(); - Ok(TryCatchOutcome::Value(catch_result?)) - } - } + try_closure.upvalues.clone(), + !try_chunk.captures_local_slots(), + Continuation::TryBody(Box::new(TryBody { + catch_closure, + has_binder: info.has_binder, + break_acc_slot: info.break_acc_slot, + break_offset: info.break_offset, + continue_offset: info.continue_offset, + })), + )) } -/// Rare spread-syntax opcodes, kept out of `run_chunk_inner_impl` (see `handle_try_catch`). +/// Rare spread-syntax opcodes, kept out of `run_frame_slice`. #[cold] #[inline(never)] fn array_spread(mut arr: RuntimeValue, source: RuntimeValue, chunk: &Chunk, ip: usize) -> VmResult { @@ -1363,7 +1888,7 @@ fn pop_value_from( Ok(into_runtime_value(v, chunks)) } -/// Rare array opcodes, kept out of `run_chunk_inner_impl` (see `handle_try_catch`). +/// Rare array opcodes, kept out of `run_frame_slice`. #[cold] #[inline(never)] fn array_misc_op( @@ -1450,7 +1975,9 @@ fn selector_op( } OpCode::SelectorMatchWithArgs(payload) => { let (selector, argc) = payload.as_ref(); - let mut args = Vec::with_capacity(*argc as usize); + // Selector arguments are usually one or two values. `Args` keeps those inline, + // avoiding a heap allocation for every parameterized selector evaluation. + let mut args = Args::with_capacity(*argc as usize); for _ in 0..*argc { args.push(pop_value_from(stack, chunks, chunk, ip)?); } diff --git a/crates/mq-lang/src/tarn/interpreter/calls.rs b/crates/mq-lang/src/tarn/interpreter/calls.rs index ea8576fa5..018926af0 100644 --- a/crates/mq-lang/src/tarn/interpreter/calls.rs +++ b/crates/mq-lang/src/tarn/interpreter/calls.rs @@ -1,10 +1,8 @@ -//! Invoking a callee: binding arguments into a new frame's locals and running its chunk. -//! Covers the generic (`call_stack_value`) and fixed-arity fast (`call_fixed_closure_from_stack`) -//! paths used by `CallLocal`/`CallValue`/`MaybeAutoCall`, and the shared parameter-binding logic -//! (`bind_params`) both funnel into. +//! Invoking a callee: binding arguments and producing the `Frame` for the trampoline to push, +//! instead of calling back into the dispatch loop directly. use super::errors::{VmError, VmResult, locate}; -use super::frame::{ExecutionContext, ExecutionLimits}; -use super::{current_self, into_runtime_value, run_chunk}; +use super::frame::{Continuation, ExecutionContext, Frame, PendingCall}; +use super::{current_self, into_runtime_value}; use crate::Shared; use crate::ast::constants::builtins; use crate::runtime::builtin::{self, Args}; @@ -13,14 +11,14 @@ use crate::runtime::runtime_value::RuntimeValue; use crate::tarn::VmEnv; use crate::tarn::bytecode::{Chunk, ParamBinding, ParamShape, SELF_SLOT, UpvalueSource}; use crate::tarn::value::{Cell, Closure, Locals, StackValue}; - -#[cfg(feature = "debugger")] -use super::DebugRuntime; +use std::collections::VecDeque; pub(super) struct CallSite<'a> { pub(super) locals: &'a Locals, pub(super) chunk: &'a Chunk, pub(super) ip: usize, + /// `None` represents the root chunk pool held by the active trampoline. + pub(super) frame_chunks: Option>>, } /// Static properties of a direct fixed-arity closure call. @@ -30,12 +28,35 @@ pub(super) struct FixedClosureCall<'a> { pub(super) remove_callee: bool, } -/// Runtime services shared by parameter binding and default-value evaluation. +/// Resolved target metadata shared by closure and static fixed-arity calls. +struct FixedChunkCall { + chunk_index: u16, + upvalues: Option>>, + argc: u16, + remove_callee: bool, +} + +/// Metadata embedded in an exact or implicit-self direct-call opcode. +pub(super) struct KnownFixedChunkCall { + pub(super) chunk_index: u16, + pub(super) upvalues: Option>>, + pub(super) argc: u16, + pub(super) uses_implicit_self: bool, + pub(super) remove_callee: bool, +} + +/// Metadata already available to a direct exact-call opcode or its active self frame. +pub(super) struct ExactCallTarget<'a> { + pub(super) chunk_index: u16, + pub(super) local_count: u16, + pub(super) captured_local_slots: &'a [u16], +} + +/// Chunk/pool access shared by parameter binding and default-value evaluation. struct ParameterContext<'chunks, 'execution> { chunks: &'chunks Shared>, - env: &'execution VmEnv, - limits: &'execution mut ExecutionLimits, - host_functions: &'execution HostFunctions, + frame_chunks: Option>>, + limits: &'execution mut super::frame::ExecutionLimits, } pub(super) fn capture_upvalues(sources: &[UpvalueSource], locals: &Locals, upvalues: &[Cell]) -> Vec { @@ -48,33 +69,37 @@ pub(super) fn capture_upvalues(sources: &[UpvalueSource], locals: &Locals, upval .collect() } +/// A resolved call: either an already-computed value (a native builtin), or a `Frame` to push. +pub(super) enum CallStep { + Value(StackValue), + Enter(Frame), +} + pub(super) fn call_stack_value( callee: StackValue, args: &mut Vec, call_site: CallSite<'_>, chunks: &Shared>, execution: &mut ExecutionContext<'_>, - #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, -) -> VmResult { +) -> VmResult { if let StackValue::Value(RuntimeValue::NativeFunction(ident)) = callee { // `drain` (rather than `into_iter`) leaves `args`'s allocation intact for the // caller to recycle, same as every other exit path below. // `Args` stores the common one- and two-argument cases inline, unlike `Vec`. let arg_values: Args = args.drain(..).map(|a| into_runtime_value(a, chunks)).collect(); let self_value = current_self(call_site.locals, chunks); - let result = call_builtin( - &ident, - &arg_values, - &self_value, - execution.env, - execution.host_functions, - ) - .map_err(|e| locate(call_site.chunk, call_site.ip, e))?; - return Ok(StackValue::Value(result)); + let result = call_builtin_args(&ident, arg_values, &self_value, execution.env, execution.host_functions) + .map_err(|e| locate(call_site.chunk, call_site.ip, e))?; + return Ok(CallStep::Value(StackValue::Value(result))); } - let (callee_chunks, callee_chunk_index, callee_upvalues): (&Shared>, u16, &[Cell]) = match &callee { - StackValue::Closure(closure) => (chunks, closure.chunk_index, &closure.upvalues), + let (callee_chunks, callee_chunk_index, callee_upvalues, callee_frame_chunks) = match &callee { + StackValue::Closure(closure) => ( + chunks, + closure.chunk_index, + closure.upvalues.clone(), + call_site.frame_chunks, + ), StackValue::Value(RuntimeValue::VmClosure(vc)) => { if !vc.bound_args.is_empty() { // `args` is a caller-owned pooled buffer. Prepend into a second pooled buffer, @@ -86,65 +111,35 @@ pub(super) fn call_stack_value( std::mem::swap(args, &mut combined); execution.limits.recycle_stack(combined); } - (&vc.chunks, vc.chunk_index, &vc.upvalues) + ( + &vc.chunks, + vc.chunk_index, + vc.upvalues.clone(), + Some(Shared::clone(&vc.chunks)), + ) } _ => return Err(locate(call_site.chunk, call_site.ip, VmError::NotCallable)), }; let callee_chunk = &callee_chunks[callee_chunk_index as usize]; - let callee_locals = execution + let mut callee_locals = execution .limits - .take_locals(callee_chunk.local_count, callee_chunk.captures_local_slots()); + .take_locals(callee_chunk.local_count, callee_chunk.captured_local_slots()); callee_locals.set(SELF_SLOT, call_site.locals.get(SELF_SLOT)); - if let Err(e) = execution.limits.enter_call() { - recycle_locals_if_possible(execution.limits, callee_locals, callee_chunk.captures_local_slots()); - return Err(locate(call_site.chunk, call_site.ip, e)); - } - if let Err(e) = bind_params( + + let frame = bind_params( &callee_chunk.param_shape, args, - &callee_locals, + callee_locals, callee_upvalues, + callee_chunk_index, &mut ParameterContext { chunks: callee_chunks, - env: execution.env, + frame_chunks: callee_frame_chunks, limits: execution.limits, - host_functions: execution.host_functions, }, - #[cfg(feature = "debugger")] - debug, - ) { - execution.limits.exit_call(); - recycle_locals_if_possible(execution.limits, callee_locals, callee_chunk.captures_local_slots()); - return Err(locate(call_site.chunk, call_site.ip, e)); - } - #[cfg(feature = "debugger")] - let caller_node = debug.current_node.clone(); - #[cfg(feature = "debugger")] - let pushed_call = if let Some(node) = &caller_node { - debug.call_stack.push(Shared::clone(node)); - true - } else { - false - }; - let call_result = run_chunk( - callee_chunk_index, - callee_chunks, - callee_locals, - callee_upvalues, - execution, - #[cfg(feature = "debugger")] - debug, - ); - execution.limits.exit_call(); - #[cfg(feature = "debugger")] - if pushed_call { - debug.call_stack.pop(); - } - #[cfg(feature = "debugger")] - { - debug.current_node = caller_node; - } - call_result + ) + .map_err(|e| locate(call_site.chunk, call_site.ip, e))?; + Ok(CallStep::Enter(frame)) } pub(super) fn call_fixed_closure_from_stack( @@ -153,10 +148,77 @@ pub(super) fn call_fixed_closure_from_stack( call_site: CallSite<'_>, chunks: &Shared>, execution: &mut ExecutionContext<'_>, - #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, -) -> VmResult { - let closure = call.closure; - let callee_chunk = &chunks[closure.chunk_index as usize]; +) -> VmResult { + call_fixed_chunk_from_stack( + FixedChunkCall { + chunk_index: call.closure.chunk_index, + upvalues: call.closure.upvalues.clone(), + argc: call.argc, + remove_callee: call.remove_callee, + }, + stack, + call_site, + chunks, + execution, + ) +} + +/// Builds a frame for a capture-free fixed-arity chunk. `CallStatic` uses this path so it does +/// not need to load or clone the closure stored in the defining local slot. +pub(super) fn call_static_chunk_from_stack( + chunk_index: u16, + argc: u16, + stack: &mut Vec, + call_site: CallSite<'_>, + chunks: &Shared>, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + call_fixed_chunk_from_stack( + FixedChunkCall { + chunk_index, + upvalues: None, + argc, + remove_callee: false, + }, + stack, + call_site, + chunks, + execution, + ) +} + +/// Builds a recursive frame using the current frame's captured environment directly. +pub(super) fn call_self_chunk_from_stack( + chunk_index: u16, + upvalues: Option>>, + argc: u16, + stack: &mut Vec, + call_site: CallSite<'_>, + chunks: &Shared>, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + call_fixed_chunk_from_stack( + FixedChunkCall { + chunk_index, + upvalues, + argc, + remove_callee: false, + }, + stack, + call_site, + chunks, + execution, + ) +} + +fn call_fixed_chunk_from_stack( + call: FixedChunkCall, + stack: &mut Vec, + call_site: CallSite<'_>, + chunks: &Shared>, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + let callee_chunk = &chunks[call.chunk_index as usize]; let Some(arity) = callee_chunk.param_shape.fixed_required_arity() else { return Err(locate( call_site.chunk, @@ -184,14 +246,49 @@ pub(super) fn call_fixed_closure_from_stack( )); } + call_known_fixed_chunk_from_stack( + KnownFixedChunkCall { + chunk_index: call.chunk_index, + upvalues: call.upvalues, + argc: call.argc, + uses_implicit_self, + remove_callee: call.remove_callee, + }, + stack, + call_site, + chunks, + execution, + ) +} + +/// Builds a frame for a direct fixed-arity call whose parameter form was established while +/// compiling bytecode. The verifier ensures the opcode agrees with the target chunk. +pub(super) fn call_known_fixed_chunk_from_stack( + call: KnownFixedChunkCall, + stack: &mut Vec, + call_site: CallSite<'_>, + chunks: &Shared>, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + let callee_chunk = &chunks[call.chunk_index as usize]; + let argc = call.argc as usize; + if stack.len() < argc + usize::from(call.remove_callee) { + return Err(locate( + call_site.chunk, + call_site.ip, + VmError::Corrupt("stack underflow in fixed closure call"), + )); + } + + let arity = argc + usize::from(call.uses_implicit_self); let initialized_slots = SELF_SLOT as usize + 1 + arity; - let callee_locals = execution.limits.take_locals_with_initialized_prefix( + let mut callee_locals = execution.limits.take_locals_with_initialized_prefix( callee_chunk.local_count, initialized_slots, - callee_chunk.captures_local_slots(), + callee_chunk.captured_local_slots(), ); let self_value = call_site.locals.get(SELF_SLOT); - let first_arg_slot = if uses_implicit_self { + let first_arg_slot = if call.uses_implicit_self { callee_locals.set(SELF_SLOT, self_value.clone()); callee_locals.set(SELF_SLOT + 1, self_value); SELF_SLOT as usize + 2 @@ -218,77 +315,226 @@ pub(super) fn call_fixed_closure_from_stack( VmError::Corrupt("stack underflow while removing fixed-call callee"), )); } + Ok(Frame::new( + call.chunk_index, + call_site.frame_chunks, + callee_locals, + call.upvalues, + !callee_chunk.captures_local_slots(), + Continuation::Push, + )) +} - if let Err(e) = execution.limits.enter_call() { - recycle_locals_if_possible(execution.limits, callee_locals, callee_chunk.captures_local_slots()); - return Err(locate(call_site.chunk, call_site.ip, e)); - } - #[cfg(feature = "debugger")] - let caller_node = debug.current_node.clone(); - #[cfg(feature = "debugger")] - let pushed_call = if let Some(node) = &caller_node { - debug.call_stack.push(Shared::clone(node)); - true - } else { - false +/// Builds a frame for a verified zero-argument direct call without entering the generic +/// fixed-call binder. +pub(super) fn call_exact_fixed_chunk_0( + target: ExactCallTarget<'_>, + upvalues: Option>>, + call_site: CallSite<'_>, + execution: &mut ExecutionContext<'_>, +) -> Frame { + let mut callee_locals = execution.limits.take_locals_with_initialized_prefix( + target.local_count, + SELF_SLOT as usize + 1, + target.captured_local_slots, + ); + callee_locals.set(SELF_SLOT, call_site.locals.get(SELF_SLOT)); + exact_fixed_frame(target, call_site.frame_chunks, callee_locals, upvalues) +} + +/// Builds a frame for a verified one-argument direct call without a parameter-binding loop. +pub(super) fn call_exact_fixed_chunk_1( + target: ExactCallTarget<'_>, + upvalues: Option>>, + stack: &mut Vec, + call_site: CallSite<'_>, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + let Some(argument) = stack.pop() else { + return Err(locate( + call_site.chunk, + call_site.ip, + VmError::Corrupt("stack underflow in one-argument exact fixed call"), + )); }; - let call_result = run_chunk( - closure.chunk_index, - chunks, + let mut callee_locals = execution.limits.take_locals_with_initialized_prefix( + target.local_count, + SELF_SLOT as usize + 2, + target.captured_local_slots, + ); + callee_locals.set(SELF_SLOT, call_site.locals.get(SELF_SLOT)); + callee_locals.set(SELF_SLOT + 1, argument); + Ok(exact_fixed_frame( + target, + call_site.frame_chunks, callee_locals, - &closure.upvalues, - execution, - #[cfg(feature = "debugger")] - debug, + upvalues, + )) +} + +/// Builds a frame for a verified two-argument direct call without a parameter-binding loop. +pub(super) fn call_exact_fixed_chunk_2( + target: ExactCallTarget<'_>, + upvalues: Option>>, + stack: &mut Vec, + call_site: CallSite<'_>, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + let Some(second_argument) = stack.pop() else { + return Err(locate( + call_site.chunk, + call_site.ip, + VmError::Corrupt("stack underflow in two-argument exact fixed call"), + )); + }; + let Some(first_argument) = stack.pop() else { + stack.push(second_argument); + return Err(locate( + call_site.chunk, + call_site.ip, + VmError::Corrupt("stack underflow in two-argument exact fixed call"), + )); + }; + let mut callee_locals = execution.limits.take_locals_with_initialized_prefix( + target.local_count, + SELF_SLOT as usize + 3, + target.captured_local_slots, ); - execution.limits.exit_call(); - #[cfg(feature = "debugger")] - if pushed_call { - debug.call_stack.pop(); - } - #[cfg(feature = "debugger")] - { - debug.current_node = caller_node; - } - call_result + callee_locals.set(SELF_SLOT, call_site.locals.get(SELF_SLOT)); + callee_locals.set(SELF_SLOT + 1, first_argument); + callee_locals.set(SELF_SLOT + 2, second_argument); + Ok(exact_fixed_frame( + target, + call_site.frame_chunks, + callee_locals, + upvalues, + )) +} + +fn exact_fixed_frame( + target: ExactCallTarget<'_>, + frame_chunks: Option>>, + callee_locals: Locals, + upvalues: Option>>, +) -> Frame { + Frame::new( + target.chunk_index, + frame_chunks, + callee_locals, + upvalues, + target.captured_local_slots.is_empty(), + Continuation::Push, + ) } +/// Binds `args` and returns the next `Frame` to push: the callee's body, or (if a missing +/// argument needs its default) the default-value expression to run first. fn bind_params( shape: &ParamShape, args: &mut Vec, - callee_locals: &Locals, - enclosing_upvalues: &[Cell], + mut callee_locals: Locals, + callee_upvalues: Option>>, + callee_chunk_index: u16, context: &mut ParameterContext<'_, '_>, - #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, -) -> VmResult<()> { +) -> VmResult { if let Some(arity) = shape.fixed_required_arity() { - return bind_fixed_required_params(arity, args, callee_locals, context.chunks); + bind_fixed_required_params(arity, args, &mut callee_locals, context.chunks)?; + return Ok(build_callee_frame( + callee_locals, + callee_upvalues, + callee_chunk_index, + context, + )); } let arg_count = args.len(); let param_count = shape.bindings.len(); let use_self_param = parameter_uses_implicit_self(shape, arg_count)?; - let mut bindings = shape.bindings.iter(); // `drain` (rather than `into_iter`) leaves `args`'s allocation for the caller to recycle. - let mut args = args.drain(..); - - if use_self_param && let Some(binding) = bindings.next() { - let self_value = current_self(callee_locals, context.chunks); + let remaining_args: VecDeque = args.drain(..).collect(); + let mut start_index = 0; + if use_self_param && let Some(binding) = shape.bindings.first() { + let self_value = current_self(&callee_locals, context.chunks); callee_locals.set(binding.slot(), StackValue::Value(self_value)); + start_index = 1; } - for binding in bindings { - match binding { + resume_bind_params( + shape, + remaining_args, + callee_locals, + callee_upvalues, + callee_chunk_index, + context, + start_index, + arg_count, + param_count, + ) +} + +/// Stores a completed default-value expression's result and resumes binding. +pub(super) fn apply_pending( + mut pending: PendingCall, + value: StackValue, + execution: &mut ExecutionContext<'_>, +) -> VmResult { + pending.callee_locals.set(pending.target_slot, value); + let PendingCall { + callee_locals, + callee_locals_reusable: _, + callee_upvalues, + callee_chunk_index, + callee_chunks, + remaining_args, + target_slot: _, + next_index, + arg_count, + param_count, + } = pending; + let shape = &callee_chunks[callee_chunk_index as usize].param_shape; + let mut context = ParameterContext { + chunks: &callee_chunks, + frame_chunks: Some(Shared::clone(&callee_chunks)), + limits: execution.limits, + }; + resume_bind_params( + shape, + remaining_args, + callee_locals, + callee_upvalues, + callee_chunk_index, + &mut context, + next_index, + arg_count, + param_count, + ) +} + +/// Resumes binding `shape.bindings[start_index..]`. +#[allow(clippy::too_many_arguments)] +fn resume_bind_params( + shape: &ParamShape, + mut remaining_args: VecDeque, + mut callee_locals: Locals, + callee_upvalues: Option>>, + callee_chunk_index: u16, + context: &mut ParameterContext<'_, '_>, + start_index: usize, + arg_count: usize, + param_count: usize, +) -> VmResult { + for index in start_index..shape.bindings.len() { + match &shape.bindings[index] { ParamBinding::Variadic(slot) => { - let collected: Vec = args - .by_ref() + let collected: Vec = remaining_args + .drain(..) .map(|arg| into_runtime_value(arg, context.chunks)) .collect(); callee_locals.set(*slot, StackValue::Value(RuntimeValue::Array(Shared::new(collected)))); } ParamBinding::Required(slot) => { - let Some(value) = args.next() else { + let Some(value) = remaining_args.pop_front() else { return Err(VmError::ArityMismatch { expected: param_count, actual: arg_count, @@ -297,50 +543,71 @@ fn bind_params( callee_locals.set(*slot, value); } ParamBinding::Optional(slot, default_chunk, default_upvalues) => { - if let Some(value) = args.next() { + if let Some(value) = remaining_args.pop_front() { callee_locals.set(*slot, value); } else { - let captured = capture_upvalues(default_upvalues, callee_locals, enclosing_upvalues); + let captured = capture_upvalues( + default_upvalues, + &callee_locals, + callee_upvalues.as_deref().map_or_else(|| &[][..], Vec::as_slice), + ); let default_chunk_ref = &context.chunks[*default_chunk as usize]; - let default_locals = context + let mut default_locals = context .limits - .take_locals(default_chunk_ref.local_count, default_chunk_ref.captures_local_slots()); + .take_locals(default_chunk_ref.local_count, default_chunk_ref.captured_local_slots()); default_locals.set(SELF_SLOT, callee_locals.get(SELF_SLOT)); - if let Err(error) = context.limits.enter_call() { - recycle_locals_if_possible( - context.limits, - default_locals, - default_chunk_ref.captures_local_slots(), - ); - return Err(error); - } - let result = { - let mut execution = ExecutionContext { - env: context.env, - limits: context.limits, - host_functions: context.host_functions, - }; - run_chunk( - *default_chunk, - context.chunks, - default_locals, - &captured, - &mut execution, - #[cfg(feature = "debugger")] - debug, - ) + let callee_locals_reusable = !context.chunks[callee_chunk_index as usize].captures_local_slots(); + let pending = PendingCall { + callee_locals, + callee_locals_reusable, + callee_upvalues, + callee_chunk_index, + callee_chunks: Shared::clone(context.chunks), + remaining_args, + target_slot: *slot, + next_index: index + 1, + arg_count, + param_count, }; - context.limits.exit_call(); - callee_locals.set(*slot, result?); + return Ok(Frame::new( + *default_chunk, + Some(Shared::clone(context.chunks)), + default_locals, + (!captured.is_empty()).then(|| Shared::new(captured)), + !default_chunk_ref.captures_local_slots(), + Continuation::ResumeBindParams(Box::new(pending)), + )); } } } } - Ok(()) + Ok(build_callee_frame( + callee_locals, + callee_upvalues, + callee_chunk_index, + context, + )) +} + +fn build_callee_frame( + callee_locals: Locals, + callee_upvalues: Option>>, + callee_chunk_index: u16, + context: &mut ParameterContext<'_, '_>, +) -> Frame { + let reusable = !context.chunks[callee_chunk_index as usize].captures_local_slots(); + Frame::new( + callee_chunk_index, + context.frame_chunks.take(), + callee_locals, + callee_upvalues, + reusable, + Continuation::Push, + ) } /// Returns a frame to the allocation pool when no closure can retain its local cells. -fn recycle_locals_if_possible(limits: &mut ExecutionLimits, locals: Locals, captures_local_slots: bool) { +fn recycle_locals_if_possible(limits: &mut super::frame::ExecutionLimits, locals: Locals, captures_local_slots: bool) { if !captures_local_slots { limits.recycle_locals(locals); } @@ -349,7 +616,7 @@ fn recycle_locals_if_possible(limits: &mut ExecutionLimits, locals: Locals, capt fn bind_fixed_required_params( arity: usize, args: &mut Vec, - callee_locals: &Locals, + callee_locals: &mut Locals, chunks: &Shared>, ) -> VmResult<()> { let arg_count = args.len(); @@ -442,45 +709,3 @@ pub(super) fn negate_ident() -> &'static crate::Ident { static NEGATE: LazyLock = LazyLock::new(|| crate::Ident::new(builtins::NEGATE)); &NEGATE } - -#[cfg(test)] -mod tests { - use super::*; - use crate::runtime::runtime_value::RuntimeValue; - - fn number(value: i64) -> StackValue { - StackValue::Value(RuntimeValue::Number(value.into())) - } - - fn value_at(locals: &Locals, slot: u16) -> RuntimeValue { - match locals.get(slot) { - StackValue::Value(value) => value, - StackValue::Closure(_) => panic!("expected a runtime value"), - } - } - - #[test] - fn fixed_required_binder_handles_explicit_and_implicit_self_arguments() { - let chunks = Shared::new(Vec::new()); - let explicit_locals = Locals::boxed(3); - bind_fixed_required_params(2, &mut vec![number(3), number(4)], &explicit_locals, &chunks).unwrap(); - assert_eq!(value_at(&explicit_locals, 1), RuntimeValue::Number(3.into())); - assert_eq!(value_at(&explicit_locals, 2), RuntimeValue::Number(4.into())); - - let implicit_locals = Locals::boxed(3); - implicit_locals.set(0, number(10)); - bind_fixed_required_params(2, &mut vec![number(4)], &implicit_locals, &chunks).unwrap(); - assert_eq!(value_at(&implicit_locals, 1), RuntimeValue::Number(10.into())); - assert_eq!(value_at(&implicit_locals, 2), RuntimeValue::Number(4.into())); - } - - #[test] - fn fixed_required_binder_rejects_invalid_arity() { - let chunks = Shared::new(Vec::new()); - let locals = Locals::boxed(1); - assert!(matches!( - bind_fixed_required_params(0, &mut vec![number(1)], &locals, &chunks), - Err(VmError::ArityMismatch { expected: 0, actual: 1 }) - )); - } -} diff --git a/crates/mq-lang/src/tarn/interpreter/errors.rs b/crates/mq-lang/src/tarn/interpreter/errors.rs index b15863e7d..74bed3ed8 100644 --- a/crates/mq-lang/src/tarn/interpreter/errors.rs +++ b/crates/mq-lang/src/tarn/interpreter/errors.rs @@ -1,12 +1,12 @@ //! `VmError`: Tarn's runtime error type, its `Display`/tree-walker-error conversions, and the //! small helpers (`locate`, `error_dict`, `error_message`, `flow_break_value`/`flow_continue`) //! built on top of it. +use crate::DictMap; use crate::ast::TokenId; use crate::runtime::builtin; use crate::runtime::runtime_value::RuntimeValue; use crate::tarn::bytecode::Chunk; use crate::{Ident, Shared}; -use std::collections::BTreeMap; use std::fmt; use std::time::Duration; @@ -134,7 +134,7 @@ impl From for VmError { pub(super) type VmResult = Result; pub(super) fn error_dict(e: &VmError) -> RuntimeValue { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert( Ident::new("message"), RuntimeValue::String(Shared::new(error_message(e))), @@ -336,7 +336,21 @@ mod tests { fn zero_division_message_matches_through_a_real_try_catch() { let token_arena = Shared::new(crate::SharedCell::new(crate::arena::Arena::new(100))); let program = crate::parse("try: 1 / 0 catch(e): get(e, \"message\");", Shared::clone(&token_arena)).unwrap(); - let result = super::super::super::compile_and_run(&program, token_arena).unwrap(); + let compiled = super::super::super::compiler::compile_program( + &program, + token_arena, + crate::ModuleLoader::new(crate::module::resolver::std_resolver::StdModuleResolver), + ) + .unwrap(); + let result = super::super::run_with_globals( + &compiled, + RuntimeValue::None, + &crate::runtime::host::HostFunctions::default(), + None, + super::super::super::Options::default().max_call_stack_depth, + &[], + ) + .unwrap(); assert_eq!( result, RuntimeValue::String(Shared::new("Division by zero".to_string())) diff --git a/crates/mq-lang/src/tarn/interpreter/frame.rs b/crates/mq-lang/src/tarn/interpreter/frame.rs index d2b99522b..740ce42ab 100644 --- a/crates/mq-lang/src/tarn/interpreter/frame.rs +++ b/crates/mq-lang/src/tarn/interpreter/frame.rs @@ -1,11 +1,107 @@ //! Per-execution frame state: deadline/call-depth tracking and the `Locals`/operand-stack //! pools that let repeated calls reuse allocations instead of hitting the allocator. use super::errors::{VmError, VmResult}; +use crate::Shared; use crate::runtime::host::HostFunctions; use crate::tarn::VmEnv; -use crate::tarn::value::{Locals, StackValue}; +use crate::tarn::bytecode::Chunk; +use crate::tarn::value::{Cell, Closure, Locals, StackValue}; +use std::collections::VecDeque; use std::time::{Duration, Instant}; +#[cfg(feature = "debugger")] +use super::DebugRuntime; +#[cfg(feature = "debugger")] +use crate::ast::node::Node; + +/// One call's state, held on the trampoline's frame stack instead of a native Rust call frame. +pub(super) struct Frame { + pub(super) chunk_index: u16, + /// The chunk pool when it differs from the program's root pool. `None` means the root + /// chunk pool, supplied once by the trampoline. Most calls stay within the program being + /// evaluated, so avoiding an `Rc`/`Arc` clone here removes reference-count traffic from the + /// fixed-call hot path. + pub(super) chunks: Option>>, + pub(super) locals: Locals, + /// `None` for the top-level frame, which has no captures and must not allocate an empty + /// vector for every evaluation. + pub(super) upvalues: Option>>, + /// First operand-stack slot owned by this frame. The operand stack itself is shared by the + /// whole execution, so entering a call only records this boundary. + pub(super) stack_base: usize, + pub(super) ip: usize, + pub(super) reusable_locals: bool, + pub(super) on_complete: Continuation, + #[cfg(feature = "debugger")] + pub(super) caller_node: Option>, + #[cfg(feature = "debugger")] + pub(super) pushed_call_stack_entry: bool, +} + +impl Frame { + /// Debug fields are filled in by `push_frame`. + pub(super) fn new( + chunk_index: u16, + chunks: Option>>, + locals: Locals, + upvalues: Option>>, + reusable_locals: bool, + on_complete: Continuation, + ) -> Self { + Self { + chunk_index, + chunks, + locals, + upvalues, + stack_base: 0, + ip: 0, + reusable_locals, + on_complete, + #[cfg(feature = "debugger")] + caller_node: None, + #[cfg(feature = "debugger")] + pushed_call_stack_entry: false, + } + } +} + +/// What to do with a frame's outcome, replacing a recursive call's implicit return. +/// +/// `TryBody`/`ResumeBindParams` are boxed so the (overwhelmingly common) `Push` case keeps +/// `Continuation` — and so `Frame`, which every call pushes/pops — pointer-sized. +pub(super) enum Continuation { + /// Normal call return / `catch` body: push the value on success; propagate on error. + Push, + /// `try` body: success behaves like `Push`. `break`/`continue` rewrites the parent's + /// accumulator/`ip` directly; any other error spawns a `catch` frame. + TryBody(Box), + /// Default-parameter value expression: store the result and resume binding on success; + /// recycle the unbound callee's locals and propagate on error. + ResumeBindParams(Box), +} + +pub(super) struct TryBody { + pub(super) catch_closure: Shared, + pub(super) has_binder: bool, + pub(super) break_acc_slot: Option, + pub(super) break_offset: Option, + pub(super) continue_offset: Option, +} + +/// A callee whose parameter binding suspended on a default-value expression. +pub(super) struct PendingCall { + pub(super) callee_locals: Locals, + pub(super) callee_locals_reusable: bool, + pub(super) callee_upvalues: Option>>, + pub(super) callee_chunk_index: u16, + pub(super) callee_chunks: Shared>, + pub(super) remaining_args: VecDeque, + pub(super) target_slot: u16, + pub(super) next_index: usize, + pub(super) arg_count: usize, + pub(super) param_count: usize, +} + /// Instructions between deadline checks. const TIMEOUT_CHECK_INTERVAL: u32 = 1024; @@ -25,11 +121,14 @@ pub(crate) struct ExecutionPools { local_pool: Vec>, pooled_local_slots: usize, stack_pool: Vec>, + frame_stack: Vec, } const MAX_POOLED_LOCAL_COUNT: usize = 256; const MAX_POOLED_LOCAL_SLOTS: usize = 4096; const MAX_POOLED_STACK_CAPACITY: usize = 4096; +const INITIAL_FRAME_STACK_CAPACITY: usize = 32; +const MAX_POOLED_FRAME_STACK_CAPACITY: usize = 4096; impl ExecutionLimits { pub(super) fn new(timeout: Option, max_call_stack_depth: u32, pools: ExecutionPools) -> Self { @@ -71,31 +170,106 @@ impl ExecutionLimits { self.deadline.is_some() } - #[inline(always)] - pub(super) fn enter_call(&mut self) -> VmResult<()> { + /// Pushes a frame, enforcing `max_call_stack_depth`. + #[cfg_attr(not(feature = "debugger"), allow(unused_mut))] + pub(super) fn push_frame( + &mut self, + frames: &mut Vec, + mut frame: Frame, + #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, + ) -> VmResult<()> { if self.call_depth >= self.max_call_stack_depth { + if frame.reusable_locals { + self.recycle_locals(frame.locals); + } return Err(VmError::RecursionError(self.max_call_stack_depth)); } self.call_depth += 1; + #[cfg(feature = "debugger")] + { + let caller_node = debug.current_node.clone(); + frame.pushed_call_stack_entry = if let Some(node) = &caller_node { + debug.call_stack.push(crate::Shared::clone(node)); + true + } else { + false + }; + frame.caller_node = caller_node; + } + frames.push(frame); Ok(()) } - #[inline(always)] - pub(super) fn exit_call(&mut self) { + /// Pops the top frame, recycling its locals, and returns its `Continuation`. + pub(super) fn pop_frame( + &mut self, + frames: &mut Vec, + #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, + ) -> Option { + let frame = frames.pop()?; self.call_depth = self.call_depth.saturating_sub(1); + if frame.reusable_locals { + self.recycle_locals(frame.locals); + } + #[cfg(feature = "debugger")] + { + if frame.pushed_call_stack_entry { + debug.call_stack.pop(); + } + debug.current_node = frame.caller_node; + } + Some(frame.on_complete) + } + + /// Replaces the active frame with its tail-call callee without consuming another call-depth + /// slot. The callee inherits the caller's continuation and operand-stack boundary. + pub(super) fn replace_top_frame( + &mut self, + frames: &mut Vec, + mut replacement: Frame, + #[cfg(feature = "debugger")] debug: &mut DebugRuntime<'_>, + ) { + let frame = frames.pop().expect("tail call requires an active frame"); + replacement.on_complete = frame.on_complete; + if frame.reusable_locals { + self.recycle_locals(frame.locals); + } + #[cfg(feature = "debugger")] + { + if frame.pushed_call_stack_entry { + debug.call_stack.pop(); + } + debug.current_node = frame.caller_node; + let caller_node = debug.current_node.clone(); + replacement.pushed_call_stack_entry = if let Some(node) = &caller_node { + debug.call_stack.push(crate::Shared::clone(node)); + true + } else { + false + }; + replacement.caller_node = caller_node; + } + frames.push(replacement); + } + + /// Releases a not-yet-running callee's locals when binding its parameters fails. + pub(super) fn recycle_pending_locals(&mut self, pending: PendingCall) { + if pending.callee_locals_reusable { + self.recycle_locals(pending.callee_locals); + } } - pub(super) fn take_locals(&mut self, count: u16, captures: bool) -> Locals { - self.take_locals_with_initialized_prefix(count, 0, captures) + pub(super) fn take_locals(&mut self, count: u16, captured_slots: &[u16]) -> Locals { + self.take_locals_with_initialized_prefix(count, 0, captured_slots) } pub(super) fn take_locals_with_initialized_prefix( &mut self, count: u16, initialized: usize, - captures: bool, + captured_slots: &[u16], ) -> Locals { - let locals = if captures { + let locals = if !captured_slots.is_empty() { None } else { self.pools @@ -106,7 +280,7 @@ impl ExecutionLimits { if locals.is_some() { self.pools.pooled_local_slots = self.pools.pooled_local_slots.saturating_sub(count as usize); } - let locals = locals.unwrap_or_else(|| fresh_locals(count as usize, captures)); + let mut locals = locals.unwrap_or_else(|| fresh_locals(count as usize, captured_slots)); locals.reset_from(initialized.min(count as usize)); locals } @@ -140,6 +314,24 @@ impl ExecutionLimits { self.pools.stack_pool.push(stack); } } + + /// Takes the reusable trampoline frame stack for an evaluation. + pub(super) fn take_frame_stack(&mut self) -> Vec { + let mut frames = std::mem::take(&mut self.pools.frame_stack); + if frames.capacity() == 0 { + frames.reserve(INITIAL_FRAME_STACK_CAPACITY); + } + frames + } + + /// Retains an empty trampoline frame stack for the next non-overlapping evaluation. + pub(super) fn recycle_frame_stack(&mut self, mut frames: Vec) { + debug_assert!(frames.is_empty(), "all VM frames must be popped before recycling"); + frames.clear(); + if frames.capacity() <= MAX_POOLED_FRAME_STACK_CAPACITY { + self.pools.frame_stack = frames; + } + } } #[cfg(test)] @@ -150,11 +342,11 @@ impl ExecutionPools { } } -fn fresh_locals(count: usize, captures: bool) -> Locals { - if captures { - Locals::boxed(count) - } else { +fn fresh_locals(count: usize, captured_slots: &[u16]) -> Locals { + if captured_slots.is_empty() { Locals::flat(count) + } else { + Locals::for_captured_slots(count, captured_slots) } } diff --git a/crates/mq-lang/src/tarn/interpreter/selectors.rs b/crates/mq-lang/src/tarn/interpreter/selectors.rs index a72632472..a6d9b9a67 100644 --- a/crates/mq-lang/src/tarn/interpreter/selectors.rs +++ b/crates/mq-lang/src/tarn/interpreter/selectors.rs @@ -1,10 +1,10 @@ //! Applies a compiled `Selector` to a runtime value: dispatches to `mq_markdown`'s selector //! evaluator for `Markdown` values and recurses through `Array`/`Dict` for the rest. +use crate::DictMap; use crate::runtime::builtin; use crate::runtime::runtime_value::RuntimeValue; use crate::selector::Selector; use crate::{Ident, Shared}; -use std::collections::BTreeMap; use std::sync::LazyLock; fn type_ident() -> &'static Ident { @@ -91,7 +91,7 @@ fn eval_selector_expr_impl(value: &RuntimeValue, selector: &Selector, args: Opti if args.is_none() && matches!(selector, Selector::Recursive) { return RuntimeValue::Array(Shared::new(collect_recursive(value))); } - let new_map: BTreeMap<_, _> = map + let new_map: DictMap = map .iter() .map(|(k, v)| { let new_v = if k == type_ident() { @@ -149,27 +149,38 @@ fn eval_property_selector_expr(value: &RuntimeValue, property_name: &Ident) -> R } fn collect_recursive(value: &RuntimeValue) -> Vec { - let mut result = vec![value.clone()]; + let mut result = Vec::new(); + collect_recursive_into(value, &mut result); + result +} + +/// Appends a pre-order recursive walk without allocating an intermediate vector per child. +/// +/// Recursive selectors are often used on nested data converted from frontmatter. Building a +/// separate `Vec` for every child made the traversal allocation-heavy and repeatedly copied +/// partial results into its parent. A single output buffer preserves the selector's order while +/// making allocation scale with the complete result instead. +fn collect_recursive_into(value: &RuntimeValue, result: &mut Vec) { + result.push(value.clone()); match value { RuntimeValue::Array(items) => { for item in items.iter() { - result.extend(collect_recursive(item)); + collect_recursive_into(item, result); } } RuntimeValue::Dict(map) => { for v in map.values() { - result.extend(collect_recursive(v)); + collect_recursive_into(v, result); } } _ => {} } - result } #[cfg(test)] mod tests { use super::*; - use std::collections::BTreeMap; + use crate::DictMap; #[test] fn list_selector_maps_an_array_without_changing_element_order() { @@ -188,7 +199,7 @@ mod tests { #[test] fn recursive_selector_flattens_dict_results_inside_an_array() { - let dict = RuntimeValue::Dict(Shared::new(BTreeMap::from([( + let dict = RuntimeValue::Dict(Shared::new(DictMap::from_iter([( Ident::new("key"), RuntimeValue::Number(1.into()), )]))); @@ -204,4 +215,29 @@ mod tests { "recursive dict results must remain flattened when selected through an array" ); } + + #[test] + fn collect_recursive_preserves_preorder_for_nested_values() { + let input = RuntimeValue::Array(Shared::new(vec![ + RuntimeValue::Number(1.into()), + RuntimeValue::Dict(Shared::new(DictMap::from_iter([( + Ident::new("nested"), + RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into())])), + )]))), + ])); + + assert_eq!( + collect_recursive(&input), + vec![ + input.clone(), + RuntimeValue::Number(1.into()), + RuntimeValue::Dict(Shared::new(DictMap::from_iter([( + Ident::new("nested"), + RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into())])), + )]))), + RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into())])), + RuntimeValue::Number(2.into()), + ] + ); + } } diff --git a/crates/mq-lang/src/tarn/nodes_split.rs b/crates/mq-lang/src/tarn/nodes_split.rs index 5883131c9..69e4dfc26 100644 --- a/crates/mq-lang/src/tarn/nodes_split.rs +++ b/crates/mq-lang/src/tarn/nodes_split.rs @@ -16,7 +16,7 @@ pub(super) fn program_after_nodes(before: ProgramSlice<'_>, after: ProgramSlice< .iter() .filter(|node| { matches!( - *node.expr, + &node.expr, Expr::Def(..) | Expr::Include(..) | Expr::Import(..) | Expr::Module(..) ) }) @@ -30,7 +30,7 @@ pub(super) fn program_after_nodes(before: ProgramSlice<'_>, after: ProgramSlice< pub(super) fn let_names_before_nodes(before: ProgramSlice<'_>) -> Vec { let mut names = Vec::new(); for node in before { - match &*node.expr { + match &node.expr { Expr::Let(pattern, _) | Expr::Var(pattern, _) => compiler::collect_pattern_idents(pattern, &mut names), Expr::As(ident, _) => names.push(ident.name), _ => {} @@ -44,7 +44,7 @@ pub(super) fn immutable_let_names_before_nodes(before: ProgramSlice<'_>) -> Vec< let mut names = Vec::new(); let mut shadowed = std::collections::HashSet::new(); for node in before.iter().rev() { - let (mut declared, immutable) = match &*node.expr { + let (mut declared, immutable) = match &node.expr { Expr::Let(pattern, _) => { let mut declared = Vec::new(); compiler::collect_pattern_idents(pattern, &mut declared); @@ -71,7 +71,7 @@ pub(super) fn immutable_let_names_before_nodes(before: ProgramSlice<'_>) -> Vec< pub(super) fn top_level_binding_names(program: &Program) -> Vec { let mut names = Vec::new(); for node in program { - match &*node.expr { + match &node.expr { Expr::Let(pattern, _) | Expr::Var(pattern, _) => compiler::collect_pattern_idents(pattern, &mut names), Expr::Def(ident, ..) => names.push(ident.name), Expr::As(ident, _) => names.push(ident.name), diff --git a/crates/mq-lang/src/tarn/resolver.rs b/crates/mq-lang/src/tarn/resolver.rs index 661489a01..d13f90db0 100644 --- a/crates/mq-lang/src/tarn/resolver.rs +++ b/crates/mq-lang/src/tarn/resolver.rs @@ -13,6 +13,10 @@ pub(crate) struct FunctionScope { next_block: u32, upvalues: Vec<(Ident, UpvalueSource)>, immutable: std::collections::HashSet, + /// Capture-free, fixed-arity function chunks bound to immutable local slots. + /// + /// Calls through these slots can bypass materializing and loading a closure. + static_functions: std::collections::HashMap, pub(crate) shadowed_builtin: Option, } @@ -25,6 +29,7 @@ impl Default for FunctionScope { next_block: ROOT_BLOCK + 1, upvalues: Vec::new(), immutable: std::collections::HashSet::new(), + static_functions: std::collections::HashMap::new(), shadowed_builtin: None, } } @@ -64,6 +69,19 @@ impl FunctionScope { pub(crate) fn unmark_immutable(&mut self, slot: u16) { self.immutable.remove(&slot); + self.static_functions.remove(&slot); + } + + pub(crate) fn set_static_function(&mut self, slot: u16, chunk: u16) { + self.static_functions.insert(slot, chunk); + } + + pub(crate) fn clear_static_function(&mut self, slot: u16) { + self.static_functions.remove(&slot); + } + + pub(crate) fn static_function(&self, slot: u16) -> Option { + self.static_functions.get(&slot).copied() } /// Reuses `name`'s slot if one is currently visible (an existing loop counter, e.g. diff --git a/crates/mq-lang/src/tarn/tests.rs b/crates/mq-lang/src/tarn/tests.rs index 36efd941b..4f5cd71de 100644 --- a/crates/mq-lang/src/tarn/tests.rs +++ b/crates/mq-lang/src/tarn/tests.rs @@ -1,26 +1,55 @@ use super::interpreter::ExecutionPools; use super::*; -#[cfg(not(feature = "tarn"))] -use crate::Selector; -#[cfg(not(feature = "tarn"))] -use crate::ast::node::{self as ast, Args}; -#[cfg(not(feature = "tarn"))] -use crate::ast::node::{MatchArm, Param, Pattern}; -#[cfg(not(feature = "tarn"))] -use crate::error::runtime::RuntimeError; -#[cfg(not(feature = "tarn"))] -use crate::number::{INFINITE, NAN, Number}; +use crate::module::resolver::std_resolver::StdModuleResolver; use crate::range::Range; -#[cfg(not(feature = "tarn"))] -use crate::{AstExpr, AstNode, DefaultModuleLoader, IdentWithToken, Program, error::InnerError}; use crate::{Shared, SharedCell}; use crate::{Token, TokenKind, arena::Arena, token_alloc}; use proptest::prelude::*; use rstest::rstest; -#[cfg(not(feature = "tarn"))] -use smallvec::{SmallVec, smallvec}; -#[cfg(not(feature = "tarn"))] -use std::f64::consts::PI; + +fn compile_and_run(program: &Program, token_arena: TokenArena) -> Result { + compile_and_run_full( + program, + RuntimeValue::None, + &HostFunctions::default(), + None, + token_arena, + ) +} + +fn compile_and_run_with_input( + program: &Program, + input: RuntimeValue, + token_arena: TokenArena, +) -> Result { + compile_and_run_full(program, input, &HostFunctions::default(), None, token_arena) +} + +fn compile_and_run_full( + program: &Program, + input: RuntimeValue, + host_functions: &HostFunctions, + timeout: Option, + token_arena: TokenArena, +) -> Result { + let compiled = compiler::compile_program(program, token_arena, ModuleLoader::new(StdModuleResolver))?; + Ok(interpreter::run_with_globals( + &compiled, + input, + host_functions, + timeout, + Options::default().max_call_stack_depth, + &[], + )?) +} + +#[test] +fn default_call_stack_depth_matches_the_build_profile() { + assert_eq!( + Options::default().max_call_stack_depth, + if cfg!(debug_assertions) { 256 } else { 10_000 } + ); +} #[rstest] #[case::selector_chain(".h1 | .text")] @@ -99,58 +128,6 @@ fn token_arena() -> Shared>>> { token_arena } -#[cfg(not(feature = "tarn"))] -fn ast_node(expr: AstExpr) -> Shared { - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(expr), - }) -} - -#[cfg(not(feature = "tarn"))] -fn ast_call(name: &str, args: Args) -> Shared { - Shared::new(AstNode { - token_id: 0.into(), - expr: Shared::new(ast::Expr::Call(IdentWithToken::new(name), args)), - }) -} - -// The shared table keeps VM and evaluator cases aligned. -#[cfg(not(feature = "tarn"))] -crate::eval_table_cases!( - evaluator_table_cases_run_on_vm, - token_arena, - runtime_values, - program, - expected, - { - let host_functions = HostFunctions::default(); - let vm_result = compile_and_run_many( - &program, - runtime_values.into_iter(), - EngineRunContext { - host_functions: &host_functions, - // Hand-built AST cases must never leave the VM test worker running forever. - timeout: Some(std::time::Duration::from_secs(1)), - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, - token_arena, - module_loader: DefaultModuleLoader::default(), - global_bindings: &[], - session: None, - preresolved_module_vars: compiler::ResolvedModuleVars::default(), - }, - ); - - match expected { - Ok(expected_values) => assert_eq!( - vm_result.expect("VM should accept a successful evaluator table case"), - expected_values, - ), - Err(_) => assert!(vm_result.is_err(), "VM should reject an evaluator error case"), - } - } -); - fn run(code: &str) -> RuntimeValue { let token_arena = Shared::new(SharedCell::new(Arena::new(100))); let program = crate::parse(code, Shared::clone(&token_arena)).unwrap(); @@ -167,12 +144,13 @@ fn run_with_max_depth(code: &str, max_call_stack_depth: u32) -> Result RuntimeValue { let token_arena = Shared::new(SharedCell::new(Arena::new(100))); let program = crate::parse(code, Shared::clone(&token_arena)).unwrap(); let compiled = compiler::compile_program_with_builtin_prelude(&program, token_arena, ModuleLoader::new(StdModuleResolver)) .unwrap(); - match interpreter::run( + match interpreter::run_with_globals( &compiled, RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, + &[], ) { Ok(v) => v, Err(e) => panic!("{e}"), @@ -248,6 +253,7 @@ fn non_capturing_closures_use_chunk_static_storage() { let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); assert_eq!(compiled.chunks[0].static_closures.len(), 1); + assert!(compiled.chunks[0].static_closures[0].upvalues.is_none()); assert!( compiled.chunks[0] .code @@ -258,7 +264,7 @@ fn non_capturing_closures_use_chunk_static_storage() { compiled.chunks[0] .code .iter() - .any(|op| matches!(op, OpCode::CallLocal(_, 1))) + .any(|op| matches!(op, OpCode::CallStaticExact1(_))) ); assert_eq!(compiled.chunks[1].param_shape.fixed_required_arity(), Some(1)); } @@ -279,6 +285,43 @@ fn local_binary_expressions_use_compact_bytecode() { ); } +#[test] +fn local_constant_assignment_uses_update_opcode() { + use super::bytecode::{BinaryOp, OpCode}; + + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse("var x = 1 | x += 2 | x", Shared::clone(&token_arena)).unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::UpdateLocalConst { op: BinaryOp::Add, .. })) + ); + assert_eq!(run("var x = 1 | x += 2 | x"), RuntimeValue::Number(3.into())); +} + +#[test] +fn local_assignment_uses_local_update_opcode() { + use super::bytecode::{BinaryOp, OpCode}; + + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse("var x = 1 | var y = 2 | x += y | x", Shared::clone(&token_arena)).unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::UpdateLocalLocal { op: BinaryOp::Add, .. })) + ); + assert_eq!( + run("var x = 1 | var y = 2 | x += y | x"), + RuntimeValue::Number(3.into()) + ); +} + #[test] fn top_level_function_literal_produces_a_callable_value() { assert!(matches!(run("fn(x): x;"), RuntimeValue::VmClosure(_))); @@ -348,7 +391,7 @@ fn breakpoint_is_a_no_op_when_the_debugger_feature_is_disabled() { } #[test] -fn top_level_def_calls_use_call_local() { +fn top_level_def_calls_use_call_static() { use super::bytecode::OpCode; let token_arena = Shared::new(SharedCell::new(Arena::new(100))); let program = crate::parse( @@ -357,13 +400,181 @@ fn top_level_def_calls_use_call_local() { ) .unwrap(); let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + assert!( + compiled.chunks.iter().any(|c| c.code.iter().any(|op| matches!( + op, + OpCode::CallStaticExact0(_) | OpCode::CallStaticExact1(_) | OpCode::CallStaticExact2(_) + ))), + "capture-free common-arity top-level def call should compile to a specialized CallStaticExact opcode" + ); +} + +#[test] +fn fixed_static_calls_specialize_the_exact_and_implicit_self_forms() { + use super::bytecode::OpCode; + + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse( + "def identity(x): x; | identity(1) | identity()", + Shared::clone(&token_arena), + ) + .unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::CallStaticExact1(_))) + ); + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::CallStaticImplicitSelf(_, 0))) + ); + assert_eq!( + run("def identity(x): x; | identity(1) | identity()"), + RuntimeValue::Number(1.into()) + ); +} + +#[test] +fn fixed_static_calls_bind_zero_and_two_arguments_without_generic_binding() { + use super::bytecode::OpCode; + + let source = "def constant(): 7; | def add(left, right): left + right; | constant() + add(20, 22)"; + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse(source, Shared::clone(&token_arena)).unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::CallStaticExact0(_))) + ); + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::CallStaticExact2(_))) + ); + assert_eq!(run(source), RuntimeValue::Number(49.into())); +} + +#[test] +fn static_calls_with_captured_locals_keep_the_generic_exact_opcode() { + use super::bytecode::OpCode; + + let source = "let f = fn(value): fn(): value;; | let read = f(42) | read()"; + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse(source, Shared::clone(&token_arena)).unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::CallStaticExact(_, 1))) + ); + assert_eq!(run(source), RuntimeValue::Number(42.into())); +} + +#[test] +fn fixed_static_arity_mismatches_keep_the_checked_call_form() { + use super::bytecode::OpCode; + + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse("def identity(x): x; | identity(1, 2)", Shared::clone(&token_arena)).unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!( + compiled.chunks[0] + .code + .iter() + .any(|op| matches!(op, OpCode::CallStatic(_, 2))) + ); +} + +#[test] +fn fixed_arity_recursive_def_uses_call_self_without_capturing_itself() { + use super::bytecode::OpCode; + + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse( + "def count(n): if (n == 0): 0 else: count(n - 1); | count(10)", + Shared::clone(&token_arena), + ) + .unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + let recursive_chunk = compiled + .chunks + .iter() + .find(|chunk| chunk.code.iter().any(|op| matches!(op, OpCode::CallSelfExact1))) + .expect("recursive body should use CallSelfExact1"); + + assert!(recursive_chunk.upvalue_names.is_empty()); + assert_eq!( + run("def count(n): if (n == 0): 0 else: count(n - 1); | count(10)"), + RuntimeValue::Number(0.0.into()) + ); +} + +#[test] +fn fixed_arity_recursive_def_specializes_implicit_self_calls() { + use super::bytecode::OpCode; + + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse( + "def identity(x): if (true): x else: identity(); | 42 | identity()", + Shared::clone(&token_arena), + ) + .unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + + assert!(compiled.chunks.iter().any(|chunk| { + chunk + .code + .iter() + .any(|op| matches!(op, OpCode::CallSelfImplicitSelf(0))) + })); + assert_eq!( + run("def identity(x): if (true): x else: identity(); | 42 | identity()"), + RuntimeValue::Number(42.into()) + ); +} + +#[test] +fn tail_recursive_call_reuses_its_frame() { + let code = "def count(n): if (n <= 0): 0 else: count(n - 1); | count(100)"; + assert_eq!(run_with_max_depth(code, 1).unwrap(), RuntimeValue::Number(0.0.into())); +} + +/// Direct builtin calls preserve argument order through the specialized common-arity paths. +#[rstest] +#[case("type(42)", RuntimeValue::String(Shared::new("number".to_string())))] +#[case("sub(5, 3)", RuntimeValue::Number(2.0.into()))] +fn direct_builtin_calls_with_common_arities_preserve_results(#[case] code: &str, #[case] expected: RuntimeValue) { + assert_eq!(run(code), expected); +} + +#[test] +fn immutable_function_upvalue_calls_use_call_upvalue() { + use super::bytecode::OpCode; + + let source = "let increment = fn(x): x + 1; | let apply = fn(x): increment(x); | apply(41)"; + let token_arena = Shared::new(SharedCell::new(Arena::new(100))); + let program = crate::parse(source, Shared::clone(&token_arena)).unwrap(); + let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(StdModuleResolver)).unwrap(); + assert!( compiled .chunks .iter() - .any(|c| c.code.iter().any(|op| matches!(op, OpCode::CallLocal(_, _)))), - "top-level def call should compile to CallLocal, not the slower CallValue path" + .any(|chunk| chunk.code.iter().any(|op| matches!(op, OpCode::CallUpvalue(_, 1)))) ); + assert_eq!(run(source), RuntimeValue::Number(42.0.into())); } #[test] @@ -559,12 +770,13 @@ fn engine_compiler_reachable_prelude_cache_is_correct_across_different_queries() &compiler::ResolvedModuleVars::default(), ) .unwrap(); - interpreter::run( + interpreter::run_with_globals( &compiled, RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, + &[], ) .unwrap() } @@ -991,28 +1203,10 @@ fn text_node(value: &str) -> mq_markdown::Node { }) } -/// Reference output from the tree-walking evaluator. -#[cfg(not(feature = "tarn"))] -fn tree_walk_eval(code: &str, input: RuntimeValue) -> RuntimeValue { - tree_walk_eval_many(code, vec![input]).remove(0) -} - -#[cfg(not(feature = "tarn"))] -fn tree_walk_eval_many(code: &str, inputs: Vec) -> Vec { - let mut engine = crate::DefaultEngine::default(); - engine.evaluator.load_builtin_module_full().unwrap(); - let compiled = engine.compile(code).unwrap(); - engine.evaluator.eval(compiled.program(), inputs.into_iter()).unwrap() -} - -// In a Tarn-only build the tree walker is intentionally absent. Keep the test helpers usable -// for VM-only behavioural assertions without pulling the legacy evaluator into the binary. -#[cfg(feature = "tarn")] fn tree_walk_eval(code: &str, input: RuntimeValue) -> RuntimeValue { vm_engine_eval_many(code, vec![input]).remove(0) } -#[cfg(feature = "tarn")] fn tree_walk_eval_many(code: &str, inputs: Vec) -> Vec { vm_engine_eval_many(code, inputs) } @@ -1109,7 +1303,7 @@ fn nodes_capture_uses_the_latest_slot_for_a_name_rebound_by_repeated_destructuri EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1122,7 +1316,7 @@ fn nodes_capture_uses_the_latest_slot_for_a_name_rebound_by_repeated_destructuri assert_eq!(results, vec![RuntimeValue::Number(2.0.into())]); } -#[cfg(all(feature = "tarn", not(feature = "debugger")))] +#[cfg(not(feature = "debugger"))] #[test] fn cached_nodes_capture_reuses_precomputed_slots() { let mut engine = crate::DefaultEngine::default(); @@ -1171,7 +1365,7 @@ fn nodes_aggregates_per_input_results_into_one_run() { EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1206,7 +1400,7 @@ fn nodes_split_also_works_through_the_debugger_hooked_entry_point() { engine: EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1237,7 +1431,7 @@ fn nodes_runs_the_pre_nodes_portion_once_per_input_first() { EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1264,7 +1458,7 @@ fn markdown_fragment_input_that_matches_at_the_top_runs_only_once() { EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1299,7 +1493,7 @@ fn markdown_selector_recurses_into_a_non_matching_container_to_find_matches_belo EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1321,7 +1515,7 @@ fn non_fragment_markdown_input_still_runs_the_query_once() { EngineRunContext { host_functions: &HostFunctions::default(), timeout: None, - max_call_stack_depth: crate::eval::Options::default().max_call_stack_depth, + max_call_stack_depth: crate::tarn::Options::default().max_call_stack_depth, token_arena, module_loader: ModuleLoader::new(StdModuleResolver), global_bindings: &[], @@ -1410,6 +1604,19 @@ fn try_depth_limit_returns_its_unstarted_frame_to_the_pool() { assert_eq!(pools.pooled_local_frame_count(), 2); } +/// Regression: mq call depth used to equal native Rust stack depth, so a high +/// `max_call_stack_depth` could overflow the OS thread stack instead of hitting +/// `RecursionError`. The trampoline's `Vec` is heap-bound, so this must just complete. +#[rstest] +#[case::plain_recursion("def f(n): if (n <= 0): 0 else: 1 + f(n - 1); | f(100000)")] +#[case::recursion_through_try_catch("def f(n): if (n <= 0): 0 else: try: 1 + f(n - 1) catch(e): -1; | f(100000)")] +fn deep_non_tail_recursion_does_not_overflow_the_native_stack(#[case] code: &str) { + assert_eq!( + run_with_max_depth(code, 1_000_000).unwrap(), + RuntimeValue::Number(100000.into()) + ); +} + #[cfg(feature = "debugger")] #[test] fn debugger_metadata_tracks_boundaries_and_static_slots() { @@ -1487,7 +1694,7 @@ fn debugger_hook_receives_live_bindings_and_call_stack() { RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, &[], &mut recorder, ) @@ -1542,7 +1749,7 @@ fn debugger_hook_exposes_closure_bindings() { RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, &[], &mut recorder, ) @@ -1646,7 +1853,7 @@ fn vm_debugger_hook_adapts_breakpoints_to_existing_handler() { RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, &[], &mut hook, ) @@ -1735,7 +1942,7 @@ fn vm_debugger_hook_applies_live_frame_writes( RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, &[], &mut hook, ) @@ -1794,7 +2001,7 @@ fn breakpoint_builtin_pauses_unconditionally_with_no_registered_breakpoints() { RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, &[], &mut hook, ) @@ -1871,7 +2078,7 @@ fn vm_debugger_hook_evaluates_hit_conditions_and_logpoints() { RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, &[], &mut hook, ) @@ -2254,12 +2461,13 @@ fn run_with_local_module(dir: &tempfile::TempDir, code: &str) -> RuntimeValue { let resolver = crate::module::resolver::local_fs_resolver::LocalFsModuleResolver::new(Some(vec![dir.path().to_path_buf()])); let compiled = compiler::compile_program(&program, token_arena, ModuleLoader::new(resolver)).unwrap(); - interpreter::run( + interpreter::run_with_globals( &compiled, RuntimeValue::None, &HostFunctions::default(), None, - crate::eval::Options::default().max_call_stack_depth, + crate::tarn::Options::default().max_call_stack_depth, + &[], ) .unwrap() } diff --git a/crates/mq-lang/src/tarn/value.rs b/crates/mq-lang/src/tarn/value.rs index 69874fccf..aea430dce 100644 --- a/crates/mq-lang/src/tarn/value.rs +++ b/crates/mq-lang/src/tarn/value.rs @@ -1,4 +1,5 @@ use super::bytecode::Chunk; +use crate::number::Number; use crate::runtime::runtime_value::RuntimeValue; use crate::{Shared, SharedCell}; @@ -15,14 +16,19 @@ pub(crate) enum StackValue { /// A closure on the VM operand stack. pub(crate) struct Closure { pub(crate) chunk_index: u16, - pub(crate) upvalues: Vec, + /// Absent for the common capture-free closure. Capturing closures share their cells with + /// call frames, avoiding a deep copy on every call. + pub(crate) upvalues: Option>>, } impl std::fmt::Debug for Closure { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Closure") .field("chunk_index", &self.chunk_index) - .field("upvalue_count", &self.upvalues.len()) + .field( + "upvalue_count", + &self.upvalues.as_ref().map_or(0, |upvalues| upvalues.len()), + ) .finish() } } @@ -32,7 +38,7 @@ impl std::fmt::Debug for Closure { pub(crate) struct VmClosureValue { pub(crate) chunks: Shared>, pub(crate) chunk_index: u16, - pub(crate) upvalues: Vec, + pub(crate) upvalues: Option>>, pub(crate) bound_args: Vec, } @@ -53,10 +59,20 @@ pub(crate) fn new_cell(value: StackValue) -> Cell { Shared::new(SharedCell::new(value)) } -/// One frame's local slots. `Boxed` slots support captures. +/// One frame's local slots. +/// +/// A capturing frame only allocates shared cells for slots a nested closure actually captures; +/// its other slots retain the direct-value representation used by non-capturing frames. pub(crate) enum Locals { #[cfg(not(feature = "sync"))] - Flat(Vec>), + /// A contiguous, exclusively owned local region for a non-capturing frame. + Flat(Vec), + #[cfg(not(feature = "sync"))] + /// Direct slots plus cells at the sparse set of captured slot positions. + Hybrid { + slots: Vec, + captured: Vec>, + }, Boxed(Vec), } @@ -65,11 +81,7 @@ impl Locals { pub(crate) fn flat(count: usize) -> Self { #[cfg(not(feature = "sync"))] { - Locals::Flat( - (0..count) - .map(|_| std::cell::RefCell::new(StackValue::Value(RuntimeValue::None))) - .collect(), - ) + Locals::Flat((0..count).map(|_| StackValue::Value(RuntimeValue::None)).collect()) } #[cfg(feature = "sync")] { @@ -78,7 +90,32 @@ impl Locals { } /// Creates a capture-capable frame. - pub(crate) fn boxed(count: usize) -> Self { + pub(crate) fn for_captured_slots(count: usize, captured_slots: &[u16]) -> Self { + if captured_slots.len() == count { + return Locals::boxed(count); + } + #[cfg(not(feature = "sync"))] + { + let mut captured = vec![None; count]; + for &slot in captured_slots { + if let Some(cell) = captured.get_mut(slot as usize) { + *cell = Some(new_cell(StackValue::Value(RuntimeValue::None))); + } + } + Locals::Hybrid { + slots: (0..count).map(|_| StackValue::Value(RuntimeValue::None)).collect(), + captured, + } + } + #[cfg(feature = "sync")] + { + let _ = captured_slots; + Locals::boxed(count) + } + } + + /// Creates a frame whose every slot is a shared cell. + fn boxed(count: usize) -> Self { Locals::Boxed( (0..count) .map(|_| new_cell(StackValue::Value(RuntimeValue::None))) @@ -91,17 +128,31 @@ impl Locals { match self { #[cfg(not(feature = "sync"))] Locals::Flat(slots) => slots.len(), + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, .. } => slots.len(), Locals::Boxed(slots) => slots.len(), } } /// Clears slots from `from` onward. - pub(crate) fn reset_from(&self, from: usize) { + pub(crate) fn reset_from(&mut self, from: usize) { match self { #[cfg(not(feature = "sync"))] Locals::Flat(slots) => { - for slot in &slots[from.min(slots.len())..] { - *slot.borrow_mut() = StackValue::Value(RuntimeValue::None); + let from = from.min(slots.len()); + for slot in &mut slots[from..] { + *slot = StackValue::Value(RuntimeValue::None); + } + } + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, captured } => { + let from = from.min(slots.len()); + for index in from..slots.len() { + if let Some(cell) = &captured[index] { + write_cell(cell, StackValue::Value(RuntimeValue::None)); + } else { + slots[index] = StackValue::Value(RuntimeValue::None); + } } } Locals::Boxed(slots) => { @@ -116,7 +167,11 @@ impl Locals { pub(crate) fn get(&self, slot: u16) -> StackValue { match self { #[cfg(not(feature = "sync"))] - Locals::Flat(slots) => slots[slot as usize].borrow().clone(), + Locals::Flat(slots) => slots[slot as usize].clone(), + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, captured } => captured[slot as usize] + .as_ref() + .map_or_else(|| slots[slot as usize].clone(), read_cell), Locals::Boxed(slots) => read_cell(&slots[slot as usize]), } } @@ -127,10 +182,18 @@ impl Locals { } /// Writes a local slot. - pub(crate) fn set(&self, slot: u16, value: StackValue) { + pub(crate) fn set(&mut self, slot: u16, value: StackValue) { match self { #[cfg(not(feature = "sync"))] - Locals::Flat(slots) => *slots[slot as usize].borrow_mut() = value, + Locals::Flat(slots) => slots[slot as usize] = value, + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, captured } => { + if let Some(cell) = &captured[slot as usize] { + write_cell(cell, value); + } else { + slots[slot as usize] = value; + } + } Locals::Boxed(slots) => write_cell(&slots[slot as usize], value), } } @@ -139,7 +202,7 @@ impl Locals { /// /// # Safety /// `slot` must be `< self.len()` (guaranteed by `bytecode::verify_chunks` for any - /// GetLocal/SetLocal/TeeLocal/BinaryLocalLocal/BinaryLocalConst/ArrayLenLocal/ + /// GetLocal/SetLocal/TeeLocal/BinaryLocalLocal/BinaryLocalConst/UpdateLocalConst/UpdateLocalLocal/ArrayLenLocal/ /// ArrayGetLocalAt opcode slot). #[inline(always)] pub(crate) unsafe fn get_unchecked(&self, slot: u16) -> StackValue { @@ -147,7 +210,16 @@ impl Locals { #[cfg(not(feature = "sync"))] Locals::Flat(slots) => { // SAFETY: inherited from `Locals::get_unchecked`'s caller contract. - unsafe { slots.get_unchecked(slot as usize) }.borrow().clone() + unsafe { slots.get_unchecked(slot as usize) }.clone() + } + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, captured } => { + // SAFETY: inherited from `Locals::get_unchecked`'s caller contract. + match unsafe { captured.get_unchecked(slot as usize) } { + Some(cell) => read_cell(cell), + // SAFETY: inherited from `Locals::get_unchecked`'s caller contract. + None => unsafe { slots.get_unchecked(slot as usize) }.clone(), + } } Locals::Boxed(slots) => { // SAFETY: inherited from `Locals::get_unchecked`'s caller contract. @@ -158,12 +230,22 @@ impl Locals { /// Like [`Locals::set`], without the bounds check. See [`Locals::get_unchecked`]. #[inline(always)] - pub(crate) unsafe fn set_unchecked(&self, slot: u16, value: StackValue) { + pub(crate) unsafe fn set_unchecked(&mut self, slot: u16, value: StackValue) { match self { #[cfg(not(feature = "sync"))] Locals::Flat(slots) => { // SAFETY: inherited from `Locals::set_unchecked`'s caller contract. - *unsafe { slots.get_unchecked(slot as usize) }.borrow_mut() = value; + *unsafe { slots.get_unchecked_mut(slot as usize) } = value; + } + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, captured } => { + // SAFETY: inherited from `Locals::set_unchecked`'s caller contract. + if let Some(cell) = unsafe { captured.get_unchecked(slot as usize) } { + write_cell(cell, value); + } else { + // SAFETY: inherited from `Locals::set_unchecked`'s caller contract. + *unsafe { slots.get_unchecked_mut(slot as usize) } = value; + } } Locals::Boxed(slots) => { // SAFETY: inherited from `Locals::set_unchecked`'s caller contract. @@ -177,42 +259,148 @@ impl Locals { match self { #[cfg(not(feature = "sync"))] Locals::Flat(_) => unreachable!("a non-capturing chunk's locals can't be captured"), + #[cfg(not(feature = "sync"))] + Locals::Hybrid { captured, .. } => captured[slot as usize] + .as_ref() + .expect("bytecode attempted to capture a local slot without a cell"), Locals::Boxed(slots) => &slots[slot as usize], } } - /// Reads an array slot's length and element. - pub(crate) fn array_len_and_element_at( - &self, - slot: u16, - index: usize, - ) -> Result<(usize, Option), &'static str> { + /// Appends to an array stored in a local slot. + pub(crate) fn append_to_array_at(&mut self, slot: u16, value: RuntimeValue) -> Result<(), &'static str> { match self { #[cfg(not(feature = "sync"))] Locals::Flat(slots) => { - let borrowed = slots[slot as usize].borrow(); - let StackValue::Value(RuntimeValue::Array(array)) = &*borrowed else { - return Err("ForeachNext array slot is not an array"); + let StackValue::Value(RuntimeValue::Array(array)) = &mut slots[slot as usize] else { + return Err("ForeachCollect accumulator is not an array"); }; - Ok((array.len(), array.get(index).cloned())) + crate::runtime::runtime_value::array_mut(array).push(value); + Ok(()) } - Locals::Boxed(slots) => array_len_and_element_at_cell(&slots[slot as usize], index), + #[cfg(not(feature = "sync"))] + Locals::Hybrid { slots, captured } => match &captured[slot as usize] { + Some(cell) => append_to_array_cell(cell, value), + None => { + let StackValue::Value(RuntimeValue::Array(array)) = &mut slots[slot as usize] else { + return Err("ForeachCollect accumulator is not an array"); + }; + crate::runtime::runtime_value::array_mut(array).push(value); + Ok(()) + } + }, + Locals::Boxed(slots) => append_to_array_cell(&slots[slot as usize], value), } } - /// Appends to an array stored in a local slot. - pub(crate) fn append_to_array_at(&self, slot: u16, value: RuntimeValue) -> Result<(), &'static str> { + /// Advances a `foreach` loop and updates its index, loop value, and implicit-self slots. + /// + /// # Safety + /// `array_slot`, `index_slot`, and `value_slot` must be valid local slots. The bytecode + /// verifier establishes this for every `ForeachNext` instruction before execution. + #[inline(always)] + pub(crate) unsafe fn foreach_next( + &mut self, + array_slot: u16, + index_slot: u16, + value_slot: u16, + self_slot: u16, + ) -> Result, &'static str> { match self { #[cfg(not(feature = "sync"))] Locals::Flat(slots) => { - let mut borrowed = slots[slot as usize].borrow_mut(); - let StackValue::Value(RuntimeValue::Array(array)) = &mut *borrowed else { - return Err("ForeachCollect accumulator is not an array"); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + let index_value = { + let index = unsafe { slots.get_unchecked(index_slot as usize) }; + let StackValue::Value(RuntimeValue::Number(index)) = index else { + return Err("ForeachNext has invalid loop state"); + }; + index.value() }; - crate::runtime::runtime_value::array_mut(array).push(value); - Ok(()) + + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + let value = { + let array = unsafe { slots.get_unchecked(array_slot as usize) }; + let StackValue::Value(RuntimeValue::Array(array)) = array else { + return Err("ForeachNext array slot is not an array"); + }; + if index_value >= array.len() as f64 { + return Ok(None); + } + array.get(index_value as usize).cloned().unwrap_or(RuntimeValue::None) + }; + + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + *unsafe { slots.get_unchecked_mut(index_slot as usize) } = + StackValue::Value(RuntimeValue::Number(Number::new(index_value + 1.0))); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + *unsafe { slots.get_unchecked_mut(value_slot as usize) } = StackValue::Value(value.clone()); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + *unsafe { slots.get_unchecked_mut(self_slot as usize) } = StackValue::Value(value.clone()); + Ok(Some(value)) + } + #[cfg(not(feature = "sync"))] + Locals::Hybrid { .. } => { + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + let index = unsafe { self.get_unchecked(index_slot) }; + let StackValue::Value(RuntimeValue::Number(index)) = index else { + return Err("ForeachNext has invalid loop state"); + }; + let index_value = index.value(); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + let array = unsafe { self.get_unchecked(array_slot) }; + let StackValue::Value(RuntimeValue::Array(array)) = array else { + return Err("ForeachNext array slot is not an array"); + }; + if index_value >= array.len() as f64 { + return Ok(None); + } + let value = array.get(index_value as usize).cloned().unwrap_or(RuntimeValue::None); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + unsafe { + self.set_unchecked( + index_slot, + StackValue::Value(RuntimeValue::Number(Number::new(index_value + 1.0))), + ); + self.set_unchecked(value_slot, StackValue::Value(value.clone())); + self.set_unchecked(self_slot, StackValue::Value(value.clone())); + } + Ok(Some(value)) + } + Locals::Boxed(slots) => { + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + let index = read_cell(unsafe { slots.get_unchecked(index_slot as usize) }); + let StackValue::Value(RuntimeValue::Number(index)) = index else { + return Err("ForeachNext has invalid loop state"); + }; + let index_value = index.value(); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + let array = read_cell(unsafe { slots.get_unchecked(array_slot as usize) }); + let StackValue::Value(RuntimeValue::Array(array)) = array else { + return Err("ForeachNext array slot is not an array"); + }; + if index_value >= array.len() as f64 { + return Ok(None); + } + let value = array.get(index_value as usize).cloned().unwrap_or(RuntimeValue::None); + + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + write_cell( + unsafe { slots.get_unchecked(index_slot as usize) }, + StackValue::Value(RuntimeValue::Number(Number::new(index_value + 1.0))), + ); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + write_cell( + unsafe { slots.get_unchecked(value_slot as usize) }, + StackValue::Value(value.clone()), + ); + // SAFETY: inherited from `Locals::foreach_next`'s caller contract. + write_cell( + unsafe { slots.get_unchecked(self_slot as usize) }, + StackValue::Value(value.clone()), + ); + Ok(Some(value)) } - Locals::Boxed(slots) => append_to_array_cell(&slots[slot as usize], value), } } } @@ -241,29 +429,6 @@ pub(crate) fn write_cell(cell: &Cell, value: StackValue) { } } -/// Reads an array cell's length and element. -pub(crate) fn array_len_and_element_at_cell( - cell: &Cell, - index: usize, -) -> Result<(usize, Option), &'static str> { - #[cfg(not(feature = "sync"))] - { - let stored = cell.borrow(); - let StackValue::Value(RuntimeValue::Array(array)) = &*stored else { - return Err("ForeachNext array slot is not an array"); - }; - Ok((array.len(), array.get(index).cloned())) - } - #[cfg(feature = "sync")] - { - let stored = cell.read().unwrap(); - let StackValue::Value(RuntimeValue::Array(array)) = &*stored else { - return Err("ForeachNext array slot is not an array"); - }; - Ok((array.len(), array.get(index).cloned())) - } -} - /// Appends to an array cell. pub(crate) fn append_to_array_cell(cell: &Cell, value: RuntimeValue) -> Result<(), &'static str> { #[cfg(not(feature = "sync"))] diff --git a/crates/mq-lang/src/vm_profile.rs b/crates/mq-lang/src/vm_profile.rs new file mode 100644 index 000000000..f4d953233 --- /dev/null +++ b/crates/mq-lang/src/vm_profile.rs @@ -0,0 +1,159 @@ +//! Execution-count profiling for Tarn bytecode. +//! +//! This module is available only with the `vm-profile` feature. It counts dispatched +//! instructions, rather than measuring elapsed time, because the act of profiling changes +//! dispatch performance. Use it to identify candidates for bytecode specialization, then use +//! the normal release benchmarks to measure a change. + +use std::cell::RefCell; +use std::collections::BTreeMap; +use std::fmt; + +thread_local! { + static ACTIVE_PROFILE: RefCell> = const { RefCell::new(None) }; +} + +/// A count of non-debug Tarn bytecode instructions dispatched during one profiled scope. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct VmProfile { + instruction_count: u64, + opcode_counts: BTreeMap<&'static str, u64>, +} + +impl VmProfile { + /// Returns the total number of non-debug bytecode instructions dispatched. + pub fn instruction_count(&self) -> u64 { + self.instruction_count + } + + /// Returns the number of executions of `opcode`. + pub fn opcode_count(&self, opcode: &str) -> u64 { + self.opcode_counts.get(opcode).copied().unwrap_or_default() + } + + /// Returns executed opcodes ordered by descending count, then opcode name. + pub fn most_executed(&self) -> Vec<(&'static str, u64)> { + let mut opcodes: Vec<_> = self + .opcode_counts + .iter() + .map(|(&opcode, &count)| (opcode, count)) + .collect(); + opcodes.sort_unstable_by(|(left_name, left_count), (right_name, right_count)| { + right_count.cmp(left_count).then_with(|| left_name.cmp(right_name)) + }); + opcodes + } + + fn record(&mut self, opcode: &'static str) { + self.instruction_count += 1; + *self.opcode_counts.entry(opcode).or_default() += 1; + } +} + +impl fmt::Display for VmProfile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "instructions: {}", self.instruction_count)?; + for (opcode, count) in self.most_executed() { + let percentage = if self.instruction_count == 0 { + 0.0 + } else { + count as f64 / self.instruction_count as f64 * 100.0 + }; + writeln!(f, " {opcode:<30} {count:>12} ({percentage:>5.1}%)")?; + } + Ok(()) + } +} + +/// Activates instruction counting for the current thread until [`Self::finish`] is called. +/// +/// Scopes may be nested. An inner scope temporarily replaces its parent's counters and restores +/// them when it finishes or is dropped. +pub struct VmProfileScope { + previous: Option, + finished: bool, +} + +impl VmProfileScope { + /// Starts a new instruction-count profile on the current thread. + pub fn start() -> Self { + let previous = ACTIVE_PROFILE.with(|profile| profile.replace(Some(VmProfile::default()))); + Self { + previous, + finished: false, + } + } + + /// Stops this scope, restores a possible parent scope, and returns its counters. + pub fn finish(mut self) -> VmProfile { + let profile = ACTIVE_PROFILE.with(|active| active.replace(self.previous.take()).unwrap_or_default()); + self.finished = true; + profile + } +} + +impl Drop for VmProfileScope { + fn drop(&mut self) { + if !self.finished { + ACTIVE_PROFILE.with(|active| { + active.replace(self.previous.take()); + }); + } + } +} + +/// Records one dispatched instruction when a profiling scope is active. +#[inline(always)] +pub(crate) fn record_opcode(opcode: &'static str) { + ACTIVE_PROFILE.with(|profile| { + if let Some(profile) = profile.borrow_mut().as_mut() { + profile.record(opcode); + } + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn profile_scope_counts_and_sorts_opcodes() { + let scope = VmProfileScope::start(); + record_opcode("Const"); + record_opcode("Add"); + record_opcode("Const"); + + let profile = scope.finish(); + assert_eq!(profile.instruction_count(), 3); + assert_eq!(profile.opcode_count("Const"), 2); + assert_eq!(profile.most_executed(), vec![("Const", 2), ("Add", 1)]); + } + + #[test] + fn nested_scope_restores_its_parent() { + let outer = VmProfileScope::start(); + record_opcode("Outer"); + let inner = VmProfileScope::start(); + record_opcode("Inner"); + assert_eq!(inner.finish().opcode_count("Inner"), 1); + record_opcode("Outer"); + assert_eq!(outer.finish().opcode_count("Outer"), 2); + } + + #[test] + fn engine_evaluation_records_dispatched_bytecode() { + let mut engine = crate::DefaultEngine::default(); + let scope = VmProfileScope::start(); + engine + .eval( + "var i = 3 | while(i > 0): i -= 1; | i", + std::iter::once(crate::RuntimeValue::None), + ) + .unwrap(); + + let profile = scope.finish(); + assert!(profile.instruction_count() > 0); + assert!(profile.opcode_count("GetLocal") > 0); + assert!(profile.opcode_count("Return") > 0); + } +} diff --git a/crates/mq-lang/tests/integration_tests.rs b/crates/mq-lang/tests/integration_tests.rs index 62ed98240..2a61efd2e 100644 --- a/crates/mq-lang/tests/integration_tests.rs +++ b/crates/mq-lang/tests/integration_tests.rs @@ -1,6 +1,4 @@ -use std::collections::BTreeMap; - -use mq_lang::{DefaultEngine, Engine, Ident, MqResult, RuntimeValue, Shared}; +use mq_lang::{DefaultEngine, DictMap, Engine, Ident, MqResult, RuntimeValue, Shared}; use rstest::{fixture, rstest}; #[fixture] @@ -975,7 +973,7 @@ fn engine() -> DefaultEngine { #[case::dict_map_identity(r#"let m = dict(["a", 1], ["b", 2]) | map(m, fn(kv): kv;)"#, vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("b"), RuntimeValue::Number(2.into())); dict.into() @@ -983,7 +981,7 @@ fn engine() -> DefaultEngine { #[case::dict_spread_basic("let base = {x: 1, y: 2} | {...base, z: 3}", vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("x"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("y"), RuntimeValue::Number(2.into())); dict.insert(Ident::new("z"), RuntimeValue::Number(3.into())); @@ -992,7 +990,7 @@ fn engine() -> DefaultEngine { #[case::dict_spread_later_key_overrides("let base = {x: 1, y: 2} | {...base, y: 99, z: 3}", vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("x"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("y"), RuntimeValue::Number(99.into())); dict.insert(Ident::new("z"), RuntimeValue::Number(3.into())); @@ -1001,7 +999,7 @@ fn engine() -> DefaultEngine { #[case::dict_spread_multiple("let a = {x: 1} | let b = {y: 2} | {...a, ...b}", vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("x"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("y"), RuntimeValue::Number(2.into())); dict.into() @@ -1009,7 +1007,7 @@ fn engine() -> DefaultEngine { #[case::dict_spread_none_contributes_nothing("{...None, x: 1}", vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("x"), RuntimeValue::Number(1.into())); dict.into() }].into()))] @@ -1021,7 +1019,7 @@ fn engine() -> DefaultEngine { ", vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("x"), RuntimeValue::Number(10.into())); dict.insert(Ident::new("y"), RuntimeValue::Number(20.into())); dict.into() @@ -1034,7 +1032,7 @@ fn engine() -> DefaultEngine { "#, vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("prefix_a"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("prefix_b"), RuntimeValue::Number(2.into())); dict.into() @@ -1052,7 +1050,7 @@ fn engine() -> DefaultEngine { "#, vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("num1_transformed"), RuntimeValue::Number(101.into())); dict.insert(Ident::new("num2_transformed"), RuntimeValue::Number(102.into())); dict.into() @@ -1065,7 +1063,7 @@ fn engine() -> DefaultEngine { "#, vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("b"), RuntimeValue::Number(2.into())); dict.insert(Ident::new("c"), RuntimeValue::Number(4.into())); dict.into() @@ -1080,7 +1078,7 @@ fn engine() -> DefaultEngine { ", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into()), RuntimeValue::Number(4.into()), RuntimeValue::Number(5.into()), RuntimeValue::Number(6.into()), RuntimeValue::Number(7.into()), RuntimeValue::Number(8.into()), RuntimeValue::Number(9.into())]))], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("0"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(3.into()), RuntimeValue::Number(6.into()), RuntimeValue::Number(9.into())]))); dict.insert(Ident::new("1"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(4.into()), RuntimeValue::Number(7.into())]))); dict.insert(Ident::new("2"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into()), RuntimeValue::Number(5.into()), RuntimeValue::Number(8.into())]))); @@ -1093,7 +1091,7 @@ fn engine() -> DefaultEngine { "#, vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("cat".to_string())), RuntimeValue::String(Shared::new("dog".to_string())), RuntimeValue::String(Shared::new("bird".to_string())), RuntimeValue::String(Shared::new("fish".to_string())), RuntimeValue::String(Shared::new("elephant".to_string()))]))], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("3"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("cat".to_string())), RuntimeValue::String(Shared::new("dog".to_string()))]))); dict.insert(Ident::new("4"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("bird".to_string())), RuntimeValue::String(Shared::new("fish".to_string()))]))); dict.insert(Ident::new("8"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("elephant".to_string()))]))); @@ -1113,7 +1111,7 @@ fn engine() -> DefaultEngine { ", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(42.into())]))], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("42"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(42.into())]))); dict.into() }].into()))] @@ -1124,7 +1122,7 @@ fn engine() -> DefaultEngine { "#, vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into()), RuntimeValue::Number(4.into())]))], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("same"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into()), RuntimeValue::Number(4.into())]))); dict.into() }].into()))] @@ -1135,7 +1133,7 @@ fn engine() -> DefaultEngine { ", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into()), RuntimeValue::Number(4.into()), RuntimeValue::Number(5.into()), RuntimeValue::Number(6.into())]))], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("false"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(3.into()), RuntimeValue::Number(5.into())]))); dict.insert(Ident::new("true"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into()), RuntimeValue::Number(4.into()), RuntimeValue::Number(6.into())]))); dict.into() @@ -1593,7 +1591,7 @@ fn engine() -> DefaultEngine { Ok(vec![RuntimeValue::Boolean(false)].into()))] #[case::any_dict_true(r#"any(dict(["a", 1], ["b", 2]), fn(kv): last(kv) == 2;)"#, vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("b"), RuntimeValue::Number(2.into())); dict.into() @@ -1601,7 +1599,7 @@ fn engine() -> DefaultEngine { Ok(vec![RuntimeValue::Boolean(true)].into()))] #[case::any_dict_false(r#"any(dict(["a", 1], ["b", 2]), fn(kv): last(kv) == 3;)"#, vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("b"), RuntimeValue::Number(2.into())); dict.into() @@ -1621,7 +1619,7 @@ fn engine() -> DefaultEngine { Ok(vec![RuntimeValue::Boolean(true)].into()))] #[case::all_dict_true(r#"all(dict(["a", 2], ["b", 4]), fn(kv): mod(last(kv), 2) == 0;)"#, vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(2.into())); dict.insert(Ident::new("b"), RuntimeValue::Number(4.into())); dict.into() @@ -1629,7 +1627,7 @@ fn engine() -> DefaultEngine { Ok(vec![RuntimeValue::Boolean(true)].into()))] #[case::all_dict_false(r#"all(dict(["a", 2], ["b", 3]), fn(kv): mod(last(kv), 2) == 0;)"#, vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(2.into())); dict.insert(Ident::new("b"), RuntimeValue::Number(3.into())); dict.into() @@ -1716,7 +1714,7 @@ fn engine() -> DefaultEngine { #[case::dict_literal_simple(r#"let d = {"a": 1, "b": "two"} | d"#, // Mixing string and ident keys vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("b"), RuntimeValue::String(Shared::new("two".to_string()))); dict.into() @@ -1943,7 +1941,7 @@ fn engine() -> DefaultEngine { } | d"#, vec![RuntimeValue::Number(0.into())], Ok(vec![{ - let mut dict = BTreeMap::new(); + let mut dict = DictMap::default(); dict.insert(Ident::new("a"), RuntimeValue::Number(1.into())); dict.insert(Ident::new("b"), RuntimeValue::Number(2.into())); dict.insert(Ident::new("c"), RuntimeValue::Number(3.into())); @@ -1978,12 +1976,12 @@ fn engine() -> DefaultEngine { vec![RuntimeValue::Number(0.into())], Ok(vec![RuntimeValue::Array(Shared::new(vec![ { - let mut d = BTreeMap::new(); + let mut d = DictMap::default(); d.insert(Ident::new("a"), RuntimeValue::Number(1.into())); RuntimeValue::Dict(Shared::new(d)) }, { - let mut d = BTreeMap::new(); + let mut d = DictMap::default(); d.insert(Ident::new("b"), RuntimeValue::Number(2.into())); RuntimeValue::Dict(Shared::new(d)) }, @@ -2549,21 +2547,21 @@ fn engine() -> DefaultEngine { #[case::lte_simple("lte(1, 2)", vec![RuntimeValue::None], Ok(vec![RuntimeValue::Boolean(true)].into()))] #[case::ne_simple("ne(1, 2)", vec![RuntimeValue::None], Ok(vec![RuntimeValue::Boolean(true)].into()))] #[case::csv_parse_simple(r##"_csv_parse("a,b\n1,2", ",", true)"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Array(Shared::new(vec![ - RuntimeValue::Dict(Shared::new(BTreeMap::from([ + RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string()))), (Ident::new("b"), RuntimeValue::String(Shared::new("2".to_string()))), ]))) ]))].into()))] -#[case::json_parse_simple(r##"_json_parse("{\"a\": 1}")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::json_parse_simple(r##"_json_parse("{\"a\": 1}")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("a"), RuntimeValue::Number(1.into())), ])))].into()))] -#[case::yaml_parse_simple(r##"_yaml_parse("a: 1")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::yaml_parse_simple(r##"_yaml_parse("a: 1")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("a"), RuntimeValue::Number(1.into())), ])))].into()))] -#[case::toml_parse_simple(r##"_toml_parse("a = 1")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::toml_parse_simple(r##"_toml_parse("a = 1")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("a"), RuntimeValue::Number(1.into())), ])))].into()))] -#[case::xml_parse_simple(r##"_xml_parse("text")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::xml_parse_simple(r##"_xml_parse("text")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("tag"), RuntimeValue::String(Shared::new("root".to_string()))), (Ident::new("attributes"), RuntimeValue::new_dict()), (Ident::new("children"), RuntimeValue::Array(Shared::new(vec![]))), @@ -2606,10 +2604,10 @@ fn engine() -> DefaultEngine { #[case::get_markdown_position_simple(r##"to_markdown("# title") | first() | _get_markdown_position() | get("start_line")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Number(1.into())].into()))] #[case::get_location_simple(r##"to_markdown("# title") | first() | get_location() | get("start_line")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Number(1.into())].into()))] #[case::get_location_non_markdown(r##"get_location("not a node")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::None].into()))] -#[case::toon_parse_simple(r##"_toon_parse("a: 1")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::toon_parse_simple(r##"_toon_parse("a: 1")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("a"), RuntimeValue::Number(1.into())), ])))].into()))] -#[case::capture_simple(r##"capture("abc123def", "(?P\\d+)")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::capture_simple(r##"capture("abc123def", "(?P\\d+)")"##, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("num"), RuntimeValue::String(Shared::new("123".to_string()))), ])))].into()))] #[case::is_debug_mode_simple("is_debug_mode()", vec![RuntimeValue::None], Ok(vec![RuntimeValue::Boolean(cfg!(feature = "debugger"))].into()))] @@ -2743,38 +2741,38 @@ fn engine() -> DefaultEngine { // partial: 2-param function can be partially applied — the scenario that triggered the redesign #[case::partial_two_param("def plus(a, b): a + b; | let plus10 = partial(plus, 10) | plus10(5)", vec![RuntimeValue::Number(0.into())], Ok(vec![RuntimeValue::Number(15.into())].into()))] // property selector: quoted form (."key") is the only way to access dict keys -#[case::property_selector_quoted_h1(r#"."h1""#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("h1"), RuntimeValue::String(Shared::new("title".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("title".to_string()))].into()))] -#[case::property_selector_quoted_url(r#"."url""#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("url"), RuntimeValue::String(Shared::new("https://example.com".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("https://example.com".to_string()))].into()))] -#[case::property_selector_quoted_text(r#"."text""#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("text"), RuntimeValue::String(Shared::new("hello".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("hello".to_string()))].into()))] +#[case::property_selector_quoted_h1(r#"."h1""#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("h1"), RuntimeValue::String(Shared::new("title".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("title".to_string()))].into()))] +#[case::property_selector_quoted_url(r#"."url""#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("url"), RuntimeValue::String(Shared::new("https://example.com".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("https://example.com".to_string()))].into()))] +#[case::property_selector_quoted_text(r#"."text""#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("text"), RuntimeValue::String(Shared::new("hello".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("hello".to_string()))].into()))] // property selector: quoted form with spaces in key -#[case::property_selector_quoted_space(r#"."my key""#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("my key"), RuntimeValue::String(Shared::new("val".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("val".to_string()))].into()))] +#[case::property_selector_quoted_space(r#"."my key""#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("my key"), RuntimeValue::String(Shared::new("val".to_string()))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("val".to_string()))].into()))] // property selector: missing key returns None -#[case::property_selector_quoted_missing(r#"."h1""#, vec![{let d = std::collections::BTreeMap::new(); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::None].into()))] +#[case::property_selector_quoted_missing(r#"."h1""#, vec![{let d = DictMap::default(); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::None].into()))] // nested property selector: ."a"."b" accesses {"a": {"b": 1}} -#[case::property_selector_nested(r#"."a"."b""#, vec![{let mut outer = std::collections::BTreeMap::new(); let mut inner = std::collections::BTreeMap::new(); inner.insert(Ident::new("b"), RuntimeValue::Number(1.into())); outer.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(inner))); RuntimeValue::Dict(Shared::new(outer))}], Ok(vec![RuntimeValue::Number(1.into())].into()))] +#[case::property_selector_nested(r#"."a"."b""#, vec![{let mut outer = DictMap::default(); let mut inner = DictMap::default(); inner.insert(Ident::new("b"), RuntimeValue::Number(1.into())); outer.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(inner))); RuntimeValue::Dict(Shared::new(outer))}], Ok(vec![RuntimeValue::Number(1.into())].into()))] // nested property selector: ."a"."b"."c" accesses three levels deep -#[case::property_selector_nested_three(r#"."a"."b"."c""#, vec![{let mut outer = std::collections::BTreeMap::new(); let mut mid = std::collections::BTreeMap::new(); let mut inner = std::collections::BTreeMap::new(); inner.insert(Ident::new("c"), RuntimeValue::Number(42.into())); mid.insert(Ident::new("b"), RuntimeValue::Dict(Shared::new(inner))); outer.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(mid))); RuntimeValue::Dict(Shared::new(outer))}], Ok(vec![RuntimeValue::Number(42.into())].into()))] +#[case::property_selector_nested_three(r#"."a"."b"."c""#, vec![{let mut outer = DictMap::default(); let mut mid = DictMap::default(); let mut inner = DictMap::default(); inner.insert(Ident::new("c"), RuntimeValue::Number(42.into())); mid.insert(Ident::new("b"), RuntimeValue::Dict(Shared::new(inner))); outer.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(mid))); RuntimeValue::Dict(Shared::new(outer))}], Ok(vec![RuntimeValue::Number(42.into())].into()))] // nested property selector: missing intermediate key returns None -#[case::property_selector_nested_missing(r#"."a"."b""#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("a"), RuntimeValue::Number(1.into())); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::None].into()))] +#[case::property_selector_nested_missing(r#"."a"."b""#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("a"), RuntimeValue::Number(1.into())); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::None].into()))] // property selector on an array of dicts: maps over each element #[case::property_selector_array_of_dicts(r#"."name""#, vec![RuntimeValue::Array(Shared::new(vec![ - {let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); RuntimeValue::Dict(Shared::new(d))}, - {let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Bob".to_string()))); RuntimeValue::Dict(Shared::new(d))}, - {let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Charlie".to_string()))); RuntimeValue::Dict(Shared::new(d))}, + {let mut d = DictMap::default(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); RuntimeValue::Dict(Shared::new(d))}, + {let mut d = DictMap::default(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Bob".to_string()))); RuntimeValue::Dict(Shared::new(d))}, + {let mut d = DictMap::default(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Charlie".to_string()))); RuntimeValue::Dict(Shared::new(d))}, ]))], Ok(vec![RuntimeValue::Array(Shared::new(vec!["Alice".into(), "Bob".into(), "Charlie".into()]))].into()))] // property selector on an array of dicts: non-dict elements map to None #[case::property_selector_array_of_dicts_non_dict_element(r#"."name""#, vec![RuntimeValue::Array(Shared::new(vec![ - {let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); RuntimeValue::Dict(Shared::new(d))}, + {let mut d = DictMap::default(); d.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string()))); RuntimeValue::Dict(Shared::new(d))}, RuntimeValue::Number(1.into()), ]))], Ok(vec![RuntimeValue::Array(Shared::new(vec!["Alice".into(), RuntimeValue::None]))].into()))] // property iterator: ."items"[] iterates all elements of the array stored at the key -#[case::property_selector_iterator(r#"."items"[]"#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string())), RuntimeValue::String(Shared::new("c".to_string()))]))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string())), RuntimeValue::String(Shared::new("c".to_string()))]))].into()))] +#[case::property_selector_iterator(r#"."items"[]"#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string())), RuntimeValue::String(Shared::new("c".to_string()))]))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string())), RuntimeValue::String(Shared::new("c".to_string()))]))].into()))] // property iterator with index: ."items"[0] accesses the first element of the array -#[case::property_selector_iterator_index(r#"."items"[0]"#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("a".to_string()))].into()))] +#[case::property_selector_iterator_index(r#"."items"[0]"#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("a".to_string()))].into()))] // property iterator with index: ."items"[1] accesses the second element -#[case::property_selector_iterator_index_1(r#"."items"[1]"#, vec![{let mut d = std::collections::BTreeMap::new(); d.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("b".to_string()))].into()))] +#[case::property_selector_iterator_index_1(r#"."items"[1]"#, vec![{let mut d = DictMap::default(); d.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string())), RuntimeValue::String(Shared::new("b".to_string()))]))); RuntimeValue::Dict(Shared::new(d))}], Ok(vec![RuntimeValue::String(Shared::new("b".to_string()))].into()))] // chained property iterator: ."a"."b"[] iterates all elements of a nested array -#[case::property_selector_nested_iterator(r#"."a"."b"[]"#, vec![{let mut outer = std::collections::BTreeMap::new(); let mut inner = std::collections::BTreeMap::new(); inner.insert(Ident::new("b"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]))); outer.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(inner))); RuntimeValue::Dict(Shared::new(outer))}], Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]))].into()))] +#[case::property_selector_nested_iterator(r#"."a"."b"[]"#, vec![{let mut outer = DictMap::default(); let mut inner = DictMap::default(); inner.insert(Ident::new("b"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]))); outer.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(inner))); RuntimeValue::Dict(Shared::new(outer))}], Ok(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]))].into()))] // paren-free calls: 0-arg user-defined function called without parentheses #[case::paren_free_zero_arg_user_fn("def greet(): \"Hello!\"; | greet", vec![RuntimeValue::None], Ok(vec![RuntimeValue::String(Shared::new("Hello!".to_string()))].into()))] // paren-free calls: 1-arg user-defined function called without parentheses uses current value @@ -2911,7 +2909,7 @@ fn engine() -> DefaultEngine { // try/catch(e): error binder is bound to a dict with the failure message #[case::try_catch_binder(r#"try: error("boom") catch(e): e["message"]"#, vec![RuntimeValue::None], Ok(vec![RuntimeValue::String(Shared::new("boom".to_string()))].into()))] // try/catch(e): the full error dict is accessible when bound directly -#[case::try_catch_binder_dict(r#"try: error("boom") catch(e): e"#, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(BTreeMap::from([ +#[case::try_catch_binder_dict(r#"try: error("boom") catch(e): e"#, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter([ (Ident::new("message"), RuntimeValue::String(Shared::new("boom".to_string()))), ])))].into()))] // try/catch(e): the binder is unused when the try expression succeeds @@ -3063,7 +3061,7 @@ fn engine() -> DefaultEngine { // del: None returns None #[case::del_none("del(None, 0)", vec![RuntimeValue::None], Ok(vec![RuntimeValue::None].into()))] // del: remove key from dict by string -#[case::del_dict_string(r#"del({"a": 1, "b": 2}, "a")"#, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict({let mut m = std::collections::BTreeMap::new(); m.insert(mq_lang::Ident::new("b"), RuntimeValue::Number(2.into())); Shared::new(m)})].into()))] +#[case::del_dict_string(r#"del({"a": 1, "b": 2}, "a")"#, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Dict({let mut m = DictMap::default(); m.insert(mq_lang::Ident::new("b"), RuntimeValue::Number(2.into())); Shared::new(m)})].into()))] // index: bytes haystack #[case::index_bytes(r#"index(b"hello", b"ll")"#, vec![RuntimeValue::None], Ok(vec![RuntimeValue::Number(2.into())].into()))] // index: bytes not found @@ -3583,50 +3581,11 @@ fn test_eval(mut engine: Engine, #[case] program: &str, #[case] input: Vec, - #[case] expected: MqResult, -) { - assert_eq!(engine.eval(program, input.into_iter()), expected); -} - #[rstest] #[case::invalid_function_syntax("f()def f(): 1", vec![RuntimeValue::Number(0.into())])] -#[case::func("def func1(): 1 | func1(); | func1()", vec![RuntimeValue::Number(0.into())])] +// Keep this an arity failure rather than an unbounded tail-recursive definition. Tail calls +// intentionally reuse their frame and therefore do not consume the stack-depth limit. +#[case::func("def func1(): 1; | func1(1)", vec![RuntimeValue::Number(0.into())])] #[case::func("def func1(x): 1; | func1(1, 2)", vec![RuntimeValue::Number(0.into())])] #[case::func_invalid_definition("def f(x): 1; | f2(1, 2)", vec![RuntimeValue::Number(0.into())])] #[case::invalid_definition("func1(1, 2)", vec![RuntimeValue::Number(0.into())])] @@ -3647,7 +3606,7 @@ fn test_get_set_variable_deprecated( #[case::regex_invalid_pattern(r#""abc" =~ "[invalid""#, vec![RuntimeValue::None],)] #[case::is_regex_match_invalid_pattern(r#"is_regex_match("abc", "[invalid")"#, vec![RuntimeValue::None],)] // recursion depth exceeded -#[case::recursion_limit("def f(x): f(x); | f(1)", vec![RuntimeValue::None],)] +#[case::recursion_limit("def f(x): 1 + f(x); | f(1)", vec![RuntimeValue::None],)] // too many args to a user-defined function #[case::too_many_args_user_fn("def f(x): x; | f(1, 2, 3)", vec![RuntimeValue::None],)] // too few args to a variadic user-defined function (need 2+ required, given 0) @@ -3852,7 +3811,7 @@ mod ast_json { #[case( Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::String("hello".to_string()))), + expr: AstExpr::Literal(AstLiteral::String("hello".to_string())), }), Some(vec!["Literal", "String", "hello"]), true @@ -3860,7 +3819,7 @@ mod ast_json { #[case( Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::Number(123.45.into()))), + expr: AstExpr::Literal(AstLiteral::Number(123.45.into())), }), Some(vec!["Literal", "Number", "123.45"]), true @@ -3868,7 +3827,7 @@ mod ast_json { #[case( Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Ident(mq_lang::IdentWithToken::new("my_var"))), + expr: AstExpr::Ident(mq_lang::IdentWithToken::new("my_var")), }), Some(vec!["Ident", "my_var"]), true @@ -3876,13 +3835,13 @@ mod ast_json { #[case( Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Call( + expr: AstExpr::Call( mq_lang::IdentWithToken::new("my_func"), smallvec![Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::Number(1.into()))), + expr: AstExpr::Literal(AstLiteral::Number(1.into())), })], - )), + ), }), Some(vec!["Call", "my_func", "Literal", "Number", "1.0"]), true @@ -3890,18 +3849,18 @@ mod ast_json { #[case( Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::If(smallvec![ + expr: AstExpr::If(smallvec![ ( Some(Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::Bool(true))), + expr: AstExpr::Literal(AstLiteral::Bool(true)), })), Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::String("then_branch".to_string()))), + expr: AstExpr::Literal(AstLiteral::String("then_branch".to_string())), }) ) - ])), + ]), }), Some(vec!["If", "Bool", "true", "String", "then_branch"]), false @@ -3922,7 +3881,7 @@ mod ast_json { if check_token_id { assert_eq!(deserialized_node.token_id, default_token_id()); } - if let AstExpr::Ident(ident) = &*deserialized_node.expr { + if let AstExpr::Ident(ident) = &deserialized_node.expr { assert_eq!(ident.token, None); } } @@ -3931,11 +3890,11 @@ mod ast_json { fn test_program_serialization_deserialization() { let node1 = Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::String("first".to_string()))), + expr: AstExpr::Literal(AstLiteral::String("first".to_string())), }); let node2 = Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(AstExpr::Literal(AstLiteral::Number(10.into()))), + expr: AstExpr::Literal(AstLiteral::Number(10.into())), }); let original_program: Program = vec![node1, node2]; diff --git a/crates/mq-lang/tests/property_based_tests.rs b/crates/mq-lang/tests/property_based_tests.rs index 30444fd8e..af81a2a87 100644 --- a/crates/mq-lang/tests/property_based_tests.rs +++ b/crates/mq-lang/tests/property_based_tests.rs @@ -17,7 +17,7 @@ fn default_token_id() -> mq_lang::ArenaId> { fn make_node(expr: AstExpr) -> Shared { Shared::new(AstNode { token_id: default_token_id(), - expr: Shared::new(expr), + expr, }) } @@ -247,7 +247,7 @@ proptest! { prop_assert!(!program.is_empty(), "Parsed program is empty"); - if let AstExpr::Literal(parsed_lit) = &*program[0].expr { + if let AstExpr::Literal(parsed_lit) = &program[0].expr { prop_assert!( assertions::literals_equal(&lit, parsed_lit), "Literals differ: {:?} vs {:?}", lit, parsed_lit @@ -263,7 +263,7 @@ proptest! { let program = assertions::assert_parses(&code)?; prop_assert!(!program.is_empty()); - if let AstExpr::Ident(parsed_ident) = &*program[0].expr { + if let AstExpr::Ident(parsed_ident) = &program[0].expr { prop_assert_eq!(ident.name, parsed_ident.name); } } @@ -285,7 +285,7 @@ proptest! { let program = assertions::assert_parses(&code1)?; prop_assert!(!program.is_empty()); - prop_assert!(matches!(&*program[0].expr, AstExpr::Let(_, _))); + prop_assert!(matches!(&program[0].expr, AstExpr::Let(_, _))); let code2 = program[0].to_code(); prop_assert_eq!(code1, code2, "Code roundtrip failed for let expression"); @@ -297,7 +297,7 @@ proptest! { let program = assertions::assert_parses(&code1)?; prop_assert!(!program.is_empty()); - prop_assert!(matches!(&*program[0].expr, AstExpr::Var(_, _))); + prop_assert!(matches!(&program[0].expr, AstExpr::Var(_, _))); let code2 = program[0].to_code(); prop_assert_eq!(code1, code2, "Code roundtrip failed for var expression"); @@ -324,7 +324,7 @@ proptest! { let program = assertions::assert_parses(&code1)?; prop_assert!(!program.is_empty()); - prop_assert!(matches!(&*program[0].expr, AstExpr::If(_))); + prop_assert!(matches!(&program[0].expr, AstExpr::If(_))); let code2 = program[0].to_code(); prop_assert_eq!(code1, code2, "Code roundtrip failed for if expression"); @@ -433,7 +433,7 @@ proptest! { let program = assertions::assert_parses(&code1)?; prop_assert!(!program.is_empty()); - prop_assert!(matches!(&*program[0].expr, AstExpr::Assign(_, _))); + prop_assert!(matches!(&program[0].expr, AstExpr::Assign(_, _))); let code2 = program[0].to_code(); prop_assert_eq!(code1, code2, "Code roundtrip failed for assignment expression"); @@ -456,7 +456,7 @@ proptest! { let program = assertions::assert_parses(&code1)?; prop_assert!(!program.is_empty()); - prop_assert!(matches!(&*program[0].expr, AstExpr::If(_))); + prop_assert!(matches!(&program[0].expr, AstExpr::If(_))); let code2 = program[0].to_code(); prop_assert_eq!(code1, code2, "Code roundtrip failed for complex if expression"); @@ -544,10 +544,10 @@ proptest! { prop_assert!(!program.is_empty()); let matches_expected = match keyword { - "self" => matches!(&*program[0].expr, AstExpr::Self_), - "nodes" => matches!(&*program[0].expr, AstExpr::Nodes), - "break" => matches!(&*program[0].expr, AstExpr::Break(_)), - "continue" => matches!(&*program[0].expr, AstExpr::Continue), + "self" => matches!(&program[0].expr, AstExpr::Self_), + "nodes" => matches!(&program[0].expr, AstExpr::Nodes), + "break" => matches!(&program[0].expr, AstExpr::Break(_)), + "continue" => matches!(&program[0].expr, AstExpr::Continue), _ => false, }; @@ -560,7 +560,7 @@ proptest! { let code = node.to_code(); let program = assertions::assert_parses(&code)?; - if let AstExpr::Literal(AstLiteral::Bool(parsed)) = &*program[0].expr { + if let AstExpr::Literal(AstLiteral::Bool(parsed)) = &program[0].expr { prop_assert_eq!(b, *parsed); } else { prop_assert!(false, "Expected bool literal"); diff --git a/crates/mq-markdown/src/node.rs b/crates/mq-markdown/src/node.rs index 10ec287a5..f63d45da9 100644 --- a/crates/mq-markdown/src/node.rs +++ b/crates/mq-markdown/src/node.rs @@ -938,6 +938,19 @@ impl Node { Self::_map_values(self, f) } + /// Maps this node and its fragment descendants, giving the callback ownership of each node. + /// + /// This is useful when a transform must retain an unchanged fallback node without cloning it + /// again after the callback returns. The traversal continues through a fragment returned by + /// the callback, matching [`Self::map_values_into`]'s behavior. + pub fn map_values_into_owned(self, f: &mut F) -> Result + where + E: std::error::Error, + F: FnMut(Node) -> Result, + { + Self::_map_values_owned(self, f) + } + fn _map_values(node: Node, f: &mut F) -> Result where E: std::error::Error, @@ -962,8 +975,40 @@ impl Node { } } + fn _map_values_owned(node: Node, f: &mut F) -> Result + where + E: std::error::Error, + F: FnMut(Node) -> Result, + { + match f(node)? { + Node::Fragment(mut v) => { + let values = v + .values + .into_iter() + .map(|node| Self::_map_values_owned(node, f)) + .collect::, _>>(); + match values { + Ok(values) => { + v.values = values; + Ok(Node::Fragment(v)) + } + Err(e) => Err(e), + } + } + node => Ok(node), + } + } + pub fn to_fragment(&self) -> Node { - match self.clone() { + self.clone().into_fragment() + } + + /// Converts this node into a fragment, preserving its children without cloning them. + /// + /// Leaf nodes become [`Node::Empty`]. This is the consuming counterpart to + /// [`Self::to_fragment`]. + pub fn into_fragment(self) -> Node { + match self { Node::List(List { values, .. }) | Node::TableCell(TableCell { values, .. }) | Node::TableRow(TableRow { values, .. }) @@ -1054,7 +1099,7 @@ impl Node { } pub(crate) fn render_with_theme(&self, options: &RenderOptions, theme: &ColorTheme<'_>) -> String { - match self.clone() { + match self { Self::List(List { level, checked, @@ -1064,13 +1109,13 @@ impl Node { start, .. }) => { - let marker = if ordered { - format!("{}.", start.unwrap_or(1) as usize + index) + let marker = if *ordered { + format!("{}.", start.unwrap_or(1) as usize + *index) } else { options.list_style.to_string() }; - let checkbox = checked.map(|it| if it { "[x] " } else { "[ ] " }).unwrap_or_else(|| ""); - let prefix_width = level as usize * 2 + list_own_prefix_width(ordered, index, start, checked); + let checkbox = (*checked).map(|it| if it { "[x] " } else { "[ ] " }).unwrap_or(""); + let prefix_width = *level as usize * 2 + list_own_prefix_width(*ordered, *index, *start, *checked); // A block quote/callout child needs a flat prefix_width add, not the delta below. let delta = if values.first().is_some_and(Self::is_blockquote_like) { prefix_width as isize @@ -1081,12 +1126,12 @@ impl Node { .map(|c| prefix_width as isize - (c as isize - 1)) .unwrap_or(0) }; - let content = reindent_continuation(&render_values(&values, options, theme), delta); + let content = reindent_continuation(&render_values(values, options, theme), delta); let content = reindent_first_leaf_block(content, values.first(), options, theme, prefix_width); let (ms, me) = &theme.list_marker; format!( "{}{}{}{} {}{}", - " ".repeat(level as usize), + " ".repeat(*level as usize), ms, marker, me, @@ -1103,14 +1148,14 @@ impl Node { .join("|"); format!("{}|{}{}|", ts, te, cells) } - Self::TableCell(TableCell { values, .. }) => render_values(&values, options, theme), + Self::TableCell(TableCell { values, .. }) => render_values(values, options, theme), Self::TableAlign(TableAlign { align, .. }) => { let (ts, te) = &theme.table_separator; format!("{}|{}|{}", ts, align.iter().map(|a| a.to_string()).join("|"), te) } Self::Blockquote(Blockquote { values, .. }) => { let (bs, be) = &theme.blockquote_marker; - render_values_block(&values, options, theme) + render_values_block(values, options, theme) .split('\n') .map(|line| format!("{}> {}{}", bs, be, line)) .join("\n") @@ -1128,7 +1173,7 @@ impl Node { if values.is_empty() { return header_line; } - let body = render_values_block(&values, options, theme); + let body = render_values_block(values, options, theme); if body.trim().is_empty() { header_line } else { @@ -1152,11 +1197,11 @@ impl Node { .. }) => { let (cs, ce) = &theme.code; - if lang.is_some() || fence { + if lang.is_some() || *fence { let meta = meta.as_deref().map(|meta| format!(" {}", meta)).unwrap_or_default(); let info = format!("{}{}", lang.as_deref().unwrap_or(""), meta); // Empty body skips the content line so it doesn't gain a blank one. - let fence_str = code_fence(&value, &info); + let fence_str = code_fence(value, &info); if value.is_empty() { format!("{}{}{}\n{}{}", cs, fence_str, info, fence_str, ce) } else { @@ -1176,18 +1221,19 @@ impl Node { let (us, ue) = &theme.link_url; format!( "[{}]: {}{}{}{}", - escape_label(&label.unwrap_or(ident)), + escape_label(label.as_deref().unwrap_or(ident)), us, url.to_string_with(options), ue, title + .as_ref() .map(|title| format!(" {}", title.to_string_with(options))) .unwrap_or_default() ) } Self::Delete(Delete { values, .. }) => { let (ds, de) = &theme.delete; - format!("{}~~{}~~{}", ds, render_values(&values, options, theme), de) + format!("{}~~{}~~{}", ds, render_values(values, options, theme), de) } Self::Emphasis(Emphasis { values, .. }) => { let (es, ee) = &theme.emphasis; @@ -1202,27 +1248,23 @@ impl Node { "{}{}{}{}{}", es, delim, - render_values(&values, options, theme), + render_values(values, options, theme), delim, ee ) } Self::Footnote(Footnote { values, ident, .. }) => { - format!( - "[^{}]: {}", - escape_label(&ident), - render_values(&values, options, theme) - ) + format!("[^{}]: {}", escape_label(ident), render_values(values, options, theme)) } Self::FootnoteRef(FootnoteRef { label, .. }) => { - format!("[^{}]", escape_label(&label.unwrap_or_default())) + format!("[^{}]", escape_label(label.as_deref().unwrap_or_default())) } Self::Heading(Heading { depth, values, .. }) => { let (hs, he) = &theme.heading; - let text = render_values(&values, options, theme); + let text = render_values(values, options, theme); // Multi-line content must stay setext for depths 1-2; ATX has no setext form. if text.contains('\n') && matches!(depth, 1 | 2) { - let underline = if depth == 1 { "===" } else { "---" }; + let underline = if *depth == 1 { "===" } else { "---" }; format!("{}{}\n{}{}", hs, text, underline, he) } else { // A trailing `#` run reads back as an ATX closing sequence and gets @@ -1240,7 +1282,7 @@ impl Node { None if !text.is_empty() => format!("\\{text}"), _ => text, }; - format!("{}{} {}{}", hs, "#".repeat(depth as usize), text, he) + format!("{}{} {}{}", hs, "#".repeat(*depth as usize), text, he) } } Self::Html(Html { value, .. }) => { @@ -1252,10 +1294,11 @@ impl Node { format!( "{}![{}]({}{}){}", is, - escape_label(&alt), - render_link_destination(&url, &options.link_url_style), + escape_label(alt), + render_link_destination(url, &options.link_url_style), title - .map(|it| format!(" {}", render_link_title(&it, &options.link_title_style))) + .as_deref() + .map(|it| format!(" {}", render_link_title(it, &options.link_title_style))) .unwrap_or_default(), ie ) @@ -1264,16 +1307,16 @@ impl Node { // (already correctly escaped), so escape_label would double it up. Self::ImageRef(ImageRef { alt, ident, .. }) => { let (is, ie) = &theme.image; - let mismatched = normalize_reference_identifier(&alt) != ident; - if mismatched || needs_broad_escaping(&alt) { - format!("{}![{}][{}]{}", is, escape_label(&alt), ident, ie) + let mismatched = normalize_reference_identifier(alt) != ident.as_str(); + if mismatched || needs_broad_escaping(alt) { + format!("{}![{}][{}]{}", is, escape_label(alt), ident, ie) } else { - format!("{}![{}]{}", is, escape_label(&alt), ie) + format!("{}![{}]{}", is, escape_label(alt), ie) } } Self::CodeInline(CodeInline { value, .. }) => { let (cs, ce) = &theme.code_inline; - let fence = code_span_fence(&value); + let fence = code_span_fence(value); // Padding avoids fusing with an edge backtick and protects a genuine // leading+trailing space from the parser's own space-stripping rule. let all_spaces = value.chars().all(|c| c == ' '); @@ -1295,9 +1338,10 @@ impl Node { format!( "{}[{}]({}{}){}", ls, - render_values(&values, options, theme), + render_values(values, options, theme), url.to_string_with(options), title + .as_ref() .map(|title| format!(" {}", title.to_string_with(options))) .unwrap_or_default(), le @@ -1314,9 +1358,9 @@ impl Node { // Same reasoning as ImageRef, plus the same broad-escaping fallback. Self::LinkRef(LinkRef { values, ident, .. }) => { let (ls, le) = &theme.link; - let rendered = render_values(&values, options, theme); + let rendered = render_values(values, options, theme); let plain = values_to_value(values); - let mismatched = normalize_reference_identifier(&plain) != ident; + let mismatched = normalize_reference_identifier(&plain) != ident.as_str(); if mismatched || needs_broad_escaping(&plain) { format!("{}[{}][{}]{}", ls, rendered, ident, le) @@ -1332,16 +1376,16 @@ impl Node { // values like JSON output or attr lookups must stay untouched. Self::Text(Text { value, position }) => { if position.is_some() { - escape_text(value) + escape_text(value.clone()) } else { - value + value.clone() } } Self::MdxFlowExpression(mdx_flow_expression) => { format!("{{{}}}", mdx_flow_expression.value) } Self::MdxJsxFlowElement(mdx_jsx_flow_element) => { - let name = mdx_jsx_flow_element.name.unwrap_or_default(); + let name = mdx_jsx_flow_element.name.as_deref().unwrap_or_default(); let attributes = if mdx_jsx_flow_element.attributes.is_empty() { "".to_string() } else { @@ -1349,7 +1393,7 @@ impl Node { " {}", mdx_jsx_flow_element .attributes - .into_iter() + .iter() .map(Self::mdx_attribute_content_to_string) .join(" ") ) @@ -1368,7 +1412,7 @@ impl Node { } } Self::MdxJsxTextElement(mdx_jsx_text_element) => { - let name = mdx_jsx_text_element.name.unwrap_or_default(); + let name = mdx_jsx_text_element.name.as_deref().unwrap_or_default(); let attributes = if mdx_jsx_text_element.attributes.is_empty() { "".to_string() } else { @@ -1376,7 +1420,7 @@ impl Node { " {}", mdx_jsx_text_element .attributes - .into_iter() + .iter() .map(Self::mdx_attribute_content_to_string) .join(" ") ) @@ -1434,15 +1478,15 @@ impl Node { } pub fn node_values(&self) -> Vec { - match self.clone() { - Self::Blockquote(v) => v.values, - Self::Delete(v) => v.values, - Self::Heading(h) => h.values, - Self::Emphasis(v) => v.values, - Self::List(l) => l.values, - Self::Strong(v) => v.values, + match self { + Self::Blockquote(v) => v.values.clone(), + Self::Delete(v) => v.values.clone(), + Self::Heading(h) => h.values.clone(), + Self::Emphasis(v) => v.values.clone(), + Self::List(l) => l.values.clone(), + Self::Strong(v) => v.values.clone(), #[cfg(feature = "callout")] - Self::Callout(v) => v.values, + Self::Callout(v) => v.values.clone(), _ => vec![self.clone()], } } @@ -1464,45 +1508,45 @@ impl Node { } pub fn value(&self) -> String { - match self.clone() { - Self::Blockquote(v) => values_to_value(v.values), + match self { + Self::Blockquote(v) => values_to_value(&v.values), Self::Definition(d) => d.url.as_str().to_string(), - Self::Delete(v) => values_to_value(v.values), - Self::Heading(h) => values_to_value(h.values), - Self::Emphasis(v) => values_to_value(v.values), - Self::Footnote(f) => values_to_value(f.values), - Self::FootnoteRef(f) => f.ident, - Self::Html(v) => v.value, - Self::Yaml(v) => v.value, - Self::Toml(v) => v.value, - Self::Image(i) => i.url, - Self::ImageRef(i) => i.ident, + Self::Delete(v) => values_to_value(&v.values), + Self::Heading(h) => values_to_value(&h.values), + Self::Emphasis(v) => values_to_value(&v.values), + Self::Footnote(f) => values_to_value(&f.values), + Self::FootnoteRef(f) => f.ident.clone(), + Self::Html(v) => v.value.clone(), + Self::Yaml(v) => v.value.clone(), + Self::Toml(v) => v.value.clone(), + Self::Image(i) => i.url.clone(), + Self::ImageRef(i) => i.ident.clone(), Self::CodeInline(v) => v.value.to_string(), Self::MathInline(v) => v.value.to_string(), Self::Link(l) => l.url.as_str().to_string(), - Self::LinkRef(l) => l.ident, + Self::LinkRef(l) => l.ident.clone(), #[cfg(feature = "wikilink")] - Self::WikiLink(w) => w.text.unwrap_or(w.target), + Self::WikiLink(w) => w.text.clone().unwrap_or_else(|| w.target.clone()), #[cfg(feature = "callout")] - Self::Callout(v) => values_to_value(v.values), + Self::Callout(v) => values_to_value(&v.values), #[cfg(feature = "embed")] - Self::Embed(e) => e.display.unwrap_or(e.target), - Self::Math(v) => v.value, - Self::List(l) => values_to_value(l.values), - Self::TableCell(c) => values_to_value(c.values), - Self::TableRow(c) => values_to_value(c.values), - Self::Code(c) => c.value, - Self::Strong(v) => values_to_value(v.values), - Self::Text(t) => t.value, + Self::Embed(e) => e.display.clone().unwrap_or_else(|| e.target.clone()), + Self::Math(v) => v.value.clone(), + Self::List(l) => values_to_value(&l.values), + Self::TableCell(c) => values_to_value(&c.values), + Self::TableRow(c) => values_to_value(&c.values), + Self::Code(c) => c.value.clone(), + Self::Strong(v) => values_to_value(&v.values), + Self::Text(t) => t.value.clone(), Self::Break { .. } => String::new(), Self::TableAlign(_) => String::new(), Self::MdxFlowExpression(mdx) => mdx.value.to_string(), - Self::MdxJsxFlowElement(mdx) => values_to_value(mdx.children), + Self::MdxJsxFlowElement(mdx) => values_to_value(&mdx.children), Self::MdxTextExpression(mdx) => mdx.value.to_string(), - Self::MdxJsxTextElement(mdx) => values_to_value(mdx.children), + Self::MdxJsxTextElement(mdx) => values_to_value(&mdx.children), Self::MdxJsEsm(mdx) => mdx.value.to_string(), Self::HorizontalRule { .. } => String::new(), - Self::Fragment(v) => values_to_value(v.values), + Self::Fragment(v) => values_to_value(&v.values), Self::Empty => String::new(), } } @@ -1892,26 +1936,36 @@ impl Node { } } + fn replace_value_at(values: &mut [Node], index: usize, value: &str) { + if let Some(slot) = values.get_mut(index) { + let node = std::mem::replace(slot, Self::Empty); + *slot = node.into_with_value(value); + } + } + + /// Returns a clone of this node with its value replaced. pub fn with_value(&self, value: &str) -> Self { - match self.clone() { + self.clone().into_with_value(value) + } + + /// Replaces this node's value while consuming the original tree. + /// + /// Prefer this over [`Self::with_value`] when the caller owns the node and does not need an + /// unchanged copy. + pub fn into_with_value(self, value: &str) -> Self { + match self { Self::Blockquote(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::Blockquote(v) } Self::Delete(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::Delete(v) } Self::Emphasis(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::Emphasis(v) } @@ -1940,33 +1994,27 @@ impl Node { Self::Math(math) } Self::List(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::List(v) } Self::TableCell(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::TableCell(v) } Self::TableRow(mut row) => { row.values = row .values - .iter() + .into_iter() .zip(value.split(",")) - .map(|(cell, value)| cell.with_value(value)) + .map(|(cell, value)| cell.into_with_value(value)) .collect::>(); Self::TableRow(row) } Self::Strong(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::Strong(v) } @@ -2002,9 +2050,7 @@ impl Node { Self::FootnoteRef(footnote) } Self::Heading(mut v) => { - if let Some(node) = v.values.first() { - v.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut v.values, 0, value); Self::Heading(v) } @@ -2032,9 +2078,7 @@ impl Node { Self::MdxJsEsm(mdx) } Self::MdxJsxFlowElement(mut mdx) => { - if let Some(node) = mdx.children.first() { - mdx.children[0] = node.with_value(value); - } + Self::replace_value_at(&mut mdx.children, 0, value); Self::MdxJsxFlowElement(MdxJsxFlowElement { name: mdx.name, @@ -2044,9 +2088,7 @@ impl Node { }) } Self::MdxJsxTextElement(mut mdx) => { - if let Some(node) = mdx.children.first() { - mdx.children[0] = node.with_value(value); - } + Self::replace_value_at(&mut mdx.children, 0, value); Self::MdxJsxTextElement(MdxJsxTextElement { name: mdx.name, @@ -2067,9 +2109,7 @@ impl Node { } #[cfg(feature = "callout")] Self::Callout(mut c) => { - if let Some(node) = c.values.first() { - c.values[0] = node.with_value(value); - } + Self::replace_value_at(&mut c.values, 0, value); Self::Callout(c) } #[cfg(feature = "embed")] @@ -2084,68 +2124,59 @@ impl Node { } } + /// Returns a clone of this node with the selected child's value replaced. pub fn with_children_value(&self, value: &str, index: usize) -> Self { - match self.clone() { + self.clone().into_with_children_value(value, index) + } + + /// Replaces a selected child's value while consuming the original tree. + /// + /// Prefer this over [`Self::with_children_value`] when the caller owns the node and does not + /// need an unchanged copy. + pub fn into_with_children_value(self, value: &str, index: usize) -> Self { + match self { Self::Blockquote(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::Blockquote(v) } Self::Delete(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::Delete(v) } Self::Emphasis(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::Emphasis(v) } Self::List(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::List(v) } Self::TableCell(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::TableCell(v) } Self::Strong(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::Strong(v) } Self::LinkRef(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::LinkRef(v) } Self::Heading(mut v) => { - if v.values.get(index).is_some() { - v.values[index] = v.values[index].with_value(value); - } + Self::replace_value_at(&mut v.values, index, value); Self::Heading(v) } Self::MdxJsxFlowElement(mut mdx) => { - if let Some(node) = mdx.children.first() { - mdx.children[index] = node.with_value(value); - } + Self::replace_value_at(&mut mdx.children, index, value); Self::MdxJsxFlowElement(MdxJsxFlowElement { name: mdx.name, @@ -2155,9 +2186,7 @@ impl Node { }) } Self::MdxJsxTextElement(mut mdx) => { - if let Some(node) = mdx.children.first() { - mdx.children[index] = node.with_value(value); - } + Self::replace_value_at(&mut mdx.children, index, value); Self::MdxJsxTextElement(MdxJsxTextElement { name: mdx.name, @@ -2170,9 +2199,7 @@ impl Node { a @ Self::WikiLink(_) => a, #[cfg(feature = "callout")] Self::Callout(mut c) => { - if c.values.get(index).is_some() { - c.values[index] = c.values[index].with_value(value); - } + Self::replace_value_at(&mut c.values, index, value); Self::Callout(c) } #[cfg(feature = "embed")] @@ -3506,15 +3533,15 @@ impl Node { }) } - fn mdx_attribute_content_to_string(attr: MdxAttributeContent) -> SmolStr { + fn mdx_attribute_content_to_string(attr: &MdxAttributeContent) -> SmolStr { match attr { MdxAttributeContent::Expression(value) => format!("{{{}}}", value).into(), - MdxAttributeContent::Property(property) => match property.value { + MdxAttributeContent::Property(property) => match &property.value { Some(value) => match value { MdxAttributeValue::Expression(value) => format!("{}={{{}}}", property.name, value).into(), MdxAttributeValue::Literal(literal) => format!("{}=\"{}\"", property.name, literal).into(), }, - None => property.name, + None => property.name.clone(), }, } } @@ -3602,7 +3629,7 @@ pub(crate) fn render_values_block(values: &[Node], options: &RenderOptions, them .collect::() } -fn values_to_value(values: Vec) -> String { +fn values_to_value(values: &[Node]) -> String { values.iter().map(|value| value.value()).collect::() } @@ -3896,6 +3923,30 @@ mod tests { assert_eq!(mapped.to_string(), "BEFORE"); } + #[test] + fn map_values_into_owned_moves_nodes_through_the_callback() { + let node = Node::Fragment(Fragment { + values: vec![Node::Text(Text { + value: "before".to_string(), + position: None, + })], + }); + + let mapped = node + .map_values_into_owned(&mut |node| -> Result { + Ok(match node { + Node::Text(mut text) => { + text.value.make_ascii_uppercase(); + Node::Text(text) + } + node => node, + }) + }) + .unwrap(); + + assert_eq!(mapped.to_string(), "BEFORE"); + } + #[rstest] #[case::text(Node::Text(Text{value: "".to_string(), position: None}), "test".to_string(), @@ -4022,7 +4073,8 @@ mod tests { "test".to_string(), Node::Math(Math{ value: "test".to_string(), position: None }))] fn test_with_value(#[case] node: Node, #[case] input: String, #[case] expected: Node) { - assert_eq!(node.with_value(input.as_str()), expected); + assert_eq!(node.clone().with_value(input.as_str()), expected); + assert_eq!(node.into_with_value(input.as_str()), expected); } #[rstest] @@ -4218,7 +4270,8 @@ mod tests { position: None }))] fn test_with_children_value(#[case] node: Node, #[case] value: &str, #[case] index: usize, #[case] expected: Node) { - assert_eq!(node.with_children_value(value, index), expected); + assert_eq!(node.clone().with_children_value(value, index), expected); + assert_eq!(node.into_with_children_value(value, index), expected); } #[rstest] @@ -4429,7 +4482,8 @@ mod tests { Node::WikiLink(WikiLink{target: "page".to_string(), text: Some("DISPLAY TEXT".to_string()), position: None}) )] fn test_wikilink_with_value(#[case] node: Node, #[case] value: &str, #[case] expected: Node) { - assert_eq!(node.with_value(value), expected); + assert_eq!(node.clone().with_value(value), expected); + assert_eq!(node.into_with_value(value), expected); } #[cfg(feature = "wikilink")] @@ -5089,7 +5143,8 @@ mod tests { Node::Empty)] #[case(Node::Empty, Node::Empty)] fn test_to_fragment(#[case] node: Node, #[case] expected: Node) { - assert_eq!(node.to_fragment(), expected); + assert_eq!(node.clone().to_fragment(), expected); + assert_eq!(node.into_fragment(), expected); } // Regression coverage for the 0.6.2 blank-line bug: `eval_markdown_node` @@ -6326,7 +6381,8 @@ mod tests { position: None }) )] fn test_callout_with_value(#[case] node: Node, #[case] value: &str, #[case] expected: Node) { - assert_eq!(node.with_value(value), expected); + assert_eq!(node.clone().with_value(value), expected); + assert_eq!(node.into_with_value(value), expected); } #[cfg(feature = "callout")] @@ -6496,7 +6552,8 @@ mod tests { Node::Embed(Embed { target: "note".to_string(), display: Some("800".to_string()), position: None }) )] fn test_embed_with_value(#[case] node: Node, #[case] value: &str, #[case] expected: Node) { - assert_eq!(node.with_value(value), expected); + assert_eq!(node.clone().with_value(value), expected); + assert_eq!(node.into_with_value(value), expected); } #[cfg(feature = "embed")] diff --git a/crates/mq-repl/Cargo.toml b/crates/mq-repl/Cargo.toml index 1e07bf7b3..3630117a6 100644 --- a/crates/mq-repl/Cargo.toml +++ b/crates/mq-repl/Cargo.toml @@ -34,4 +34,3 @@ scopeguard = {workspace = true} [features] clipboard = ["arboard"] default = ["clipboard"] -tarn = ["mq-lang/tarn"] diff --git a/crates/mq-repl/src/command_context.rs b/crates/mq-repl/src/command_context.rs index ca3e71cee..5162bc455 100644 --- a/crates/mq-repl/src/command_context.rs +++ b/crates/mq-repl/src/command_context.rs @@ -401,7 +401,6 @@ impl CommandContext { hir.add_builtin(); let mut engine = mq_lang::DefaultEngine::default(); engine.load_builtin_module(); - #[cfg(feature = "tarn")] engine.enable_query_session(); self.hir = hir; self.source_id = source_id; diff --git a/crates/mq-repl/src/repl.rs b/crates/mq-repl/src/repl.rs index 10d31d4cb..fcbaa802b 100644 --- a/crates/mq-repl/src/repl.rs +++ b/crates/mq-repl/src/repl.rs @@ -350,9 +350,7 @@ impl Repl { } /// Creates a REPL from a pre-configured engine (e.g. with capabilities already set). - #[cfg_attr(not(feature = "tarn"), allow(unused_mut))] pub fn with_engine(mut engine: mq_lang::DefaultEngine, input: Vec) -> Self { - #[cfg(feature = "tarn")] engine.enable_query_session(); Self { @@ -360,33 +358,10 @@ impl Repl { } } - /// Label for the currently compiled execution engine, shown next to the - /// version so it's clear at a glance which binary is running. `None` for - /// the default tree-walking interpreter; the VM is opt-in behind the - /// `tarn` feature and slated for removal, so it's called out explicitly. - fn engine_label() -> Option<&'static str> { - #[cfg(feature = "tarn")] - { - Some("tarn") - } - #[cfg(not(feature = "tarn"))] - { - None - } - } - fn print_welcome() { let version = mq_lang::DefaultEngine::version(); - let engine_suffix = Self::engine_label() - .map(|label| format!(" ({label})")) - .unwrap_or_default(); let lines = [ - format!( - "{} {}{}", - logo_primary("mq").bold(), - text_muted(&format!("v{version}")), - text_muted(&engine_suffix) - ), + format!("{} {}", logo_primary("mq").bold(), text_muted(&format!("v{version}")),), text_muted("Query. Filter. Transform Markdown.").to_string(), format!("Type {} to see available commands.", logo_primary("/help")), ]; diff --git a/crates/mq-run/Cargo.toml b/crates/mq-run/Cargo.toml index ce219772c..31cbe28c2 100644 --- a/crates/mq-run/Cargo.toml +++ b/crates/mq-run/Cargo.toml @@ -15,11 +15,11 @@ default-run = "mq" [features] css-selector = ["mq-lang/css-selector"] debugger = ["mq-lang/debugger", "dep:rustyline", "dep:strum", "dep:regex", "mq-dap", "debug-trace"] -debug-trace = ["debugger", "tarn", "mq-lang/debug-trace"] +debug-trace = ["debugger", "mq-lang/debug-trace"] +vm-profile = ["mq-lang/vm-profile"] default = ["std", "use_mimalloc", "http-import", "css-selector", "watch"] http-import = ["mq-lang/http-import-ureq"] std = [] -tarn = ["mq-lang/tarn", "mq-repl/tarn", "mq-dap/tarn"] tiktoken = ["mq-lang/tiktoken"] use_mimalloc = ["mimalloc"] watch = ["dep:notify"] diff --git a/crates/mq-run/src/cli.rs b/crates/mq-run/src/cli.rs index 8ba7a79cd..feb560df9 100644 --- a/crates/mq-run/src/cli.rs +++ b/crates/mq-run/src/cli.rs @@ -3,9 +3,10 @@ use colored::Colorize; use miette::IntoDiagnostic; use miette::miette; use mq_lang::DefaultEngine; +use mq_lang::DictMap; use mq_lang::Shared; use rayon::prelude::*; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use std::ffi::OsString; use std::fmt::Write as _; use std::io::BufRead; @@ -101,6 +102,11 @@ pub struct Cli { #[arg(long = "dump-stack", default_value_t = false)] dump_stack: bool, + /// Print Tarn VM instruction execution counts to stderr (mq-dbg profile builds only). + #[cfg(feature = "vm-profile")] + #[arg(long = "vm-profile", default_value_t = false)] + vm_profile: bool, + /// Print the Tarn VM bytecode to stderr before execution (mq-dbg `debug-trace` build only). #[cfg(feature = "debug-trace")] #[arg(long = "dump-bytecode", default_value_t = false)] @@ -1682,7 +1688,7 @@ impl Cli { || self.input.argjson.is_some() || self.input.slurp_file.is_some() { - let mut named: BTreeMap = BTreeMap::new(); + let mut named: DictMap = DictMap::default(); if let Some(args) = &self.input.args { for v in args.chunks(2) { engine.define_string_value(&v[0], &v[1]); @@ -1730,7 +1736,7 @@ impl Cli { .iter() .map(|s| mq_lang::RuntimeValue::String(Shared::new(s.clone()))) .collect(); - let args_map: BTreeMap = [ + let args_map: DictMap = [ ( mq_lang::Ident::new("positional"), mq_lang::RuntimeValue::Array(Shared::new(positional)), @@ -2020,6 +2026,9 @@ impl Cli { let is_grep = matches!(self.resolved_output_format(), OutputFormat::Grep); let grep_input: Option> = is_grep.then(|| input.clone()); + #[cfg(feature = "vm-profile")] + let vm_profile = self.vm_profile.then(mq_lang::vm_profile::VmProfileScope::start); + let runtime_values = if self.output.update { #[cfg(feature = "debug-trace")] let results = engine @@ -2041,6 +2050,9 @@ impl Cli { } }; + #[cfg(feature = "vm-profile")] + self.emit_vm_profile(vm_profile, file); + if self.output.update && self.output.diff { return self.emit_diff(&runtime_values, file, content); } @@ -2307,10 +2319,33 @@ impl Cli { // Keep --append sequential: parallel files racing the same read-then-rename // append could clobber each other. if files.len() > self.parallel_threshold && !self.output.append { - files.par_iter().try_for_each(|(file, content)| { - let mut engine = self.create_engine()?; - self.execute(&mut engine, &query, file, content) - })?; + // `CompiledProgram` uses `Rc`; compile once per Rayon worker rather than sharing it. + let can_compile_per_worker = self.all_files_same_prefix(&files) && self.output.separator.is_none(); + #[cfg(feature = "debug-trace")] + // Preserve per-file bytecode diagnostics. + let can_compile_per_worker = can_compile_per_worker && !self.dump_bytecode; + + if can_compile_per_worker { + let effective_query = self.effective_query(&query, &files[0].0); + files.par_iter().try_for_each_init( + || { + let mut engine = self.create_engine()?; + let program = engine.compile(&effective_query).map_err(|error| *error)?; + Ok::<_, miette::Error>((engine, program)) + }, + |prepared, (file, content)| { + let (engine, program) = prepared + .as_mut() + .map_err(|error| miette!("Failed to prepare parallel query worker: {error}"))?; + self.execute_compiled(engine, program, file, content) + }, + )?; + } else { + files.par_iter().try_for_each(|(file, content)| { + let mut engine = self.create_engine()?; + self.execute(&mut engine, &query, file, content) + })?; + } } else { let mut engine = self.create_engine()?; @@ -2351,6 +2386,8 @@ impl Cli { let is_grep = matches!(self.resolved_output_format(), OutputFormat::Grep); let grep_input: Option> = is_grep.then(|| combined_input.clone()); + #[cfg(feature = "vm-profile")] + let vm_profile = self.vm_profile.then(mq_lang::vm_profile::VmProfileScope::start); #[cfg(feature = "debug-trace")] let program = engine.compile(&effective_query).map_err(|error| *error)?; #[cfg(feature = "debug-trace")] @@ -2364,6 +2401,9 @@ impl Cli { .eval(&effective_query, combined_input.into_iter()) .map_err(|error| *error)?; + #[cfg(feature = "vm-profile")] + self.emit_vm_profile(vm_profile, &None); + self.emit_results(runtime_values, grep_input, &None) } @@ -2393,6 +2433,9 @@ impl Cli { let is_grep = matches!(self.resolved_output_format(), OutputFormat::Grep); let grep_input: Option> = is_grep.then(|| input.clone()); + #[cfg(feature = "vm-profile")] + let vm_profile = self.vm_profile.then(mq_lang::vm_profile::VmProfileScope::start); + let runtime_values = if self.output.update { let results = engine .eval_compiled(program, input.clone().into_iter()) @@ -2402,6 +2445,9 @@ impl Cli { engine.eval_compiled(program, input.into_iter()).map_err(|e| *e)? }; + #[cfg(feature = "vm-profile")] + self.emit_vm_profile(vm_profile, file); + if self.output.update && self.output.diff { return self.emit_diff(&runtime_values, file, content); } @@ -2427,6 +2473,8 @@ impl Cli { if let Some(f) = file { self.set_file_vars(engine, f); } + #[cfg(feature = "vm-profile")] + let vm_profile = self.vm_profile.then(mq_lang::vm_profile::VmProfileScope::start); #[cfg(feature = "debug-trace")] let program = engine.compile(query).map_err(|error| *error)?; #[cfg(feature = "debug-trace")] @@ -2438,9 +2486,22 @@ impl Cli { .map_err(|error| *error)?; #[cfg(not(feature = "debug-trace"))] let runtime_values = engine.eval(query, input.into_iter()).map_err(|error| *error)?; + #[cfg(feature = "vm-profile")] + self.emit_vm_profile(vm_profile, file); Ok(self.output.paginate(runtime_values.compact()).len()) } + #[cfg(feature = "vm-profile")] + fn emit_vm_profile(&self, scope: Option, file: &Option) { + let Some(scope) = scope else { + return; + }; + let target = file + .as_ref() + .map_or_else(|| "stdin".to_string(), |path| path.display().to_string()); + eprintln!("Tarn VM profile ({target})\n{}", scope.finish()); + } + fn process_batch_count(&self, query: &str, files: &[(Option, ContentData)]) -> miette::Result<()> { let multiple_files = files.len() > 1; let mut total = 0usize; @@ -2700,7 +2761,7 @@ impl Cli { } /// Returns `true` if the dict is a known expandable typed dict (has `type: :symbol`). - fn is_typed_dict(map: &std::collections::BTreeMap) -> bool { + fn is_typed_dict(map: &DictMap) -> bool { let type_key = mq_lang::Ident::new("type"); matches!( map.get(&type_key), @@ -2712,9 +2773,7 @@ impl Cli { /// /// Returns `None` if the dict is not a known expandable type. /// To add support for a new type, add a match arm for the type name. - fn expand_typed_dict( - map: &std::collections::BTreeMap, - ) -> Option> { + fn expand_typed_dict(map: &DictMap) -> Option> { let type_key = mq_lang::Ident::new("type"); match map.get(&type_key) { Some(mq_lang::RuntimeValue::Symbol(s)) => match s.as_str().as_str() { @@ -6030,9 +6089,7 @@ mod tests { assert!(cli.run().is_ok()); let result = fs::read_to_string(&output_file).expect("Failed to read output"); - // `named` is backed by a `BTreeMap`, whose key order depends on the - // global string interner's symbol assignment order rather than the key text, - // so compare parsed JSON values instead of the raw serialized string. + // Compare parsed JSON values (rather than the raw string) so key order doesn't matter. let actual: serde_json::Value = serde_json::from_str(result.trim()).expect("output should be valid JSON"); let expected: serde_json::Value = serde_json::from_str(r#"{"count": 42, "name": "Alice"}"#).unwrap(); assert_eq!(actual, expected); diff --git a/crates/mq-run/src/debugger.rs b/crates/mq-run/src/debugger.rs index 85d2befa8..6676e0e5b 100644 --- a/crates/mq-run/src/debugger.rs +++ b/crates/mq-run/src/debugger.rs @@ -241,7 +241,7 @@ impl DebuggerHandler { .filter_map(|frame| { let range = self.engine.token_arena().read().unwrap()[frame.token_id].range; - match &*frame.expr { + match &frame.expr { mq_lang::AstExpr::Call(ident, _) => Some(format!( "{} at {}:{}", ident, @@ -290,7 +290,6 @@ impl DebuggerHandler { let value: mq_lang::RuntimeValue = context.current_value.clone(); let mut engine = self.engine.clone(); - #[cfg(feature = "tarn")] let values = match engine.eval_debug_expression(&expr, value, &context.vm_bindings()) { Ok(v) => v, Err(e) => { @@ -298,14 +297,6 @@ impl DebuggerHandler { continue; } }; - #[cfg(not(feature = "tarn"))] - let values = match engine.eval_debug_expression(&expr, value, &context.env) { - Ok(v) => v, - Err(e) => { - eprintln!("Error evaluating expression: {}", e); - continue; - } - }; let lines = values .values() diff --git a/crates/mq-run/src/grep.rs b/crates/mq-run/src/grep.rs index a836e8a8c..0ef59aa79 100644 --- a/crates/mq-run/src/grep.rs +++ b/crates/mq-run/src/grep.rs @@ -197,8 +197,8 @@ fn flatten(value: &mq_lang::RuntimeValue) -> Vec<(String, mq_lang::RuntimeValue) #[cfg(test)] mod tests { use super::*; + use mq_lang::DictMap; use rstest::rstest; - use std::collections::BTreeMap; #[rstest] #[case::with_filename_and_line(Some("file.md".to_string()), Some(5), "## Heading", ":", "file.md:5:## Heading\n")] @@ -262,7 +262,7 @@ mod tests { #[test] fn test_flatten_flat_dict() { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert( mq_lang::Ident::new("key"), mq_lang::RuntimeValue::String(Shared::new("val".to_string())), @@ -278,12 +278,12 @@ mod tests { #[test] fn test_flatten_nested_dict() { - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert( mq_lang::Ident::new("b"), mq_lang::RuntimeValue::String(Shared::new("deep".to_string())), ); - let mut outer = BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert( mq_lang::Ident::new("a"), mq_lang::RuntimeValue::Dict(Shared::new(inner)), @@ -300,7 +300,7 @@ mod tests { #[test] fn test_flatten_dict_with_array() { // dict["key"][0] → "key[0]" - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert( mq_lang::Ident::new("key"), mq_lang::RuntimeValue::Array(Shared::new(vec![mq_lang::RuntimeValue::String(Shared::new( @@ -319,7 +319,7 @@ mod tests { #[test] fn test_flatten_array_with_dict() { // [0].key → "[0].key" - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert( mq_lang::Ident::new("b"), mq_lang::RuntimeValue::String(Shared::new("val".to_string())), @@ -361,7 +361,7 @@ mod tests { #[test] fn test_to_nodes_dict() { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert( mq_lang::Ident::new("key"), mq_lang::RuntimeValue::String(Shared::new("val".to_string())), diff --git a/crates/mq-run/src/lib.rs b/crates/mq-run/src/lib.rs index bcd885579..f5fb29514 100644 --- a/crates/mq-run/src/lib.rs +++ b/crates/mq-run/src/lib.rs @@ -11,6 +11,7 @@ //! - Multiple output formats //! - Optional debugger integration (with `debugger` feature) //! - Optional Tarn VM operand-stack tracing (with `debug-trace` feature) +//! - Optional Tarn VM instruction-count profiling (with `vm-profile` feature) //! - Configuration file support //! - Interactive REPL mode //! diff --git a/crates/mq-run/src/output/csv.rs b/crates/mq-run/src/output/csv.rs index d0610bea4..1363880d9 100644 --- a/crates/mq-run/src/output/csv.rs +++ b/crates/mq-run/src/output/csv.rs @@ -89,7 +89,7 @@ mod tests { #[test] fn test_array_of_dicts() { - let mut m1 = std::collections::BTreeMap::new(); + let mut m1 = mq_lang::DictMap::default(); m1.insert( mq_lang::Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), @@ -98,7 +98,7 @@ mod tests { mq_lang::Ident::new("age"), RuntimeValue::String(Shared::new("30".to_string())), ); - let mut m2 = std::collections::BTreeMap::new(); + let mut m2 = mq_lang::DictMap::default(); m2.insert( mq_lang::Ident::new("name"), RuntimeValue::String(Shared::new("Bob".to_string())), @@ -117,7 +117,7 @@ mod tests { #[test] fn test_single_dict() { - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string())), @@ -129,7 +129,7 @@ mod tests { #[test] fn test_needs_quoting() { - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("has,comma".to_string())), @@ -151,7 +151,7 @@ mod tests { #[test] fn test_missing_key_is_empty() { - let mut m1 = std::collections::BTreeMap::new(); + let mut m1 = mq_lang::DictMap::default(); m1.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string())), @@ -160,7 +160,7 @@ mod tests { mq_lang::Ident::new("b"), RuntimeValue::String(Shared::new("2".to_string())), ); - let mut m2 = std::collections::BTreeMap::new(); + let mut m2 = mq_lang::DictMap::default(); m2.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("3".to_string())), diff --git a/crates/mq-run/src/output/gron.rs b/crates/mq-run/src/output/gron.rs index b56e0c938..df34c367f 100644 --- a/crates/mq-run/src/output/gron.rs +++ b/crates/mq-run/src/output/gron.rs @@ -65,9 +65,9 @@ fn is_bare_ident(s: &str) -> bool { #[cfg(test)] mod tests { use super::*; + use mq_lang::DictMap; use mq_lang::Shared; use rstest::rstest; - use std::collections::BTreeMap; #[rstest] #[case("json", "foo", "json.foo")] @@ -99,12 +99,12 @@ mod tests { #[test] fn test_gron_nested_dict() { - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert( mq_lang::Ident::new("b"), mq_lang::RuntimeValue::String(Shared::new("deep".to_string())), ); - let mut outer = BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert( mq_lang::Ident::new("a"), mq_lang::RuntimeValue::Dict(Shared::new(inner)), @@ -118,7 +118,7 @@ mod tests { #[test] fn test_gron_dict_key_needing_brackets() { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert( mq_lang::Ident::new("weird key"), mq_lang::RuntimeValue::String(Shared::new("v".to_string())), diff --git a/crates/mq-run/src/output/json.rs b/crates/mq-run/src/output/json.rs index 483cfab43..6e506a244 100644 --- a/crates/mq-run/src/output/json.rs +++ b/crates/mq-run/src/output/json.rs @@ -241,7 +241,7 @@ mod tests { #[test] fn test_colorize_object_empty() { let theme = plain_theme(); - let values = vec![RuntimeValue::Dict(Shared::new(std::collections::BTreeMap::new()))]; + let values = vec![RuntimeValue::Dict(Shared::new(mq_lang::DictMap::default()))]; let result = runtime_values_to_json(&values, Some(&theme), false, " ").unwrap(); assert_eq!(result, "{}"); } @@ -249,7 +249,7 @@ mod tests { #[test] fn test_colorize_object_non_empty() { let theme = plain_theme(); - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert( mq_lang::Ident::new("key"), RuntimeValue::String(Shared::new("val".to_string())), @@ -261,7 +261,7 @@ mod tests { #[test] fn test_compact_no_theme() { - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert(mq_lang::Ident::new("a"), RuntimeValue::from(1usize)); map.insert(mq_lang::Ident::new("b"), RuntimeValue::from(2usize)); let values = vec![RuntimeValue::Dict(Shared::new(map))]; @@ -272,7 +272,7 @@ mod tests { #[test] fn test_compact_with_theme() { let theme = plain_theme(); - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert(mq_lang::Ident::new("a"), RuntimeValue::from(1usize)); map.insert(mq_lang::Ident::new("b"), RuntimeValue::from(2usize)); let values = vec![RuntimeValue::Dict(Shared::new(map))]; diff --git a/crates/mq-run/src/output/shell.rs b/crates/mq-run/src/output/shell.rs index a74bd5fe5..a9dbd7045 100644 --- a/crates/mq-run/src/output/shell.rs +++ b/crates/mq-run/src/output/shell.rs @@ -88,9 +88,9 @@ fn is_safe_unquoted_char(c: char) -> bool { #[cfg(test)] mod tests { use super::*; + use mq_lang::DictMap; use mq_lang::Shared; use rstest::rstest; - use std::collections::BTreeMap; #[rstest] #[case("", "foo", "foo")] @@ -128,7 +128,7 @@ mod tests { #[case(vec![mq_lang::RuntimeValue::Number(3i64.into())], "value=3\n")] #[case(vec![mq_lang::RuntimeValue::None], "value=\n")] #[case(vec![mq_lang::RuntimeValue::Array(Shared::new(vec![]))], "")] - #[case(vec![mq_lang::RuntimeValue::Dict(Shared::new(BTreeMap::new()))], "")] + #[case(vec![mq_lang::RuntimeValue::Dict(Shared::new(DictMap::default()))], "")] #[case( vec![mq_lang::RuntimeValue::Array(Shared::new(vec![ mq_lang::RuntimeValue::String(Shared::new("x".to_string())), @@ -142,12 +142,12 @@ mod tests { #[test] fn test_shell_nested_dict() { - let mut inner = BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert( mq_lang::Ident::new("color"), mq_lang::RuntimeValue::String(Shared::new("turquoise".to_string())), ); - let mut outer = BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert( mq_lang::Ident::new("eyes"), mq_lang::RuntimeValue::Dict(Shared::new(inner)), @@ -158,7 +158,7 @@ mod tests { #[test] fn test_shell_dict_key_needing_sanitization() { - let mut m = BTreeMap::new(); + let mut m = DictMap::default(); m.insert( mq_lang::Ident::new("weird key!"), mq_lang::RuntimeValue::String(Shared::new("v".to_string())), @@ -169,12 +169,12 @@ mod tests { #[test] fn test_shell_array_under_dict_key_has_no_double_underscore() { - let mut friend = BTreeMap::new(); + let mut friend = DictMap::default(); friend.insert( mq_lang::Ident::new("name"), mq_lang::RuntimeValue::String(Shared::new("James P. Sullivan".to_string())), ); - let mut outer = BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert( mq_lang::Ident::new("friends"), mq_lang::RuntimeValue::Array(Shared::new(vec![mq_lang::RuntimeValue::Dict(Shared::new(friend))])), diff --git a/crates/mq-run/src/output/table.rs b/crates/mq-run/src/output/table.rs index 4e16dca45..c0b1950d9 100644 --- a/crates/mq-run/src/output/table.rs +++ b/crates/mq-run/src/output/table.rs @@ -6,10 +6,11 @@ //! single `Array` is automatically expanded so each element becomes its own row. //! Markdown nodes with children are displayed with a nested children table. +use mq_lang::DictMap; use mq_lang::RuntimeValue; use mq_lang::Shared; use mq_markdown::ColorTheme; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::BTreeSet; use tabled::Table; use tabled::builder::Builder; use tabled::settings::location::Locator; @@ -82,15 +83,15 @@ pub(crate) fn runtime_values_to_table<'a>(runtime_values: &[RuntimeValue], theme rows.push(vec!["children".to_string(), children_str]); } if let Some(pos) = node.position() { - let mut start_map = BTreeMap::new(); + let mut start_map = DictMap::default(); start_map.insert(mq_lang::Ident::new("line"), pos.start.line.to_string().into()); start_map.insert(mq_lang::Ident::new("column"), pos.start.column.to_string().into()); - let mut end_map = BTreeMap::new(); + let mut end_map = DictMap::default(); end_map.insert(mq_lang::Ident::new("line"), pos.end.line.to_string().into()); end_map.insert(mq_lang::Ident::new("column"), pos.end.column.to_string().into()); - let mut pos_map = BTreeMap::new(); + let mut pos_map = DictMap::default(); pos_map.insert(mq_lang::Ident::new("start"), RuntimeValue::Dict(Shared::new(start_map))); pos_map.insert(mq_lang::Ident::new("end"), RuntimeValue::Dict(Shared::new(end_map))); let pos_str = format_cell_value(&RuntimeValue::Dict(Shared::new(pos_map)), theme); @@ -279,7 +280,7 @@ mod tests { #[test] fn test_table_dict_values() { - let mut map = std::collections::BTreeMap::new(); + let mut map = DictMap::default(); map.insert( mq_lang::Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), @@ -298,12 +299,12 @@ mod tests { #[test] fn test_table_multiple_dicts() { let make_dict = |name: &str, val: &str| { - let mut map = std::collections::BTreeMap::new(); + let mut map = DictMap::default(); map.insert( mq_lang::Ident::new("key"), RuntimeValue::String(Shared::new(val.to_string())), ); - let mut outer = std::collections::BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert( mq_lang::Ident::new("name"), RuntimeValue::String(Shared::new(name.to_string())), @@ -318,7 +319,7 @@ mod tests { #[test] fn test_table_dict_with_theme() { - let mut map = std::collections::BTreeMap::new(); + let mut map = DictMap::default(); map.insert(mq_lang::Ident::new("x"), RuntimeValue::Boolean(true)); map.insert(mq_lang::Ident::new("y"), RuntimeValue::Boolean(false)); let values = vec![RuntimeValue::Dict(Shared::new(map))]; @@ -330,12 +331,12 @@ mod tests { #[test] fn test_table_array_of_dicts() { - let mut m1 = std::collections::BTreeMap::new(); + let mut m1 = DictMap::default(); m1.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string())), ); - let mut m2 = std::collections::BTreeMap::new(); + let mut m2 = DictMap::default(); m2.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("2".to_string())), @@ -362,12 +363,12 @@ mod tests { #[test] fn test_table_nested_dict_in_cell() { - let mut inner = std::collections::BTreeMap::new(); + let mut inner = DictMap::default(); inner.insert( mq_lang::Ident::new("sub"), RuntimeValue::String(Shared::new("val".to_string())), ); - let mut outer = std::collections::BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert(mq_lang::Ident::new("nested"), RuntimeValue::Dict(Shared::new(inner))); let values = vec![RuntimeValue::Dict(Shared::new(outer))]; let table = runtime_values_to_table(&values, None); @@ -388,7 +389,7 @@ mod tests { #[test] fn test_table_empty_array_in_cell() { - let mut map = std::collections::BTreeMap::new(); + let mut map = DictMap::default(); map.insert(mq_lang::Ident::new("arr"), RuntimeValue::Array(Shared::new(vec![]))); let values = vec![RuntimeValue::Dict(Shared::new(map))]; let table = runtime_values_to_table(&values, None); @@ -472,17 +473,17 @@ mod tests { #[test] fn test_table_dict_with_nested_array_of_dicts() { - let mut inner1 = std::collections::BTreeMap::new(); + let mut inner1 = DictMap::default(); inner1.insert( mq_lang::Ident::new("k"), RuntimeValue::String(Shared::new("v1".to_string())), ); - let mut inner2 = std::collections::BTreeMap::new(); + let mut inner2 = DictMap::default(); inner2.insert( mq_lang::Ident::new("k"), RuntimeValue::String(Shared::new("v2".to_string())), ); - let mut outer = std::collections::BTreeMap::new(); + let mut outer = DictMap::default(); outer.insert( mq_lang::Ident::new("items"), RuntimeValue::Array(Shared::new(vec![ @@ -502,7 +503,7 @@ mod tests { value: "node_value".to_string(), position: None, }); - let mut map = std::collections::BTreeMap::new(); + let mut map = DictMap::default(); map.insert( mq_lang::Ident::new("md"), RuntimeValue::Markdown(Shared::new(node), None), @@ -515,7 +516,7 @@ mod tests { #[test] fn test_table_empty_dict() { - let map = std::collections::BTreeMap::new(); + let map = DictMap::default(); let values = vec![RuntimeValue::Dict(Shared::new(map))]; let table = runtime_values_to_table(&values, None); assert!(!table.to_string().is_empty()); @@ -523,7 +524,7 @@ mod tests { #[test] fn test_table_multiple_dicts_missing_key() { - let mut m1 = std::collections::BTreeMap::new(); + let mut m1 = DictMap::default(); m1.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string())), @@ -532,7 +533,7 @@ mod tests { mq_lang::Ident::new("b"), RuntimeValue::String(Shared::new("2".to_string())), ); - let mut m2 = std::collections::BTreeMap::new(); + let mut m2 = DictMap::default(); m2.insert( mq_lang::Ident::new("a"), RuntimeValue::String(Shared::new("3".to_string())), diff --git a/crates/mq-run/src/output/toml.rs b/crates/mq-run/src/output/toml.rs index 52f4b7fe3..921e54a26 100644 --- a/crates/mq-run/src/output/toml.rs +++ b/crates/mq-run/src/output/toml.rs @@ -31,7 +31,7 @@ mod tests { #[test] fn test_dict_value() { - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert( mq_lang::Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), @@ -43,12 +43,12 @@ mod tests { #[test] fn test_nested_dict() { - let mut inner = std::collections::BTreeMap::new(); + let mut inner = mq_lang::DictMap::default(); inner.insert( mq_lang::Ident::new("city"), RuntimeValue::String(Shared::new("NYC".to_string())), ); - let mut outer = std::collections::BTreeMap::new(); + let mut outer = mq_lang::DictMap::default(); outer.insert(mq_lang::Ident::new("address"), RuntimeValue::Dict(Shared::new(inner))); let values = vec![RuntimeValue::Dict(Shared::new(outer))]; let result = runtime_values_to_toml(&values).unwrap(); diff --git a/crates/mq-run/src/output/toon.rs b/crates/mq-run/src/output/toon.rs index 510bfea59..a0f5a310d 100644 --- a/crates/mq-run/src/output/toon.rs +++ b/crates/mq-run/src/output/toon.rs @@ -16,12 +16,12 @@ pub(crate) fn runtime_values_to_toon(runtime_values: &[mq_lang::RuntimeValue]) - #[cfg(test)] mod tests { use super::*; + use mq_lang::DictMap; use mq_lang::{Ident, RuntimeValue, Shared}; use rstest::rstest; - use std::collections::BTreeMap; fn single_key_dict(key: &str, value: RuntimeValue) -> RuntimeValue { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new(key), value); RuntimeValue::Dict(Shared::new(map)) } @@ -45,7 +45,7 @@ mod tests { "[2]: a,b" )] #[case::empty_array(RuntimeValue::Array(Shared::new(vec![])), "[0]:")] - #[case::empty_dict(RuntimeValue::Dict(Shared::new(BTreeMap::new())), "")] + #[case::empty_dict(RuntimeValue::Dict(Shared::new(DictMap::default())), "")] #[case::empty_string_needs_quoting(RuntimeValue::String(Shared::new("".to_string())), "\"\"")] #[case::numeric_like_string_needs_quoting(RuntimeValue::String(Shared::new("123".to_string())), "\"123\"")] #[case::keyword_like_string_needs_quoting(RuntimeValue::String(Shared::new("true".to_string())), "\"true\"")] @@ -58,7 +58,7 @@ mod tests { } fn tabular_row(id: usize, name: &str) -> RuntimeValue { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("id"), RuntimeValue::from(id)); map.insert(Ident::new("name"), RuntimeValue::String(Shared::new(name.to_string()))); RuntimeValue::Dict(Shared::new(map)) diff --git a/crates/mq-run/src/output/xml.rs b/crates/mq-run/src/output/xml.rs index 8d71e33f2..b85dc2506 100644 --- a/crates/mq-run/src/output/xml.rs +++ b/crates/mq-run/src/output/xml.rs @@ -8,25 +8,25 @@ //! arrays becoming repeated `` elements. use miette::miette; +use mq_lang::DictMap; #[cfg(test)] use mq_lang::Shared; use mq_lang::{Ident, RuntimeValue}; use quick_xml::Writer; use quick_xml::escape::escape; use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event}; -use std::collections::BTreeMap; fn xml_err(e: std::io::Error) -> miette::Report { miette!("Failed to write XML: {}", e) } -fn is_element_shape(map: &BTreeMap) -> bool { +fn is_element_shape(map: &DictMap) -> bool { matches!(map.get(&Ident::new("tag")), Some(RuntimeValue::String(_))) } /// Writes a `{tag, attributes, children, text}`-shaped dict (the shape produced by /// `xml_parse()`) as a real XML element, recursing into `children` of the same shape. -fn write_element(writer: &mut Writer<&mut Vec>, map: &BTreeMap) -> std::io::Result<()> { +fn write_element(writer: &mut Writer<&mut Vec>, map: &DictMap) -> std::io::Result<()> { let tag = match map.get(&Ident::new("tag")) { Some(RuntimeValue::String(s)) => s.clone(), _ => return Ok(()), @@ -143,9 +143,9 @@ mod tests { #[test] fn test_element_shape_round_trip() { - let mut attrs = BTreeMap::new(); + let mut attrs = DictMap::default(); attrs.insert(Ident::new("id"), RuntimeValue::String(Shared::new("1".to_string()))); - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("tag"), RuntimeValue::String(Shared::new("root".to_string()))); map.insert(Ident::new("attributes"), RuntimeValue::Dict(Shared::new(attrs))); map.insert(Ident::new("children"), RuntimeValue::Array(Shared::new(vec![]))); @@ -161,7 +161,7 @@ mod tests { #[test] fn test_generic_dict() { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert( Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), @@ -192,14 +192,14 @@ mod tests { #[test] fn test_empty_dict() { - let values = vec![RuntimeValue::Dict(Shared::new(BTreeMap::new()))]; + let values = vec![RuntimeValue::Dict(Shared::new(DictMap::default()))]; let result = runtime_values_to_xml(&values, b' ', 2).unwrap(); assert!(result.contains("")); } #[test] fn test_custom_indent_width() { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert( Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), @@ -211,7 +211,7 @@ mod tests { #[test] fn test_tab_indent() { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert( Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), diff --git a/crates/mq-run/src/output/yaml.rs b/crates/mq-run/src/output/yaml.rs index f1c9cefc3..e10190db3 100644 --- a/crates/mq-run/src/output/yaml.rs +++ b/crates/mq-run/src/output/yaml.rs @@ -61,7 +61,7 @@ mod tests { #[test] fn test_dict_value() { - let mut map = std::collections::BTreeMap::new(); + let mut map = mq_lang::DictMap::default(); map.insert( mq_lang::Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())), diff --git a/crates/mq-run/tests/cookbook_tests.rs b/crates/mq-run/tests/cookbook_tests.rs index 859df6cb1..e162607c0 100644 --- a/crates/mq-run/tests/cookbook_tests.rs +++ b/crates/mq-run/tests/cookbook_tests.rs @@ -1,6 +1,5 @@ //! Runs the query/input/output examples from `docs/books/src/cookbook/*.md` through the -//! built `mq` binary, so the docs stay honest under both the tree-walking evaluator and the -//! `tarn` bytecode VM (this file runs under both via `just test-all`'s `--all-features` step). +//! built `mq` binary, so the docs stay honest against the Tarn bytecode VM. use assert_cmd::cargo; use base64::Engine as _; diff --git a/crates/mq-run/tests/integration_tests.rs b/crates/mq-run/tests/integration_tests.rs index 9f6f501e2..3a6cdb94a 100644 --- a/crates/mq-run/tests/integration_tests.rs +++ b/crates/mq-run/tests/integration_tests.rs @@ -394,7 +394,7 @@ In {year}, the snowfall was above average. #[case::input_format_xml( vec!["--unbuffered", "-I", "xml", "self"], "text", - Some("{\"text\": \"text\", \"attributes\": {}, \"tag\": \"root\", \"children\": []}\n") + Some("{\"tag\": \"root\", \"attributes\": {}, \"children\": [], \"text\": \"text\"}\n") )] #[case::output_format_json_compact( vec!["--unbuffered", "-I", "json", "-F", "json", "--compact", "self"], @@ -1515,6 +1515,45 @@ fn test_help_documents_quiet() { assert!(output.contains("-q, --quiet"), "help was:\n{output}"); } +#[test] +fn test_parallel_batch_preserves_file_scoped_globals_with_compiled_workers() -> Result<(), Box> { + let files: Vec = (0..11) + .map(|i| create_file(&format!("test_parallel_compiled_worker_{i}.txt"), "input\n").1) + .collect(); + let files_clone = files.clone(); + + defer! { + for file in &files_clone { + if file.exists() { + std::fs::remove_file(file).expect("Failed to delete temp file"); + } + } + } + + let mut cmd = cargo::cargo_bin_cmd!("mq"); + let assert = cmd + .arg("--unbuffered") + .arg("-P") + .arg("0") + .arg("-I") + .arg("text") + .arg("__FILE_NAME__") + .args(&files) + .assert() + .success(); + let output = String::from_utf8(assert.get_output().stdout.clone())?; + + for file in &files { + let name = file.file_name().and_then(|name| name.to_str()).unwrap(); + assert!( + output.lines().any(|line| line == name), + "missing {name:?} in {output:?}" + ); + } + + Ok(()) +} + #[rstest] #[case::two_matches("# h1\n\n## h2a\n\n## h2b\n", ".h2", "2\n")] #[case::no_matches("# h1\n\nbody\n", ".h2", "0\n")] diff --git a/crates/mq-test/Cargo.toml b/crates/mq-test/Cargo.toml index 659fd2ef8..f9e5702be 100644 --- a/crates/mq-test/Cargo.toml +++ b/crates/mq-test/Cargo.toml @@ -10,10 +10,6 @@ name = "mq-test" repository.workspace = true version.workspace = true -[features] -# Runs the test runner's queries through mq-lang's Tarn bytecode VM. -tarn = ["mq-lang/tarn"] - [dependencies] clap = {workspace = true, features = ["derive"]} glob = {workspace = true} diff --git a/crates/mq-test/src/snapshot.rs b/crates/mq-test/src/snapshot.rs index 89c0c3ebf..f9bbe6a9f 100644 --- a/crates/mq-test/src/snapshot.rs +++ b/crates/mq-test/src/snapshot.rs @@ -1,8 +1,7 @@ -use std::collections::BTreeMap; use std::fs; use std::path::{Path, PathBuf}; -use mq_lang::{Ident, RuntimeValue, Shared}; +use mq_lang::{DictMap, Ident, RuntimeValue, Shared}; use similar::{ChangeTag, TextDiff}; use crate::html; @@ -94,7 +93,7 @@ pub(crate) fn check_snapshot(test_file: &Path, name: &str, actual: &str, update: } fn fail(message: String) -> RuntimeValue { - let mut map = BTreeMap::new(); + let mut map = DictMap::default(); map.insert(Ident::new("error"), RuntimeValue::Boolean(true)); map.insert(Ident::new("message"), RuntimeValue::String(message.into())); RuntimeValue::Dict(Shared::new(map)) diff --git a/crates/mq-wasm/src/script.rs b/crates/mq-wasm/src/script.rs index 2bb2a6fde..f3dd84182 100644 --- a/crates/mq-wasm/src/script.rs +++ b/crates/mq-wasm/src/script.rs @@ -1203,7 +1203,7 @@ fn extract_local_import_names(code: &str) -> Vec { program .iter() .filter_map(|node| { - let path = match &*node.expr { + let path = match &node.expr { mq_lang::AstExpr::Import(mq_lang::AstLiteral::String(p), _) => p, mq_lang::AstExpr::Include(mq_lang::AstLiteral::String(p)) => p, _ => return None, @@ -1227,7 +1227,7 @@ fn extract_http_import_urls(code: &str) -> Vec { program .iter() .filter_map(|node| { - let url = match &*node.expr { + let url = match &node.expr { mq_lang::AstExpr::Import(mq_lang::AstLiteral::String(url), _) => url, mq_lang::AstExpr::Include(mq_lang::AstLiteral::String(url)) => url, _ => return None, diff --git a/crates/mq-web-api/Cargo.toml b/crates/mq-web-api/Cargo.toml index 853f233b4..95fc37c7b 100644 --- a/crates/mq-web-api/Cargo.toml +++ b/crates/mq-web-api/Cargo.toml @@ -15,7 +15,6 @@ version.workspace = true default = ["use_mimalloc"] use_mimalloc = ["mimalloc"] otel = ["opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "tracing-opentelemetry"] -tarn = ["mq-lang/tarn"] [dependencies] axum = {workspace = true} diff --git a/crates/mq-web-api/src/api.rs b/crates/mq-web-api/src/api.rs index 8c209f8c3..761c7ed2a 100644 --- a/crates/mq-web-api/src/api.rs +++ b/crates/mq-web-api/src/api.rs @@ -585,7 +585,7 @@ fn collect_markdown_nodes(value: &mq_lang::RuntimeValue, nodes: &mut Vec) -> bool { +fn is_typed_dict(map: &mq_lang::DictMap) -> bool { let type_key = mq_lang::Ident::new("type"); matches!( map.get(&type_key), @@ -593,9 +593,7 @@ fn is_typed_dict(map: &std::collections::BTreeMap, -) -> Option> { +fn expand_typed_dict(map: &mq_lang::DictMap) -> Option> { let type_key = mq_lang::Ident::new("type"); match map.get(&type_key) { Some(mq_lang::RuntimeValue::Symbol(s)) => match s.as_str().as_str() { diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index a91c743e8..c1c70810f 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -8,12 +8,6 @@ version = "0.5.5" [package.metadata] cargo-fuzz = true -[features] -# Routes fuzzed scripts through `mq-lang`'s `tarn` bytecode VM instead of its tree-walking -# evaluator. Required by the `tarn` fuzz target; also usable with `interpreter` to fuzz the -# VM under that target's existing corpus. -tarn = ["mq-lang/tarn"] - [dependencies] arbitrary = { workspace = true } itertools = { workspace = true } @@ -29,24 +23,3 @@ doc = false name = "interpreter" path = "fuzz_targets/interpreter.rs" test = false - -[[bin]] -bench = false -doc = false -name = "tarn" -path = "fuzz_targets/tarn.rs" -test = false - -[[bin]] -bench = false -doc = false -name = "differential" -path = "fuzz_targets/differential.rs" -test = false - -[[bin]] -bench = false -doc = false -name = "differential-runner" -path = "src/bin/differential_runner.rs" -test = false diff --git a/fuzz/README.md b/fuzz/README.md index 0e2cc950e..e8faa7966 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -1,25 +1,7 @@ # Fuzzing mq -Run the tree-walking evaluator and Tarn VM independently: +Run the bytecode VM fuzz target: ```sh just test-fuzz -just test-fuzz-tarn ``` - -To compare both engines for the same deterministic programs, run: - -```sh -just test-fuzz-differential 1000 -``` - -The differential target builds a tree-walker helper and a Tarn helper separately, then compares -their successful values (or whether both reject a program). Its intentionally small language -subset covers expressions, conditionals, closures, `foreach`, and destructuring without treating -implementation-specific features or nondeterministic builtins as mismatches. - -It also generates compound boolean predicates, nested conditions, guarded matches, and multi-step -`filter`/`map` pipelines so agreement is checked beyond single-condition queries. - -The helpers are cached under `fuzz/target/`; set `MQ_DIFF_TREE_TARGET_DIR` or -`MQ_DIFF_VM_TARGET_DIR` to use different build directories. diff --git a/fuzz/fuzz_targets/differential.rs b/fuzz/fuzz_targets/differential.rs deleted file mode 100644 index bcd2dc87e..000000000 --- a/fuzz/fuzz_targets/differential.rs +++ /dev/null @@ -1,49 +0,0 @@ -#![no_main] - -//! Differential fuzzes the tree-walker and Tarn VM with the same deterministic programs. -//! -//! The engines are compiled into separate helper processes because `mq-lang/tarn` selects the -//! evaluator at compile time. `fuzz/scripts/run-differential.sh` builds those helpers and sets -//! the required environment variables before invoking this target. - -use std::process::Command; - -use libfuzzer_sys::fuzz_target; -use mq_fuzz::DifferentialContext; - -const RESULT_PREFIX: &str = "MQ_DIFF_RESULT:"; - -fn eval_with_runner(runner_variable: &str, script: &str) -> String { - let runner = std::env::var(runner_variable).unwrap_or_else(|_| { - panic!("{runner_variable} is not set; run this target through fuzz/scripts/run-differential.sh") - }); - let output = Command::new(&runner) - .arg(script) - .output() - .unwrap_or_else(|error| panic!("could not start {runner}: {error}")); - - assert!( - output.status.success(), - "{runner} exited with {}; stderr:\n{}", - output.status, - String::from_utf8_lossy(&output.stderr) - ); - - String::from_utf8_lossy(&output.stderr) - .lines() - .rev() - .find_map(|line| line.strip_prefix(RESULT_PREFIX)) - .unwrap_or_else(|| panic!("{runner} did not emit a differential result marker")) - .to_owned() -} - -fuzz_target!(|context: DifferentialContext| { - let script = context.to_script(); - let tree_result = eval_with_runner("MQ_DIFF_TREE_RUNNER", &script); - let vm_result = eval_with_runner("MQ_DIFF_VM_RUNNER", &script); - - assert_eq!( - tree_result, vm_result, - "tree-walker and Tarn VM produced different results for:\n{script}" - ); -}); diff --git a/fuzz/fuzz_targets/tarn.rs b/fuzz/fuzz_targets/tarn.rs deleted file mode 100644 index 8c9fec1f5..000000000 --- a/fuzz/fuzz_targets/tarn.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![no_main] - -// `mq-lang/tarn` routes `Engine::eval` through the bytecode VM instead of the tree-walker -// (see `mq-lang/src/tarn.rs`); without it this target would silently fuzz the tree-walker -// under the wrong name. Run with `cargo +nightly fuzz run tarn --features tarn`. -#[cfg(not(feature = "tarn"))] -compile_error!( - "the `tarn` fuzz target requires `--features tarn`, e.g. `cargo +nightly fuzz run tarn --features tarn`" -); - -use libfuzzer_sys::fuzz_target; -use mq_fuzz::Context; - -fuzz_target!(|context: Context| { - mq_fuzz::eval_and_check(&context); -}); diff --git a/fuzz/scripts/run-differential.sh b/fuzz/scripts/run-differential.sh deleted file mode 100755 index fb24afd19..000000000 --- a/fuzz/scripts/run-differential.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -workspace_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -build_dir="$workspace_dir/fuzz/target" -tree_target="${MQ_DIFF_TREE_TARGET_DIR:-$build_dir/differential-tree}" -vm_target="${MQ_DIFF_VM_TARGET_DIR:-$build_dir/differential-vm}" - -cd "$workspace_dir" -CARGO_TARGET_DIR="$tree_target" cargo build --package mq-fuzz --bin differential-runner --release -CARGO_TARGET_DIR="$vm_target" cargo build --package mq-fuzz --bin differential-runner --release --features tarn - -export MQ_DIFF_TREE_RUNNER="$tree_target/release/differential-runner" -export MQ_DIFF_VM_RUNNER="$vm_target/release/differential-runner" - -run_count="${MQ_DIFF_RUNS:-1000}" -cargo +nightly fuzz run differential -- -runs="$run_count" "$@" diff --git a/fuzz/src/bin/differential_runner.rs b/fuzz/src/bin/differential_runner.rs deleted file mode 100644 index f8e0469ba..000000000 --- a/fuzz/src/bin/differential_runner.rs +++ /dev/null @@ -1,21 +0,0 @@ -//! Executes one mq script and emits a stable result marker for differential fuzzing. - -use mq_lang::{DefaultEngine, null_input}; - -const RESULT_PREFIX: &str = "MQ_DIFF_RESULT:"; - -fn main() { - let script = std::env::args().nth(1).unwrap_or_default(); - let mut engine = DefaultEngine::default(); - engine.load_builtin_module(); - let result = match engine.eval(&script, null_input().into_iter()) { - Ok(values) => format!("ok:{values:?}"), - // The two engines have different internal error representations. Whether evaluation - // succeeds is the stable, user-visible contract this runner compares. - Err(_) => "err".to_owned(), - }; - - // Builtins may write to stdout or stderr. A distinct final stderr marker lets the fuzzer - // isolate this runner's result without treating program output as an engine difference. - eprintln!("{RESULT_PREFIX}{result}"); -} diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 8d46a50a6..be26a8bf7 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -1,9 +1,7 @@ -//! Shared arbitrary-script generation for the `interpreter` and `tarn` fuzz targets. -//! Both targets feed the same generated scripts through [`mq_lang::DefaultEngine`]; which -//! evaluator they exercise (tree-walker vs. the `tarn` bytecode VM) is decided by whether -//! `mq-lang/tarn` is enabled for the build. +//! Shared arbitrary-script generation for the `interpreter` fuzz target, which feeds +//! generated scripts through [`mq_lang::DefaultEngine`]. -use arbitrary::{Arbitrary, Unstructured}; +use arbitrary::Arbitrary; use itertools::Itertools; #[derive(Debug, Clone, Arbitrary)] @@ -76,99 +74,3 @@ pub fn eval_and_check(context: &Context) { std::panic::resume_unwind(err); } } - -/// A deterministic mq program shared by the tree-walker and Tarn differential fuzz target. -/// -/// The generator intentionally uses only language constructs supported by both execution -/// engines. This makes a reported mismatch actionable rather than an expected consequence of -/// fuzzing an implementation-specific feature. -#[derive(Debug, Clone)] -pub struct DifferentialContext { - values: [i16; 4], - selector: u8, -} - -impl<'a> Arbitrary<'a> for DifferentialContext { - fn arbitrary(unstructured: &mut Unstructured<'a>) -> arbitrary::Result { - // libFuzzer begins from an empty corpus and therefore generates very short inputs - // first. Zero-fill missing bytes so every input reaches an engine comparison instead - // of being discarded by a fixed-size `Arbitrary` derivation. - let mut bytes = [0_u8; 9]; - for byte in &mut bytes { - *byte = unstructured.arbitrary::().unwrap_or(0); - } - - Ok(Self { - values: [ - i16::from_le_bytes([bytes[0], bytes[1]]), - i16::from_le_bytes([bytes[2], bytes[3]]), - i16::from_le_bytes([bytes[4], bytes[5]]), - i16::from_le_bytes([bytes[6], bytes[7]]), - ], - selector: bytes[8], - }) - } -} - -impl DifferentialContext { - /// Renders this context as a valid, deterministic mq script. - pub fn to_script(&self) -> String { - let [a, b, c, extra] = self.values; - let prelude = format!("let a = {a} | let b = {b} | let c = {c} | let extra = {extra} | "); - - let body = match self.selector % 16 { - 0 => "a + b * c".to_owned(), - 1 => "if (a < b): a - c else: b + c".to_owned(), - 2 => format!("let values = [a, b, c] | get(values, {})", extra.rem_euclid(3)), - 3 => "let f = fn(value): a + value * c; | f(b)".to_owned(), - 4 => format!("var total = a | foreach(value, [b, c, {extra}]): total += value; | total"), - 5 => "let [first, second] = [a, b] | first * second + c".to_owned(), - 6 => format!("len([a, b, c, {extra}])"), - 7 => "let distance = fn(value): if (value > a): value - a else: a - value; | distance(b)".to_owned(), - 8 => "if ((a < b && b < c) || a == extra): a + b else: c - extra".to_owned(), - 9 => "if (a < b): if (b < c): a + b + c else: a + b - c else: if (a == c): extra else: a - c".to_owned(), - 10 => "let values = [a, b, c, extra] | values | filter(fn(value): (value > a && value < b) || value == c;) | map(fn(value): value + c;) | filter(fn(value): value != extra || value == a;)".to_owned(), - 11 => "var total = 0 | foreach(value, [a, b, c, extra]): if ((value > a && value < b) || value == c): total += value else: total -= value; | total".to_owned(), - 12 => "let qualifies = fn(value): (value >= a && value <= b) || value == c; | if (qualifies(extra) && extra != a): extra + c else: extra - c".to_owned(), - 13 => "match([a, b]) do | [first, second] if (first < second && second != c): first + second | [first, _] if (first == c || first == extra): first | _: c end".to_owned(), - 14 => "(a < b && b < c) || (extra == a && c != b)".to_owned(), - _ => "let values = [a, b, c, extra] | values | filter(fn(value): value >= a && value <= c;) | map(fn(value): if (value == b || value == extra): value * 2 else: value - 1;)".to_owned(), - }; - - format!("{prelude}{body}") - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn differential_scripts_evaluate_for_every_variant() { - for selector in 0..16 { - let context = DifferentialContext { - values: [-3, 5, 2, 7], - selector, - }; - let script = context.to_script(); - let mut engine = mq_lang::DefaultEngine::default(); - engine.load_builtin_module(); - let result = engine.eval(&script, mq_lang::null_input().into_iter()); - - assert!( - result.is_ok(), - "generated script failed to evaluate: {script}\nerror: {result:?}" - ); - } - } - - #[test] - fn differential_context_accepts_short_fuzzer_inputs() { - for input_length in 0..9 { - let bytes = vec![0; input_length]; - let mut input = Unstructured::new(&bytes); - - assert!(DifferentialContext::arbitrary(&mut input).is_ok()); - } - } -} diff --git a/justfile b/justfile index 9af072eb7..076f90dc1 100644 --- a/justfile +++ b/justfile @@ -26,20 +26,6 @@ bench: build-bench bench-local: cargo bench -# Run the shared mq-lang benchmark suite on the tree-walking evaluator. -bench-tree: - cargo bench -p mq-lang --bench benchmark - -# Run the same mq-lang benchmark suite on Tarn. `tarn` routes Engine::eval to the VM. -bench-vm: - cargo bench -p mq-lang --bench benchmark --features tarn - -# Run the same shared benchmark on the tree-walker and VM in sequence. -# Example: just bench-compare eval_compiled_fibonacci -bench-compare filter: - cargo bench -p mq-lang --bench benchmark {{filter}} - cargo bench -p mq-lang --bench benchmark --features tarn {{filter}} - # Build the project in release mode build: cargo build --release -p mq-run --bin mq @@ -63,10 +49,16 @@ build-target target: dump-bytecode query: cargo run -p mq-run --bin mq-dbg --features="debugger" -- -C --dump-bytecode -I null '{{query}}' -# Build benchmarks with codspeed. Runs against the tarn VM backend, not the tree-walker. +# Profiles executed Tarn opcodes through the debug binary. The profile changes dispatch cost, +# so use it to find hot instructions, not to measure elapsed time. +# Example: just vm-profile 'var i = 10000 | while(i > 0): i -= 1; | i' +vm-profile query: + cargo run -p mq-run --bin mq-dbg --features="debugger,vm-profile" -- --vm-profile -I null '{{query}}' + +# Build benchmarks with codspeed. [working-directory: 'crates/mq-lang'] build-bench: - cargo codspeed build --features tarn + cargo codspeed build # Build WebAssembly package for web use [working-directory: 'crates/mq-wasm'] @@ -101,17 +93,10 @@ build-node: build-node-wasm fmt: cargo fmt --all -- --check -# Run bundled mq tests through the tree-walking evaluator. -test-mq-tree: +# Run bundled mq tests through the Tarn bytecode VM. +test-mq: cargo run -p mq-test -- crates/mq-lang/builtin_tests.mq crates/mq-lang/modules/*_test.mq -# Run the identical bundled mq tests through Tarn. -test-mq-vm: - cargo run -p mq-test --features tarn -- crates/mq-lang/builtin_tests.mq crates/mq-lang/modules/*_test.mq - -# Keep both execution engines as a required validation gate until cutover. -test-mq: test-mq-tree test-mq-vm - # Check -U round-trip fidelity against the GFM spec examples (fetches spec.txt over the network) test-gfm-spec: cargo test -p mq-markdown --test gfm_roundtrip_fidelity -- --ignored --nocapture @@ -132,18 +117,10 @@ test-all: fmt lint test-mq test-doc test-all-features test test-cov: cargo llvm-cov --open --html --workspace --all-features --ignore-filename-regex 'crates/mq-(crawler|test|wasm|web-api|dap|python|lsp/src/capabilities\.rs|repl/src/repl\.rs)' -# Run fuzzing tests against the tree-walking evaluator +# Run fuzzing tests against the Tarn bytecode VM test-fuzz: cargo +nightly fuzz run interpreter -# Run fuzzing tests against the tarn bytecode VM -test-fuzz-tarn: - cargo +nightly fuzz run tarn --features tarn - -# Differentially fuzz the tree-walking evaluator and Tarn VM. Optional argument is run count. -test-fuzz-differential runs="1000": - MQ_DIFF_RUNS={{runs}} ./fuzz/scripts/run-differential.sh - # Run WebAssembly tests in Chrome [working-directory: 'crates/mq-wasm'] test-wasm: