diff --git a/.github/PRIVACY.md b/.github/PRIVACY.md index a559d700b..cc88bd1ec 100644 --- a/.github/PRIVACY.md +++ b/.github/PRIVACY.md @@ -1,4 +1,4 @@ -# Privacy Policy โ€” mq for Markdown (Chrome Extension) +# Privacy Policy: mq for Markdown (Chrome Extension) Last updated: 2026-08-10 @@ -14,7 +14,7 @@ Nothing is sent to a server. - Reads the HTML of the tab you're currently viewing (only when you click "Extract page") and converts it to Markdown, entirely on your device. - Runs the mq query you write against that content, also entirely on your device. - Optionally renders a preview overlay of the converted content on the page you're viewing. -- Copies the result to your clipboard when you click "Copy" โ€” this is a manual, user-initiated action; the extension does not read or write your clipboard otherwise. +- Copies the result to your clipboard when you click "Copy", this is a manual, user-initiated action; the extension does not read or write your clipboard otherwise. ## Data collection diff --git a/README.md b/README.md index 633d8d465..320ce1a8b 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,9 @@ Visit the site ๐ŸŒ -โ€” +ยท Read the book ๐Ÿ“– -โ€” +ยท Playground ๐ŸŽฎ

mq

@@ -136,8 +136,8 @@ mq is a Rust + TypeScript monorepo. | [`mq-crawler`](crates/mq-crawler) | Directory crawler for batch Markdown processing | [![Crates.io](https://img.shields.io/crates/v/mq-crawler?style=flat-square&labelColor=162032&color=498eb6)](https://crates.io/crates/mq-crawler) | | [`mq-macros`](crates/mq-macros) | Procedural macros for builtin function registration | [![Crates.io](https://img.shields.io/crates/v/mq-macros?style=flat-square&labelColor=162032&color=498eb6)](https://crates.io/crates/mq-macros) | | [`mq-test`](crates/mq-test) | Test runner for mq | [![Crates.io](https://img.shields.io/crates/v/mq-test?style=flat-square&labelColor=162032&color=498eb6)](https://crates.io/crates/mq-test) | -| [`mq-ffi`](crates/mq-ffi) | C API for integrating mq into C applications | โ€” | -| [`mq-wasm`](crates/mq-wasm) | WebAssembly bindings | โ€” | +| [`mq-ffi`](crates/mq-ffi) | C API for integrating mq into C applications | - | +| [`mq-wasm`](crates/mq-wasm) | WebAssembly bindings | - | | [`mq-web`](packages/mq-web) | Official WebAssembly build for the browser | [![npm](https://img.shields.io/npm/v/mq-web?style=flat-square&labelColor=162032&color=498eb6)](https://www.npmjs.com/package/mq-web) | | [`mq-nodejs`](packages/mq-nodejs) | Node.js bindings | [![npm](https://img.shields.io/npm/v/mq-nodejs?style=flat-square&labelColor=162032&color=498eb6)](https://www.npmjs.com/package/mq-nodejs) | diff --git a/crates/mq-bench/Cargo.toml b/crates/mq-bench/Cargo.toml index e1d75954a..e70f36679 100644 --- a/crates/mq-bench/Cargo.toml +++ b/crates/mq-bench/Cargo.toml @@ -1,7 +1,7 @@ [package] authors.workspace = true categories = ["command-line-utilities", "text-processing"] -description = "Benchmark runner for mq โ€” times bench_ functions written in .mq files" +description = "Benchmark runner for mq: times bench_ functions written in .mq files" edition.workspace = true homepage = "https://mqlang.org/" keywords = ["markdown", "jq", "query", "benchmark"] diff --git a/crates/mq-bench/README.md b/crates/mq-bench/README.md index 3ed1fdbba..712aaa1cb 100644 --- a/crates/mq-bench/README.md +++ b/crates/mq-bench/README.md @@ -1,6 +1,6 @@

mq-bench

-Standalone benchmark runner for mq โ€” times `bench_` functions written in `.mq` files. +Standalone benchmark runner for mq: times `bench_` functions written in `.mq` files. ## Overview @@ -11,7 +11,7 @@ Standalone benchmark runner for mq โ€” times `bench_` functions written in `.mq` Bench discovery uses the CST, same as [`mq-test`](../mq-test). Each bench takes no parameters, is compiled once, warmed up, then run `--iterations` times sequentially with -`std::time::Instant` โ€” never in parallel, since that would add scheduling noise to the +`std::time::Instant`: never in parallel, since that would add scheduling noise to the timings. This complements low-level, single-instruction Tarn VM benches (written in Rust with @@ -65,4 +65,4 @@ def bench_markdown_table_render(): end ``` -Bench functions must take no parameters โ€” the runner calls each one as `name()`. +Bench functions must take no parameters: the runner calls each one as `name()`. diff --git a/crates/mq-bench/src/main.rs b/crates/mq-bench/src/main.rs index a531a6463..85679cc74 100644 --- a/crates/mq-bench/src/main.rs +++ b/crates/mq-bench/src/main.rs @@ -9,7 +9,7 @@ use std::{path::PathBuf, process::ExitCode}; #[derive(Parser, Debug)] #[command(name = "mq-bench")] #[command(version = env!("CARGO_PKG_VERSION"))] -#[command(about = "Benchmark runner for mq โ€” times bench_ functions written in .mq files")] +#[command(about = "Benchmark runner for mq: times bench_ functions written in .mq files")] #[command(after_help = "# Examples:\n\n\ ## Run all benches in a specific file:\n\ mq-bench benches.mq\n\n\ diff --git a/crates/mq-bench/src/runner.rs b/crates/mq-bench/src/runner.rs index 600e0e947..ca7e6346a 100644 --- a/crates/mq-bench/src/runner.rs +++ b/crates/mq-bench/src/runner.rs @@ -584,7 +584,7 @@ mod tests { let bench_file = dir.join("bench.mq"); fs::write(&bench_file, "def bench_needs_arg(x):\n x\nend\n").unwrap(); - // Skipped, not failed โ€” nothing errored, so the run still reports success. + // Skipped, not failed: nothing errored, so the run still reports success. let passed = BenchRunner::new(vec![bench_file]).run().unwrap(); assert!(passed); diff --git a/crates/mq-check/src/builtin.rs b/crates/mq-check/src/builtin.rs index a700d3aaf..20d53f094 100644 --- a/crates/mq-check/src/builtin.rs +++ b/crates/mq-check/src/builtin.rs @@ -2036,1320 +2036,4 @@ fn register_bytes(ctx: &mut InferenceContext) { } #[cfg(test)] -mod tests { - use mq_hir::Hir; - use rstest::rstest; - - use crate::{TypeChecker, TypeError, types::Type}; - - /// Helper function to create HIR from code - fn create_hir(code: &str) -> Hir { - let mut hir = Hir::default(); - // Enable builtins to test builtin function types - hir.builtin.disabled = false; - hir.add_builtin(); // Add builtin functions to HIR - hir.add_code(None, code); - hir - } - - /// Helper function to run type checker - fn check_types(code: &str) -> Vec { - let hir = create_hir(code); - let mut checker = TypeChecker::new(); - checker.check(&hir) - } - - #[rstest] - #[case::flat(Type::array(Type::Number), Type::Number)] - #[case::nested(Type::array(Type::array(Type::array(Type::String))), Type::String)] - #[case::mixed_tuple( - Type::tuple(vec![Type::array(Type::String), Type::String]), - Type::String - )] - #[case::union_members( - Type::array(Type::union(vec![Type::Number, Type::array(Type::String)])), - Type::union(vec![Type::Number, Type::String]) - )] - fn test_flatten_result_type(#[case] arg: Type, #[case] leaf: Type) { - let signature = super::refine_signature("flatten", std::slice::from_ref(&arg)); - assert_eq!(signature, Some(Type::function(vec![arg], Type::array(leaf)))); - } - - #[test] - fn test_refine_signature_ignores_unresolved_and_other_builtins() { - assert_eq!(super::refine_signature("flatten", &[Type::Number]), None); - assert_eq!(super::refine_signature("reverse", &[Type::array(Type::Number)]), None); - } - - #[test] - fn test_registered_builtins_are_known_to_hir() { - let mut ctx = crate::infer::InferenceContext::new(); - super::register_all(&mut ctx); - let hir = create_hir(""); - let known: std::collections::HashSet<&str> = hir - .symbols() - .filter(|(_, symbol)| hir.is_builtin_symbol(symbol)) - .filter_map(|(_, symbol)| symbol.value.as_deref()) - .collect(); - let mut missing: Vec<&str> = ctx - .builtin_names() - .filter(|name| name.chars().all(|c| c.is_alphanumeric() || c == '_')) - .filter(|name| !known.contains(name)) - .collect(); - missing.sort_unstable(); - assert!( - missing.is_empty(), - "type signatures without a builtin definition: {missing:?}" - ); - } - - #[test] - fn test_public_native_builtins_have_type_signatures() { - let mut ctx = crate::infer::InferenceContext::new(); - super::register_all(&mut ctx); - let registered: std::collections::HashSet<&str> = ctx.builtin_names().collect(); - let mut missing: Vec<&str> = mq_lang::BUILTIN_FUNCTION_NAMES - .iter() - .copied() - .filter(|name| !registered.contains(name)) - .collect(); - missing.sort_unstable(); - assert!( - missing.is_empty(), - "public native builtins without a type signature: {missing:?}" - ); - } - - // Mathematical Functions - - #[rstest] - #[case::abs("abs(42)", true)] - #[case::abs_negative("abs(-10)", true)] - #[case::ceil("ceil(3.14)", true)] - #[case::floor("floor(3.14)", true)] - #[case::round("round(3.14)", true)] - #[case::trunc("trunc(3.14)", true)] - #[case::abs_string("abs(\"hello\")", false)] // Should fail: wrong type - fn test_unary_math_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::add_numbers("1 + 2")] - #[case::add_number_string("1 + \"string\"")] - #[case::add_strings("\"hello\" + \"world\"")] - #[case::add_string_number("\"value: \" + 42")] // String + number coercion - #[case::add_arrays("[1, 2] + [3, 4]")] // Array concatenation - #[case::add_mixed("1 + \"world\"")] // Number + string coercion - #[case::sub("10 - 5")] - #[case::mul("3 * 4")] - #[case::mul_array_repeat("[1, 2] * 3")] // Array repetition - #[case::div("10 / 2")] - #[case::mod_op("10 % 3")] - #[case::pow("2 ^ 8")] - fn test_arithmetic_operators(#[case] code: &str) { - let result = check_types(code); - assert!(result.is_empty(), "Code: {}\nResult: {:?}", code, result); - } - - #[rstest] - #[case::lt("5 < 10", true)] - #[case::gt("10 > 5", true)] - #[case::lte("5 <= 10", true)] - #[case::gte("10 >= 5", true)] - #[case::lt_string("\"a\" < \"b\"", true)] // String comparison - #[case::gt_string("\"z\" > \"a\"", true)] // String comparison - #[case::lt_bool("true < false", true)] // Bool comparison (false < true) - #[case::lt_mixed("\"a\" < 1", false)] // Should fail: mixed types - fn test_comparison_operators(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::eq_numbers("1 == 1", true)] - #[case::eq_strings("\"a\" == \"b\"", true)] - #[case::ne_numbers("1 != 2", true)] - #[case::ne_strings("\"a\" != \"b\"", true)] - fn test_equality_operators(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::min_numbers("min(1, 2)", true)] - #[case::max_numbers("max(1, 2)", true)] - #[case::min_strings("min(\"a\", \"b\")", true)] - #[case::max_strings("max(\"a\", \"b\")", true)] - fn test_min_max(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::and_op("true && false", true)] - #[case::or_op("true || false", true)] - #[case::not_op("!true", true)] - #[case::bang_op("!false", true)] - #[case::and_number("1 && 2", true)] // mq supports truthy/falsy semantics - fn test_logical_operators(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::nan("nan()", true)] - #[case::infinite("infinite()", true)] - #[case::is_nan("is_nan(1.0)", true)] - #[case::rand("rand()", true)] - #[case::rand_seeded("rand(42)", true)] - #[case::rand_int("rand_int(1, 10)", true)] - #[case::rand_int_seeded("rand_int(1, 10, 42)", true)] - #[case::ln("ln(2.0)", true)] - #[case::log10("log10(100)", true)] - #[case::log("log(2.0)", true)] - #[case::sqrt("sqrt(4)", true)] - #[case::exp("exp(1)", true)] - #[case::sin("sin(0)", true)] - #[case::cos("cos(0)", true)] - #[case::tan("tan(0)", true)] - #[case::ln_string("ln(\"x\")", false)] // Should fail: wrong type - #[case::sqrt_string("sqrt(\"x\")", false)] // Should fail: wrong type - #[case::sin_string("sin(\"x\")", false)] // Should fail: wrong type - fn test_special_number_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::downcase("downcase(\"HELLO\")", true)] - #[case::upcase("upcase(\"hello\")", true)] - #[case::ascii_downcase("ascii_downcase(\"HELLO\")", true)] - #[case::ascii_upcase("ascii_upcase(\"hello\")", true)] - #[case::trim("trim(\" hello \")", true)] - #[case::downcase_number("downcase(42)", false)] // Should fail: wrong type - #[case::ascii_downcase_number("ascii_downcase(42)", false)] // Should fail: wrong type - #[case::rtrim("rtrim(\" hello \")", true)] - #[case::rindex("rindex(\"hello world hello\", \"hello\")", true)] - #[case::token_count("token_count(\"hello world\", \"gpt-4\")", true)] - #[case::token_count_no_model("token_count(\"hello world\")", true)] - #[case::token_count_number("token_count(42, \"gpt-4\")", false)] // Should fail: wrong type - #[case::token_compress("to_markdown(\"# Title\\n\\nBody\") | token_compress(100)", true)] - #[case::token_compress_with_model("to_markdown(\"# Title\\n\\nBody\") | token_compress(100, \"gpt-4\")", true)] - #[case::token_compress_string("token_compress(\"not an array\", 100)", false)] // Should fail: wrong type - #[case::capture("capture(\"hello 42\", \"(?P\\\\w+)\")", true)] - #[case::is_regex_match("is_regex_match(\"hello123\", \"[0-9]+\")", true)] - #[case::is_not_regex_match("is_not_regex_match(\"hello123\", \"[0-9]+\")", true)] - #[case::is_not_regex_match_number("is_not_regex_match(42, \"[0-9]+\")", false)] // Should fail: wrong type - #[case::scan("scan(\"2024-06\", \"(\\\\d{4})-(\\\\d{2})\")", true)] - #[case::scan_number("scan(42, \"[0-9]+\")", false)] // Should fail: wrong type - #[case::base64url("base64url(\"hello\")", true)] - #[case::base64urld("base64urld(\"aGVsbG8=\")", true)] - #[case::ltrim_number("ltrim(42)", false)] - #[case::rtrim_number("rtrim(42)", false)] - #[case::html_escape("html_escape(\"hi\")", true)] - #[case::html_escape_number("html_escape(42)", false)] // Should fail: wrong type - #[case::html_unescape("html_unescape(\"<b>hi</b>\")", true)] - #[case::html_unescape_number("html_unescape(42)", false)] // Should fail: wrong type - #[case::strip_tags("strip_tags(\"hi\")", true)] - #[case::strip_tags_number("strip_tags(42)", false)] // Should fail: wrong type - #[case::sanitize_html("sanitize_html(\"\")", true)] - #[case::sanitize_html_number("sanitize_html(42)", false)] // Should fail: wrong type - #[case::markdown_escape("markdown_escape(\"**bold**\", \"text\")", true)] - #[case::markdown_escape_number("markdown_escape(42, \"text\")", false)] // Should fail: wrong type - #[case::markdown_escape_context_number("markdown_escape(\"a\", 42)", false)] // Should fail: wrong type - #[case::markdown_escape_markdown_input("to_code(\"value\", \"text\") | markdown_escape(\"text\")", true)] - fn test_string_case_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::starts_with("starts_with(\"hello\", \"he\")", true)] - #[case::ends_with("ends_with(\"hello\", \"lo\")", true)] - #[case::index("index(\"hello\", \"ll\")", true)] - #[case::rindex("rindex(\"hello\", \"l\")", true)] - #[case::indices("indices(\"hello world hello\", \"hello\")", true)] - fn test_string_search_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::replace("replace(\"hello\", \"l\", \"r\")", true)] - #[case::gsub("gsub(\"hello\", \"l\", \"r\")", true)] - #[case::split("split(\"a,b,c\", \",\")", true)] - #[case::join("join([\"a\", \"b\"], \",\")", true)] - fn test_string_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::lines("lines(\"a\\nb\\nc\")", true)] - #[case::lines_wrong_type("lines(42)", false)] - #[case::unlines("unlines([\"a\", \"b\", \"c\"])", true)] - fn test_lines_unlines_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::explode("explode(\"hello\")", true)] - #[case::implode("implode([104, 101, 108, 108, 111])", true)] - #[case::utf8bytelen("utf8bytelen(\"hello\")", true)] - fn test_string_codepoint_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::regex_match("regex_match(\"hello123\", \"[0-9]+\")", true)] - #[case::regex_escape("regex_escape(\"a.b*c?\")", true)] - #[case::regex_escape_number("regex_escape(42)", false)] // Should fail: wrong type - #[case::base64("base64(\"hello\")", true)] - #[case::base64d("base64d(\"aGVsbG8=\")", true)] - #[case::base64d_bytes("base64d_bytes(\"aGVsbG8=\")", true)] - #[case::base64urld_bytes("base64urld_bytes(\"aGVsbG8\")", true)] - #[case::url_encode("url_encode(\"hello world\")", true)] - #[case::url_decode("url_decode(\"hello%20world\")", true)] - #[case::uuid("uuid()", true)] - #[case::uuid_v4("uuid_v4()", true)] - #[case::uuid_v7("uuid_v7()", true)] - #[case::random_string("random_string(10, \"abc\")", true)] - #[case::random_string_seeded("random_string(10, \"abc\", 42)", true)] - // Should fail: wrong types. Uses non-numeric/non-string args so it can't accidentally - // satisfy the seeded ternary overload via the implicit-piped-input fallback (a bare - // `(String, Number)` pair would coincidentally match that overload's charset/seed slots). - #[case::random_string_wrong_types("random_string(true, false)", false)] - fn test_string_encoding_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Array Functions - - #[rstest] - #[case::flatten("flatten([[1, 2], [3, 4]])", true)] - #[case::reverse("reverse([1, 2, 3])", true)] - #[case::sort("sort([3, 1, 2])", true)] - #[case::uniq("uniq([1, 2, 2, 3])", true)] - #[case::compact("compact([1, none, 2])", true)] - #[case::shuffle("shuffle([1, 2, 3])", true)] - #[case::shuffle_seeded("shuffle([1, 2, 3], 42)", true)] - #[case::sample("sample([1, 2, 3], 2)", true)] - #[case::sample_seeded("sample([1, 2, 3], 2, 42)", true)] - fn test_array_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::len_array("len([1, 2, 3])", true)] - #[case::len_string("len(\"hello\")", true)] - #[case::slice("slice([1, 2, 3, 4], 1, 3)", true)] - #[case::insert("insert([1, 3], 1, 2)", true)] - fn test_array_access_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::range_two_args("range(1, 5)", true)] - #[case::range_three_args("range(1, 10, 2)", true)] - #[case::repeat("repeat(\"x\", 3)", true)] - fn test_array_creation_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Dictionary Functions - - #[rstest] - #[case::keys("keys({\"a\": 1, \"b\": 2})", true)] - #[case::values("values({\"a\": 1, \"b\": 2})", true)] - #[case::entries("entries({\"a\": 1, \"b\": 2})", true)] - #[case::has_dict("has({\"a\": 1}, \"a\")", true)] - #[case::has_array("has([1, 2, 3], 1)", true)] - fn test_dict_query_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::pick("pick({\"a\": 1, \"b\": 2, \"c\": 3}, [\"a\", \"b\"])", true)] - #[case::omit("omit({\"a\": 1, \"b\": 2, \"c\": 3}, [\"a\"])", true)] - fn test_dict_pick_omit_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::get("get({\"a\": 1}, \"a\")", true)] - #[case::get_array("get([1, 2, 3], 0)", true)] - #[case::get_generic_binary("let d = dict() | get(d, \"key\")", true)] - #[case::get_generic_ternary("let d = dict() | get(d, \"key\")[\"name\"]", true)] - #[case::set("set({\"a\": 1}, \"b\", 2)", true)] - #[case::del("del({\"a\": 1, \"b\": 2}, \"a\")", true)] - #[case::update("update({\"a\": 1}, {\"b\": 2})", true)] - #[case::from_entries("from_entries([[\"a\", 1], [\"b\", 2]])", true)] - #[case::with_entries("with_entries({\"a\": 1}, fn(pair): [pair[0], pair[1] + 1];)", true)] - fn test_dict_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Type Conversion Functions - - #[rstest] - #[case::to_number("to_number(\"42\")", true)] - #[case::to_string("to_string(42)", true)] - #[case::to_array("to_array(42)", true)] - #[case::type_of("type(42)", true)] - #[case::to_boolean_string("to_boolean(\"true\")", true)] - #[case::to_boolean_bool("to_boolean(true)", true)] - #[case::to_boolean_number("to_boolean(42)", false)] // Should fail: wrong type - fn test_type_conversion_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Date/Time Functions - - #[rstest] - #[case::now("now()", true)] - #[case::from_date("from_date(\"2024-01-01\")", true)] - #[case::to_date("to_date(1704067200000, \"%Y-%m-%d\")", true)] - #[case::gmtime("gmtime(0)", true)] - #[case::localtime("localtime(0)", true)] - #[case::mktime("mktime([2024, 0, 1, 0, 0, 0, 1, 0])", true)] - #[case::strftime("strftime(0, \"%Y-%m-%d\")", true)] - #[case::strptime("strptime(\"2024-01-01\", \"%Y-%m-%d\")", true)] - #[case::date_add("date_add([2024, 0, 1, 0, 0, 0, 1, 0], 1, \"days\")", true)] - #[case::date_diff( - "date_diff([2024, 0, 1, 0, 0, 0, 1, 0], [2024, 0, 2, 0, 0, 0, 2, 1], \"days\")", - true - )] - #[case::gmtime_string("gmtime(\"x\")", false)] // Should fail: wrong type - #[case::mktime_string("mktime(\"x\")", false)] // Should fail: wrong type - #[case::strftime_swapped("strftime(\"x\", 1)", false)] // Should fail: wrong type - #[case::strptime_swapped("strptime(1, \"%Y-%m-%d\")", false)] // Should fail: wrong type - fn test_datetime_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // I/O And Utility Functions - - #[rstest] - #[case::print("print(42)", true)] - #[case::stderr("stderr(\"error\")", true)] - #[case::input("input()", true)] - fn test_io_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // File I/O And Path Functions - - #[rstest] - #[case::read_file("read_file(\"a.md\")", true)] - #[case::read_file_bytes("read_file_bytes(\"a.md\")", true)] - #[case::open_file("open_file(\"a.md\")", true)] - #[case::open_file_number("open_file(42)", false)] - #[case::file_exists("file_exists(\"a.md\")", true)] - #[case::file_size("file_size(\"a.md\")", true)] - #[case::file_info("file_info(\"a.md\")", true)] - #[case::collection("collection(\"docs\")", true)] - #[case::collection_respect_gitignore("collection(\"docs\", true)", true)] - #[case::collection_len("len(collection(\"docs\"))", true)] - #[case::basename("basename(\"a/b.md\")", true)] - #[case::dirname("dirname(\"a/b.md\")", true)] - #[case::extname("extname(\"a/b.md\")", true)] - #[case::stem("stem(\"a/b.md\")", true)] - #[case::path_join("path_join(\"a\", \"b.md\")", true)] - #[case::glob_match("glob_match(\"*.md\", \"a.md\")", true)] - #[case::write_file_string("write_file(\"a.md\", \"content\")", true)] - #[case::write_file_bytes("write_file(\"a.md\", to_bytes(\"content\"))", true)] - #[case::http_get("http(\"get\", \"https://example.invalid\")", true)] - #[case::http_get_symbol("http(:get, \"https://example.invalid\")", true)] - #[case::http_post("http(\"post\", \"https://example.invalid\", \"{}\")", true)] - #[case::http_post_symbol("http(:post, \"https://example.invalid\", \"{}\")", true)] - #[case::http_headers( - "http(\"get\", \"https://example.invalid\", {\"Accept\": \"application/json\"})", - true - )] - #[case::http_headers_symbol("http(:get, \"https://example.invalid\", {\"Accept\": \"application/json\"})", true)] - #[case::http_body_and_headers( - "http(\"post\", \"https://example.invalid\", \"{}\", {\"Content-Type\": \"application/json\"})", - true - )] - #[case::http_body_and_headers_symbol( - "http(:post, \"https://example.invalid\", \"{}\", {\"Content-Type\": \"application/json\"})", - true - )] - #[case::read_file_number("read_file(42)", false)] // Should fail: wrong type - #[case::file_exists_number("file_exists(42)", false)] // Should fail: wrong type - #[case::file_size_number("file_size(42)", false)] // Should fail: wrong type - #[case::file_info_number("file_info(42)", false)] // Should fail: wrong type - #[case::collection_number("collection(42)", false)] // Should fail: wrong type - #[case::collection_respect_gitignore_number("collection(\"docs\", 42)", false)] // Should fail: wrong type - #[case::basename_number("basename(42)", false)] // Should fail: wrong type - #[case::path_join_number("path_join(42, \"b\")", false)] // Should fail: wrong type - #[case::glob_match_number("glob_match(42, \"a.md\")", false)] // Should fail: wrong type - #[case::write_file_number("write_file(42, \"content\")", false)] // Should fail: wrong type - #[case::http_get_number("http(\"get\", 42)", false)] // Should fail: wrong type - #[case::http_headers_number("http(\"get\", \"https://example.invalid\", 42)", false)] // Should fail: wrong type - fn test_file_io_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Complex Expressions With Builtins - - #[rstest] - #[case::chained_string_ops("upcase(trim(\" hello \"))", true)] - #[case::math_expression("abs(min(-5, -10) + max(3, 7))", true)] - #[case::array_pipeline("len(reverse(sort([3, 1, 2])))", true)] - #[case::mixed_operations("to_string(len(split(\"a,b,c\", \",\")))", true)] - fn test_complex_builtin_expressions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Pipe Type Propagation - - #[rstest] - #[case::string_to_upcase("\"hello\" | upcase", true)] - #[case::string_to_ascii_upcase("\"hello\" | ascii_upcase", true)] - #[case::string_to_trim("\" hello \" | trim", true)] - #[case::number_to_abs("-42 | abs", true)] - #[case::string_to_len("\"hello\" | len", true)] - #[case::chained_pipes("\" hello \" | trim | upcase", true)] - #[case::chained_string_to_len("\"hello\" | upcase | len", true)] - #[case::chained_split_to_len("\"hello\" | split(\",\") | len", true)] - #[case::chained_array_reverse_first("[1,2,3] | reverse | first", true)] - #[case::number_to_upcase("42 | upcase", false)] // Number piped to string function - #[case::number_to_ascii_upcase("42 | ascii_upcase", false)] // Number piped to string function - fn test_pipe_type_propagation(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Type Error Cases - #[rstest] - #[case::add_bool_number("true + 1", false)] // bool + number is invalid - #[case::add_bool_string("true + \"s\"", false)] // bool + string is invalid - #[case::sub_strings("\"a\" - \"b\"", false)] // strings cannot be subtracted - #[case::sub_string_number("\"a\" - 1", false)] // string - number is invalid - #[case::mul_string_number("\"a\" * 3", true)] // string * number is invalid - #[case::div_strings("\"a\" / \"b\"", false)] // strings cannot be divided - #[case::mod_strings("\"a\" % \"b\"", false)] // strings cannot use modulo - #[case::sub_bool_bool("true - false", false)] // booleans cannot be subtracted - #[case::mul_bool_number("true * 2", false)] // bool * number is invalid - #[case::div_bool_number("true / 2", false)] // bool / number is invalid - #[case::mod_bool_number("true % 2", false)] // bool % number is invalid - #[case::mod_number_string("10 % \"2\"", false)] // number % string is invalid - fn test_arithmetic_type_errors(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::lt_string_number("\"a\" < 1", false)] // mixed types - #[case::gt_number_string("1 > \"a\"", false)] // mixed types - #[case::lte_bool_number("true <= 1", false)] // mixed types - #[case::gte_string_bool("\"a\" >= true", false)] // mixed types - #[case::lt_number_bool("1 < true", false)] // mixed types - #[case::gt_bool_string("true > \"x\"", false)] // mixed types - #[case::lte_string_number("\"a\" <= 1", false)] // mixed types - #[case::gte_number_bool("1 >= false", false)] // mixed types - fn test_comparison_type_errors(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::not_number("!42", true)] // number is truthy/falsy โ†’ bool - #[case::not_string("!\"hello\"", true)] // string is truthy/falsy โ†’ bool - #[case::not_array("![1, 2, 3]", true)] // array is truthy/falsy โ†’ bool - fn test_logical_type_errors(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::abs_string("abs(\"not a number\")", false)] // wrong argument type - #[case::downcase_number("downcase(42)", false)] // wrong argument type - #[case::ascii_downcase_number("ascii_downcase(42)", false)] // wrong argument type - #[case::ceil_string("ceil(\"hello\")", false)] // wrong argument type - #[case::floor_bool("floor(true)", false)] // wrong argument type - #[case::len_no_args("len()", true)] // valid: root-level dynamic piped input satisfies the arg - #[case::split_numbers("split(1, 2)", false)] // wrong argument types - #[case::join_numbers("join(1, 2)", false)] // wrong argument types - #[case::starts_with_numbers("starts_with(1, 2)", false)] // wrong argument types - #[case::round_string("round(\"hello\")", false)] // wrong argument type - #[case::trunc_string("trunc(\"hello\")", false)] // wrong argument type - #[case::abs_bool("abs(true)", false)] // wrong argument type - #[case::floor_string("floor(\"hello\")", false)] // wrong argument type - #[case::ceil_bool("ceil(false)", false)] // wrong argument type - #[case::upcase_number("upcase(42)", false)] // wrong argument type - #[case::ascii_upcase_number("ascii_upcase(42)", false)] // wrong argument type - #[case::trim_number("trim(42)", false)] // wrong argument type - #[case::ltrim_bool("ltrim(true)", false)] // wrong argument type - #[case::rtrim_bool("rtrim(true)", false)] // wrong argument type - #[case::explode_number("explode(42)", false)] // expects string - #[case::implode_string("implode(\"hello\")", false)] // expects array of numbers - #[case::split_wrong_sep("split(\"hello\", 42)", false)] // separator must be string - #[case::join_wrong_sep("join([\"a\", \"b\"], 42)", false)] // separator must be string - #[case::replace_wrong_sep("replace(\"hello\", 42, \"r\")", false)] // wrong separator type - #[case::replace_too_few_args("replace(\"l\")", false)] // 1 explicit + 1 piped = 2 args, needs 3 - #[case::replace_too_many_args("replace(\"hello\", \"l\", \"r\", \"extra\")", false)] // too many args - #[case::gsub_wrong_first("gsub(42, \"l\", \"r\")", false)] // expects string first arg - #[case::starts_with_num_sep("starts_with(\"hello\", 42)", false)] // expects string prefix - #[case::ends_with_num_sep("ends_with(\"hello\", 42)", false)] // expects string suffix - #[case::flatten_string("flatten(\"hello\")", false)] // expects array - #[case::sort_string("sort(\"hello\")", false)] // expects array - #[case::uniq_string("uniq(\"hello\")", false)] // expects array - #[case::compact_string("compact(\"hello\")", false)] // expects array - #[case::reverse_number("reverse(42)", false)] // expects array or string - #[case::values_number("values(42)", false)] // expects dict - #[case::values_string("values(\"hello\")", false)] // expects dict - #[case::entries_string("entries(\"hello\")", false)] // expects dict - #[case::range_string("range(\"a\", true)", false)] // expects number or string - #[case::repeat_bool("repeat(\"x\", true)", false)] // second arg must be number - fn test_function_type_errors(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::number_to_upcase("42 | upcase", false)] // number piped to string function - #[case::number_to_trim("42 | trim", false)] // number piped to string function - #[case::string_to_abs("\"hello\" | abs", false)] // string piped to number function - #[case::string_to_ceil("\"hello\" | ceil", false)] // string piped to number function - #[case::number_to_downcase("42 | downcase", false)] // number piped to string function - #[case::number_to_ascii_downcase("42 | ascii_downcase", false)] // number piped to string function - #[case::bool_to_upcase("true | upcase", false)] // bool piped to string function - #[case::bool_to_ascii_upcase("true | ascii_upcase", false)] // bool piped to string function - #[case::bool_to_trim("true | trim", false)] // bool piped to string function - #[case::bool_to_abs("true | abs", false)] // bool piped to number function - #[case::string_to_floor("\"hello\" | floor", false)] // string piped to number function - #[case::string_to_round("\"hello\" | round", false)] // string piped to number function - #[case::string_to_trunc("\"hello\" | trunc", false)] // string piped to number function - #[case::number_to_len("42 | len", false)] // number piped to len (expects array or string) - #[case::bool_to_len("true | len", false)] // bool piped to len - #[case::number_to_sort("42 | sort", false)] // number piped to sort (expects array) - #[case::number_to_reverse("42 | reverse", false)] // number piped to reverse - #[case::number_to_flatten("42 | flatten", false)] // number piped to flatten - #[case::number_to_uniq("42 | uniq", false)] // number piped to uniq - fn test_pipe_type_errors(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - #[case::piped_join("[\"a\", \"b\"] | join(\",\")", true)] // array piped as first arg to join - #[case::piped_split("\"a,b,c\" | split(\",\")", true)] // string piped as first arg to split - #[case::piped_starts_with("\"hello\" | starts_with(\"he\")", true)] - #[case::piped_ends_with("\"hello\" | ends_with(\"lo\")", true)] - #[case::piped_index("\"hello\" | index(\"ll\")", true)] - #[case::piped_replace("\"hello\" | replace(\"l\", \"r\")", true)] - #[case::piped_gsub("\"hello\" | gsub(\"l\", \"r\")", true)] - #[case::piped_gsub_variable_arg("def slugify(s, separator = \"-\"): s | gsub(\"[^a-z0-9]+\", separator) end", true)] // regression: default param used in gsub should not produce false error - #[case::replace_sstring_args("def f(x, v): x | replace(s\"old${v}\", s\"new${v}\") end", true)] // s-string args should not expand segments as separate arguments - #[case::replace_chained_sstring( - "def f(line, prev, ver): line | replace(s\"version = \\\"${prev}\\\"\", s\"version = \\\"${ver}\\\"\") | replace(s\"old${prev}\", s\"new${ver}\") end", - true - )] // chained replace with s-string args - #[case::piped_slice("[1, 2, 3, 4] | slice(1, 3)", true)] - #[case::piped_repeat("\"x\" | repeat(3)", true)] - #[case::piped_join_wrong_type("42 | join(\",\")", false)] // number piped to join (expects array) - #[case::piped_split_wrong_type("42 | split(\",\")", false)] // number piped to split (expects string) - fn test_piped_function_calls(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Collection Functions - - #[rstest] - #[case::first_array("first([1, 2, 3])", true)] - #[case::last_array("last([1, 2, 3])", true)] - #[case::first_string("first(\"hello\")", true)] - #[case::last_string("last(\"hello\")", true)] - #[case::contains_string("contains(\"hello world\", \"world\")", true)] - #[case::contains_array("contains([1, 2, 3], 2)", true)] - #[case::contains_dict("contains({\"a\": 1, \"b\": 2}, \"a\")", true)] - #[case::in_array("in([1, 2, 3], 1)", true)] - #[case::in_string("in(\"hello world\", \"world\")", true)] - #[case::in_sub_array("in([1, 2, 3, 4], [2, 3])", true)] - #[case::in_return_plus_number("in([1, 2, 3], 1) + 1", false)] // in returns bool; bool + number is invalid - #[case::contains_return_plus_number("contains(\"hello\", \"he\") + 1", false)] // contains returns bool; bool + number is invalid - #[case::bsearch("bsearch([1, 2, 3, 4], 2)", true)] - #[case::bsearch_other_type("bsearch([1, 2, 3, 4], \"2\")", true)] // the target is never found - fn test_collection_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Higher-Order Collection Functions - - #[rstest] - #[case::map_array("map([1, 2, 3], fn(x): x + 1;)", true)] - #[case::filter_array("filter([1, 2, 3], fn(x): x > 1;)", true)] - #[case::fold_array("fold([1, 2, 3], 0, fn(acc, x): acc + x;)", true)] - #[case::sort_by_array("sort_by([1, 2, 3], fn(x): x;)", true)] - #[case::flat_map_array("flat_map([1, 2, 3], fn(x): [x];)", true)] - fn test_higher_order_collection_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // None Propagation for Higher-Order Functions - - #[rstest] - #[case::filter_none_identity("filter(None, fn(x): x;)", true)] - #[case::filter_none_array_return("filter(None, fn(x): [x[0], x[1]];)", true)] - #[case::flat_map_none("flat_map(None, fn(x): [x];)", true)] - #[case::map_none_identity("map(None, fn(x): x;)", true)] - fn test_none_propagation_higher_order(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Type Check Functions - - #[rstest] - #[case::is_none("is_none(42)", true)] - #[case::is_array("is_array([1,2,3])", true)] - #[case::is_dict("is_dict({\"a\": 1})", true)] - #[case::is_string("is_string(\"hello\")", true)] - #[case::is_number("is_number(42)", true)] - #[case::is_bool("is_bool(true)", true)] - #[case::is_empty_string("is_empty(\"\")", true)] - #[case::is_empty_array("is_empty([])", true)] - fn test_type_check_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Type filter functions (`arrays`, `booleans`, etc.) are `def`-based builtins from - // builtin.mq, not registered in this file: hir.add_builtin() loads builtin.mq's - // source into the HIR, so their types are inferred from the function bodies just - // like any other user-defined function. - #[rstest] - #[case::arrays("arrays([1, 2])", true)] - #[case::markdowns("markdowns(to_hr())", true)] - #[case::booleans("booleans(true)", true)] - #[case::numbers("numbers(42)", true)] - #[case::strings("strings(\"a\")", true)] - #[case::dicts("dicts({\"a\": 1})", true)] - #[case::nones("nones(None)", true)] - #[case::bytes_filter("bytes(to_bytes(\"a\"))", true)] - #[case::iterables_array("iterables([1, 2])", true)] - #[case::iterables_dict("iterables({\"a\": 1})", true)] - #[case::scalars("scalars(1)", true)] - fn test_type_filter_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Utility Functions - - #[rstest] - #[case::coalesce("coalesce(1, 2)", true)] - #[case::error_func("error(\"message\")", true)] - #[case::halt_func("halt(1)", true)] - #[case::all_symbols("all_symbols()", true)] - #[case::intern("intern(\"symbol\")", true)] - #[case::is_debug_mode("is_debug_mode()", true)] - #[case::breakpoint("breakpoint()", true)] - #[case::assert_func("assert(42)", true)] - #[case::assert_func_bool("assert(true)", true)] - #[case::negate_func("negate(5)", true)] - #[case::pow_func("pow(2, 8)", true)] - #[case::convert_at_operator("\"hello\" @ :text", true)] - #[case::convert_func("convert(\"hello\", :text)", true)] - fn test_utility_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Numeric Bit-Shift Functions - - #[rstest] - #[case::shift_left_numbers("shift_left(3, 1)", true)] - #[case::shift_right_numbers("shift_right(8, 2)", true)] - #[case::shift_left_wrong_second_arg("shift_left(\"hello\", \"str\")", false)] - #[case::shift_right_wrong_second_arg("shift_right(\"hello\", \"str\")", false)] - fn test_bitshift_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Markdown Type Check Functions - - #[rstest] - #[case::is_h("to_markdown(\"# hello\") | first() | is_h()", true)] - #[case::is_p("to_markdown(\"hello\") | first() | is_p()", true)] - #[case::is_code("to_markdown(\"hello\") | first() | is_code()", true)] - #[case::is_code_block("to_markdown(\"hello\") | first() | is_code_block()", true)] - #[case::is_code_inline("to_markdown(\"hello\") | first() | is_code_inline()", true)] - #[case::is_em("to_markdown(\"hello\") | first() | is_em()", true)] - #[case::is_strong("to_markdown(\"hello\") | first() | is_strong()", true)] - #[case::is_link("to_markdown(\"hello\") | first() | is_link()", true)] - #[case::is_image("to_markdown(\"hello\") | first() | is_image()", true)] - #[case::is_list("to_markdown(\"hello\") | first() | is_list()", true)] - #[case::is_list_item("to_markdown(\"hello\") | first() | is_list_item()", true)] - #[case::is_table("to_markdown(\"hello\") | first() | is_table()", true)] - #[case::is_table_row("to_markdown(\"hello\") | first() | is_table_row()", true)] - #[case::is_table_cell("to_markdown(\"hello\") | first() | is_table_cell()", true)] - #[case::is_blockquote("to_markdown(\"hello\") | first() | is_blockquote()", true)] - #[case::is_hr("to_markdown(\"hello\") | first() | is_hr()", true)] - #[case::is_html("to_markdown(\"hello\") | first() | is_html()", true)] - #[case::is_text("to_markdown(\"hello\") | first() | is_text()", true)] - #[case::is_softbreak("to_markdown(\"hello\") | first() | is_softbreak()", true)] - #[case::is_hardbreak("to_markdown(\"hello\") | first() | is_hardbreak()", true)] - #[case::is_task_list_item("to_markdown(\"hello\") | first() | is_task_list_item()", true)] - #[case::is_footnote("to_markdown(\"hello\") | first() | is_footnote()", true)] - #[case::is_footnote_ref("to_markdown(\"hello\") | first() | is_footnote_ref()", true)] - #[case::is_strikethrough("to_markdown(\"hello\") | first() | is_strikethrough()", true)] - #[case::is_math("to_markdown(\"hello\") | first() | is_math()", true)] - #[case::is_math_inline("to_markdown(\"hello\") | first() | is_math_inline()", true)] - #[case::is_toml("to_markdown(\"hello\") | first() | is_toml()", true)] - #[case::is_yaml("to_markdown(\"hello\") | first() | is_yaml()", true)] - #[case::is_h_level("to_markdown(\"# hello\") | first() | is_h_level(1)", true)] - #[case::is_h_level_wrong_type("is_h_level(42, \"str\")", false)] - #[case::is_callout("to_markdown(\"hello\") | first() | is_callout()", true)] - #[case::is_table_align("to_markdown(\"hello\") | first() | is_table_align()", true)] - fn test_markdown_type_check_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Markdown Conversion Functions - - #[rstest] - #[case::from_html("\"

Hello

\" | from_html()", true)] - #[case::to_markdown("to_markdown(\"hello\")", true)] - #[case::to_text("to_markdown(\"hello\") | first() | to_text()", true)] - #[case::to_html("to_markdown(\"hello\") | first() | to_html()", true)] - #[case::to_markdown_string("to_markdown(\"hello\") | first() | to_markdown_string()", true)] - #[case::to_code("to_markdown(\"hello\") | first() | to_code(\"rust\")", true)] - #[case::to_code_inline("to_markdown(\"hello\") | first() | to_code_inline()", true)] - #[case::to_h("to_markdown(\"hello\") | first() | to_h(1)", true)] - #[case::to_hr("to_hr()", true)] - #[case::to_image("to_image(\"alt\", \"url\", \"title\")", true)] - #[case::to_link("to_link(\"text\", \"url\", \"title\")", true)] - #[case::to_md_list("to_markdown(\"hello\") | first() | to_md_list(1)", true)] - #[case::to_md_name("to_markdown(\"hello\") | first() | to_md_name()", true)] - #[case::to_md_table_cell("to_md_table_cell(\"hello\", 0, 0)", true)] - #[case::to_md_table_row("to_markdown(\"hello\") | first() | to_md_table_row()", true)] - #[case::to_md_text("to_markdown(\"hello\") | first() | to_md_text()", true)] - #[case::to_math("to_markdown(\"hello\") | first() | to_math()", true)] - #[case::to_math_inline("to_markdown(\"hello\") | first() | to_math_inline()", true)] - #[case::to_mdx("to_mdx(\"hello\")", true)] - #[case::to_strong("to_markdown(\"hello\") | first() | to_strong()", true)] - #[case::to_em("to_markdown(\"hello\") | first() | to_em()", true)] - #[case::to_blockquote("to_markdown(\"hello\") | first() | to_blockquote()", true)] - #[case::to_delete("to_markdown(\"hello\") | first() | to_delete()", true)] - #[case::to_callout("to_markdown(\"hello\") | first() | to_callout(\"note\", \"Note\")", true)] - #[case::to_md_fragment_markdown("to_markdown(\"hello\") | first() | to_md_fragment()", true)] - #[case::to_md_fragment_array("to_md_fragment([\"a\", \"b\"])", true)] - #[case::to_md_table_align("to_md_table_align([\"left\", \"right\"])", true)] - fn test_markdown_conversion_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Markdown Manipulation Functions - - #[rstest] - #[case::attr("to_markdown(\"[link](url)\") | first() | attr(\"url\")", true)] - #[case::set_attr("to_markdown(\"[link](url)\") | first() | set_attr(\"href\", \"new\")", true)] - #[case::set_children("to_markdown(\"# heading\") | first() | set_children([\"new\"])", true)] - #[case::get_title("to_markdown(\"[link](url)\") | first() | get_title", true)] - #[case::get_url("to_markdown(\"[link](url)\") | first() | get_url", true)] - #[case::get_location("to_markdown(\"# heading\") | first() | get_location", true)] - #[case::set_check("to_markdown(\"- [ ] task\") | first() | set_check(true)", true)] - #[case::set_list_ordered("to_markdown(\"- item\") | first() | set_list_ordered(false)", true)] - #[case::set_code_block_lang("to_markdown(\"```\\ncode\\n```\") | first() | set_code_block_lang(\"rust\")", true)] - #[case::set_ref("to_markdown(\"[link][ref]\") | first() | set_ref(\"ref\")", true)] - fn test_markdown_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Dict/Array Constructor Functions - - #[rstest] - #[case::dict_nullary("dict()", true)] - #[case::array_func("array(42)", true)] - fn test_constructor_functions(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - // Higher-Order Function Lambda Type Errors - - #[rstest] - #[case::map_lambda_sub_error("map([1, 2, 3], fn(x): x - \"str\";)", false, "map lambda body type error")] - #[case::map_lambda_bool_op("map([1, 2, 3], fn(x): x - true;)", false, "map lambda bool operand")] - #[case::filter_lambda_sub_error("filter([1, 2, 3], fn(x): x - \"str\";)", false, "filter lambda body type error")] - #[case::fold_lambda_type_error( - "fold([1, 2, 3], 0, fn(acc, x): acc - \"str\";)", - false, - "fold lambda body type error" - )] - #[case::map_lambda_valid("map([1, 2, 3], fn(x): x + 1;)", true, "map lambda valid")] - #[case::filter_lambda_valid("filter([1, 2, 3], fn(x): x > 1;)", true, "filter lambda valid")] - #[case::fold_lambda_valid("fold([1, 2, 3], 0, fn(acc, x): acc + x;)", true, "fold lambda valid")] - fn test_higher_order_lambda_type_errors( - #[case] code: &str, - #[case] should_succeed: bool, - #[case] description: &str, - ) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "{}: Code='{}' Result={:?}", - description, - code, - result - ); - } - - // Bit-Shift Operator Type Errors - - #[rstest] - #[case::shift_left_bool_rhs("3 << true", false, "shift left with bool right operand")] - #[case::shift_right_bool_rhs("8 >> true", false, "shift right with bool right operand")] - #[case::shift_left_string_rhs("3 << \"1\"", false, "shift left with string right operand")] - #[case::shift_right_string_rhs("8 >> \"2\"", false, "shift right with string right operand")] - #[case::shift_left_valid("3 << 1", true, "shift left with numbers is valid")] - #[case::shift_right_valid("8 >> 2", true, "shift right with numbers is valid")] - fn test_bitshift_op_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "{}: Code='{}' Result={:?}", - description, - code, - result - ); - } - - // Markdown Function Type Errors - - #[rstest] - #[case::to_markdown_number("to_markdown(42)", true, "to_markdown accepts any type")] - #[case::to_markdown_bool("to_markdown(true)", true, "to_markdown accepts any type")] - #[case::to_h_wrong_level("to_markdown(\"hello\") | to_h(\"one\")", false, "to_h expects number level")] - #[case::to_md_list_wrong_type("to_markdown(\"hello\") | to_md_list(\"x\")", false, "to_md_list expects number")] - #[case::to_md_table_cell_wrong_col( - "to_md_table_cell(\"hello\", \"col\", 0)", - false, - "to_md_table_cell expects number col" - )] - #[case::to_md_table_cell_wrong_row( - "to_md_table_cell(\"hello\", 0, \"row\")", - false, - "to_md_table_cell expects number row" - )] - #[case::set_check_wrong_type("to_markdown(\"- [ ] task\") | set_check(42)", false, "set_check expects bool")] - #[case::set_children_wrong_type( - "to_markdown(\"# heading\") | set_children(\"not_an_array\")", - false, - "set_children expects array" - )] - #[case::set_list_ordered_wrong_type( - "to_markdown(\"- item\") | set_list_ordered(42)", - false, - "set_list_ordered expects bool" - )] - #[case::set_code_block_lang_wrong_type( - "to_markdown(\"hello\") | set_code_block_lang(42)", - false, - "set_code_block_lang expects string" - )] - #[case::to_markdown_valid("to_markdown(\"# hello\")", true, "to_markdown with string is valid")] - #[case::to_h_valid("to_markdown(\"hello\") | to_h(1)", true, "to_h with number is valid")] - #[case::to_callout_wrong_kind_type( - "to_markdown(\"hello\") | first() | to_callout(42, \"Note\")", - false, - "to_callout expects string kind" - )] - #[case::to_callout_wrong_title_type( - "to_markdown(\"hello\") | first() | to_callout(\"note\", 42)", - false, - "to_callout expects string title" - )] - #[case::to_md_table_align_wrong_type("to_md_table_align(\"left\")", false, "to_md_table_align expects an array")] - fn test_markdown_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "{}: Code='{}' Result={:?}", - description, - code, - result - ); - } - - // Piped Wrong-Type Comprehensive Cases - - #[rstest] - #[case::number_to_explode("42 | explode", false, "number piped to explode (expects string)")] - #[case::number_to_downcase("42 | downcase", false, "number piped to downcase")] - #[case::number_to_ascii_downcase("42 | ascii_downcase", false, "number piped to ascii_downcase")] - #[case::number_to_ltrim("42 | ltrim", false, "number piped to ltrim")] - #[case::number_to_rtrim("42 | rtrim", false, "number piped to rtrim")] - #[case::bool_to_floor("true | floor", false, "bool piped to floor")] - #[case::bool_to_round("true | round", false, "bool piped to round")] - #[case::bool_to_trunc("true | trunc", false, "bool piped to trunc")] - #[case::bool_to_sort("true | sort", false, "bool piped to sort")] - #[case::string_to_flatten("\"hello\" | flatten", false, "string piped to flatten")] - #[case::string_to_sort("\"hello\" | sort", false, "string piped to sort")] - #[case::string_to_uniq("\"hello\" | uniq", false, "string piped to uniq")] - #[case::string_to_compact("\"hello\" | compact", false, "string piped to compact")] - fn test_pipe_wrong_type_comprehensive(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "{}: Code='{}' Result={:?}", - description, - code, - result - ); - } - - // Bytes builtin type signatures - - #[rstest] - // to_bytes: string -> bytes - #[case::to_bytes_string(r#"to_bytes("hello")"#, true)] - // to_bytes: [number] -> bytes - #[case::to_bytes_array("to_bytes([104, 101, 108, 108, 111])", true)] - // to_bytes: bytes -> bytes (identity) - #[case::to_bytes_bytes(r#"to_bytes("hi") | to_bytes"#, true)] - // _cbor_stringify produces bytes - #[case::cbor_stringify_produces_bytes(r#"{"k": 1} | _cbor_stringify | to_hex"#, true)] - // bytes add/concatenation - #[case::bytes_add(r#"to_bytes("ab") | add(to_bytes("cd"))"#, true)] - #[case::bytes_add_op(r#"to_bytes("ab") + to_bytes("cd")"#, true)] - // slice on bytes - #[case::bytes_slice(r#"to_bytes("hello") | slice(1, 3)"#, true)] - // reverse on bytes - #[case::bytes_reverse(r#"to_bytes("hello") | reverse"#, true)] - // len on bytes - #[case::bytes_len(r#"to_bytes("hello") | len"#, true)] - // base64 on bytes produces string - #[case::bytes_base64(r#"to_bytes("hello") | base64"#, true)] - // to_hex: bytes -> string - #[case::to_hex(r#"to_bytes("hello") | to_hex"#, true)] - // hexdump: bytes -> string - #[case::hexdump(r#"to_bytes("hello") | hexdump"#, true)] - // from_hex: string -> bytes - #[case::from_hex(r#"from_hex("deadbeef")"#, true)] - // utf8: bytes -> string - #[case::utf8(r#"to_bytes("hello") | utf8"#, true)] - // xor: (bytes, bytes) -> bytes - #[case::xor(r#"to_bytes("ab") | xor(to_bytes("cd"))"#, true)] - // sha512: string -> string - #[case::sha512_string(r#"sha512("hello")"#, true)] - // sha512: bytes -> string - #[case::sha512_bytes(r#"to_bytes("hello") | sha512"#, true)] - // bytes comparisons - #[case::bytes_gt(r#"gt(to_bytes("b"), to_bytes("a"))"#, true)] - #[case::bytes_gte(r#"gte(to_bytes("a"), to_bytes("a"))"#, true)] - #[case::bytes_lt(r#"lt(to_bytes("a"), to_bytes("b"))"#, true)] - #[case::bytes_lte(r#"lte(to_bytes("a"), to_bytes("a"))"#, true)] - // is_bytes: (a) -> bool - #[case::is_bytes(r#"to_bytes("hello") | is_bytes"#, true)] - // base64d_bytes: string -> bytes, usable where bytes are expected - #[case::base64d_bytes_xor(r#"xor(base64d_bytes("aGVsbG8="), to_bytes("world"))"#, true)] - #[case::base64urld_bytes_is_bytes(r#"base64urld_bytes("aGVsbG8") | is_bytes"#, true)] - fn test_bytes_builtin_signatures(#[case] code: &str, #[case] should_succeed: bool) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "Code: {}\nResult: {:?}", - code, - result - ); - } - - #[rstest] - // to_hex requires bytes, not string - #[case::to_hex_string(r#"to_hex("hello")"#, false, "to_hex expects bytes not string")] - // utf8 requires bytes, not string - #[case::utf8_string(r#"utf8("hello")"#, false, "utf8 expects bytes not string")] - // hexdump requires bytes, not string - #[case::hexdump_string(r#"hexdump("hello")"#, false, "hexdump expects bytes not string")] - // from_hex requires string, not bytes - #[case::from_hex_bytes(r#"to_bytes("hello") | from_hex"#, false, "from_hex expects string not bytes")] - fn test_bytes_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { - let result = check_types(code); - assert_eq!( - result.is_empty(), - should_succeed, - "{}: Code='{}' Result={:?}", - description, - code, - result - ); - } -} +mod tests; diff --git a/crates/mq-check/src/builtin/tests.rs b/crates/mq-check/src/builtin/tests.rs new file mode 100644 index 000000000..5ff89fb8b --- /dev/null +++ b/crates/mq-check/src/builtin/tests.rs @@ -0,0 +1,1311 @@ +use mq_hir::Hir; +use rstest::rstest; + +use crate::{TypeChecker, TypeError, types::Type}; + +/// Helper function to create HIR from code +fn create_hir(code: &str) -> Hir { + let mut hir = Hir::default(); + // Enable builtins to test builtin function types + hir.builtin.disabled = false; + hir.add_builtin(); // Add builtin functions to HIR + hir.add_code(None, code); + hir +} + +/// Helper function to run type checker +fn check_types(code: &str) -> Vec { + let hir = create_hir(code); + let mut checker = TypeChecker::new(); + checker.check(&hir) +} + +#[rstest] +#[case::flat(Type::array(Type::Number), Type::Number)] +#[case::nested(Type::array(Type::array(Type::array(Type::String))), Type::String)] +#[case::mixed_tuple( + Type::tuple(vec![Type::array(Type::String), Type::String]), + Type::String + )] +#[case::union_members( + Type::array(Type::union(vec![Type::Number, Type::array(Type::String)])), + Type::union(vec![Type::Number, Type::String]) + )] +fn test_flatten_result_type(#[case] arg: Type, #[case] leaf: Type) { + let signature = super::refine_signature("flatten", std::slice::from_ref(&arg)); + assert_eq!(signature, Some(Type::function(vec![arg], Type::array(leaf)))); +} + +#[test] +fn test_refine_signature_ignores_unresolved_and_other_builtins() { + assert_eq!(super::refine_signature("flatten", &[Type::Number]), None); + assert_eq!(super::refine_signature("reverse", &[Type::array(Type::Number)]), None); +} + +#[test] +fn test_registered_builtins_are_known_to_hir() { + let mut ctx = crate::infer::InferenceContext::new(); + super::register_all(&mut ctx); + let hir = create_hir(""); + let known: std::collections::HashSet<&str> = hir + .symbols() + .filter(|(_, symbol)| hir.is_builtin_symbol(symbol)) + .filter_map(|(_, symbol)| symbol.value.as_deref()) + .collect(); + let mut missing: Vec<&str> = ctx + .builtin_names() + .filter(|name| name.chars().all(|c| c.is_alphanumeric() || c == '_')) + .filter(|name| !known.contains(name)) + .collect(); + missing.sort_unstable(); + assert!( + missing.is_empty(), + "type signatures without a builtin definition: {missing:?}" + ); +} + +#[test] +fn test_public_native_builtins_have_type_signatures() { + let mut ctx = crate::infer::InferenceContext::new(); + super::register_all(&mut ctx); + let registered: std::collections::HashSet<&str> = ctx.builtin_names().collect(); + let mut missing: Vec<&str> = mq_lang::BUILTIN_FUNCTION_NAMES + .iter() + .copied() + .filter(|name| !registered.contains(name)) + .collect(); + missing.sort_unstable(); + assert!( + missing.is_empty(), + "public native builtins without a type signature: {missing:?}" + ); +} + +// Mathematical Functions + +#[rstest] +#[case::abs("abs(42)", true)] +#[case::abs_negative("abs(-10)", true)] +#[case::ceil("ceil(3.14)", true)] +#[case::floor("floor(3.14)", true)] +#[case::round("round(3.14)", true)] +#[case::trunc("trunc(3.14)", true)] +#[case::abs_string("abs(\"hello\")", false)] // Should fail: wrong type +fn test_unary_math_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::add_numbers("1 + 2")] +#[case::add_number_string("1 + \"string\"")] +#[case::add_strings("\"hello\" + \"world\"")] +#[case::add_string_number("\"value: \" + 42")] // String + number coercion +#[case::add_arrays("[1, 2] + [3, 4]")] // Array concatenation +#[case::add_mixed("1 + \"world\"")] // Number + string coercion +#[case::sub("10 - 5")] +#[case::mul("3 * 4")] +#[case::mul_array_repeat("[1, 2] * 3")] // Array repetition +#[case::div("10 / 2")] +#[case::mod_op("10 % 3")] +#[case::pow("2 ^ 8")] +fn test_arithmetic_operators(#[case] code: &str) { + let result = check_types(code); + assert!(result.is_empty(), "Code: {}\nResult: {:?}", code, result); +} + +#[rstest] +#[case::lt("5 < 10", true)] +#[case::gt("10 > 5", true)] +#[case::lte("5 <= 10", true)] +#[case::gte("10 >= 5", true)] +#[case::lt_string("\"a\" < \"b\"", true)] // String comparison +#[case::gt_string("\"z\" > \"a\"", true)] // String comparison +#[case::lt_bool("true < false", true)] // Bool comparison (false < true) +#[case::lt_mixed("\"a\" < 1", false)] // Should fail: mixed types +fn test_comparison_operators(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::eq_numbers("1 == 1", true)] +#[case::eq_strings("\"a\" == \"b\"", true)] +#[case::ne_numbers("1 != 2", true)] +#[case::ne_strings("\"a\" != \"b\"", true)] +fn test_equality_operators(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::min_numbers("min(1, 2)", true)] +#[case::max_numbers("max(1, 2)", true)] +#[case::min_strings("min(\"a\", \"b\")", true)] +#[case::max_strings("max(\"a\", \"b\")", true)] +fn test_min_max(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::and_op("true && false", true)] +#[case::or_op("true || false", true)] +#[case::not_op("!true", true)] +#[case::bang_op("!false", true)] +#[case::and_number("1 && 2", true)] // mq supports truthy/falsy semantics +fn test_logical_operators(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::nan("nan()", true)] +#[case::infinite("infinite()", true)] +#[case::is_nan("is_nan(1.0)", true)] +#[case::rand("rand()", true)] +#[case::rand_seeded("rand(42)", true)] +#[case::rand_int("rand_int(1, 10)", true)] +#[case::rand_int_seeded("rand_int(1, 10, 42)", true)] +#[case::ln("ln(2.0)", true)] +#[case::log10("log10(100)", true)] +#[case::log("log(2.0)", true)] +#[case::sqrt("sqrt(4)", true)] +#[case::exp("exp(1)", true)] +#[case::sin("sin(0)", true)] +#[case::cos("cos(0)", true)] +#[case::tan("tan(0)", true)] +#[case::ln_string("ln(\"x\")", false)] // Should fail: wrong type +#[case::sqrt_string("sqrt(\"x\")", false)] // Should fail: wrong type +#[case::sin_string("sin(\"x\")", false)] // Should fail: wrong type +fn test_special_number_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::downcase("downcase(\"HELLO\")", true)] +#[case::upcase("upcase(\"hello\")", true)] +#[case::ascii_downcase("ascii_downcase(\"HELLO\")", true)] +#[case::ascii_upcase("ascii_upcase(\"hello\")", true)] +#[case::trim("trim(\" hello \")", true)] +#[case::downcase_number("downcase(42)", false)] // Should fail: wrong type +#[case::ascii_downcase_number("ascii_downcase(42)", false)] // Should fail: wrong type +#[case::rtrim("rtrim(\" hello \")", true)] +#[case::rindex("rindex(\"hello world hello\", \"hello\")", true)] +#[case::token_count("token_count(\"hello world\", \"gpt-4\")", true)] +#[case::token_count_no_model("token_count(\"hello world\")", true)] +#[case::token_count_number("token_count(42, \"gpt-4\")", false)] // Should fail: wrong type +#[case::token_compress("to_markdown(\"# Title\\n\\nBody\") | token_compress(100)", true)] +#[case::token_compress_with_model("to_markdown(\"# Title\\n\\nBody\") | token_compress(100, \"gpt-4\")", true)] +#[case::token_compress_string("token_compress(\"not an array\", 100)", false)] // Should fail: wrong type +#[case::capture("capture(\"hello 42\", \"(?P\\\\w+)\")", true)] +#[case::is_regex_match("is_regex_match(\"hello123\", \"[0-9]+\")", true)] +#[case::is_not_regex_match("is_not_regex_match(\"hello123\", \"[0-9]+\")", true)] +#[case::is_not_regex_match_number("is_not_regex_match(42, \"[0-9]+\")", false)] // Should fail: wrong type +#[case::scan("scan(\"2024-06\", \"(\\\\d{4})-(\\\\d{2})\")", true)] +#[case::scan_number("scan(42, \"[0-9]+\")", false)] // Should fail: wrong type +#[case::base64url("base64url(\"hello\")", true)] +#[case::base64urld("base64urld(\"aGVsbG8=\")", true)] +#[case::ltrim_number("ltrim(42)", false)] +#[case::rtrim_number("rtrim(42)", false)] +#[case::html_escape("html_escape(\"hi\")", true)] +#[case::html_escape_number("html_escape(42)", false)] // Should fail: wrong type +#[case::html_unescape("html_unescape(\"<b>hi</b>\")", true)] +#[case::html_unescape_number("html_unescape(42)", false)] // Should fail: wrong type +#[case::strip_tags("strip_tags(\"hi\")", true)] +#[case::strip_tags_number("strip_tags(42)", false)] // Should fail: wrong type +#[case::sanitize_html("sanitize_html(\"\")", true)] +#[case::sanitize_html_number("sanitize_html(42)", false)] // Should fail: wrong type +#[case::markdown_escape("markdown_escape(\"**bold**\", \"text\")", true)] +#[case::markdown_escape_number("markdown_escape(42, \"text\")", false)] // Should fail: wrong type +#[case::markdown_escape_context_number("markdown_escape(\"a\", 42)", false)] // Should fail: wrong type +#[case::markdown_escape_markdown_input("to_code(\"value\", \"text\") | markdown_escape(\"text\")", true)] +fn test_string_case_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::starts_with("starts_with(\"hello\", \"he\")", true)] +#[case::ends_with("ends_with(\"hello\", \"lo\")", true)] +#[case::index("index(\"hello\", \"ll\")", true)] +#[case::rindex("rindex(\"hello\", \"l\")", true)] +#[case::indices("indices(\"hello world hello\", \"hello\")", true)] +fn test_string_search_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::replace("replace(\"hello\", \"l\", \"r\")", true)] +#[case::gsub("gsub(\"hello\", \"l\", \"r\")", true)] +#[case::split("split(\"a,b,c\", \",\")", true)] +#[case::join("join([\"a\", \"b\"], \",\")", true)] +fn test_string_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::lines("lines(\"a\\nb\\nc\")", true)] +#[case::lines_wrong_type("lines(42)", false)] +#[case::unlines("unlines([\"a\", \"b\", \"c\"])", true)] +fn test_lines_unlines_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::explode("explode(\"hello\")", true)] +#[case::implode("implode([104, 101, 108, 108, 111])", true)] +#[case::utf8bytelen("utf8bytelen(\"hello\")", true)] +fn test_string_codepoint_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::regex_match("regex_match(\"hello123\", \"[0-9]+\")", true)] +#[case::regex_escape("regex_escape(\"a.b*c?\")", true)] +#[case::regex_escape_number("regex_escape(42)", false)] // Should fail: wrong type +#[case::base64("base64(\"hello\")", true)] +#[case::base64d("base64d(\"aGVsbG8=\")", true)] +#[case::base64d_bytes("base64d_bytes(\"aGVsbG8=\")", true)] +#[case::base64urld_bytes("base64urld_bytes(\"aGVsbG8\")", true)] +#[case::url_encode("url_encode(\"hello world\")", true)] +#[case::url_decode("url_decode(\"hello%20world\")", true)] +#[case::uuid("uuid()", true)] +#[case::uuid_v4("uuid_v4()", true)] +#[case::uuid_v7("uuid_v7()", true)] +#[case::random_string("random_string(10, \"abc\")", true)] +#[case::random_string_seeded("random_string(10, \"abc\", 42)", true)] +// Should fail: wrong types. Uses non-numeric/non-string args so it can't accidentally +// satisfy the seeded ternary overload via the implicit-piped-input fallback (a bare +// `(String, Number)` pair would coincidentally match that overload's charset/seed slots). +#[case::random_string_wrong_types("random_string(true, false)", false)] +fn test_string_encoding_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Array Functions + +#[rstest] +#[case::flatten("flatten([[1, 2], [3, 4]])", true)] +#[case::reverse("reverse([1, 2, 3])", true)] +#[case::sort("sort([3, 1, 2])", true)] +#[case::uniq("uniq([1, 2, 2, 3])", true)] +#[case::compact("compact([1, none, 2])", true)] +#[case::shuffle("shuffle([1, 2, 3])", true)] +#[case::shuffle_seeded("shuffle([1, 2, 3], 42)", true)] +#[case::sample("sample([1, 2, 3], 2)", true)] +#[case::sample_seeded("sample([1, 2, 3], 2, 42)", true)] +fn test_array_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::len_array("len([1, 2, 3])", true)] +#[case::len_string("len(\"hello\")", true)] +#[case::slice("slice([1, 2, 3, 4], 1, 3)", true)] +#[case::insert("insert([1, 3], 1, 2)", true)] +fn test_array_access_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::range_two_args("range(1, 5)", true)] +#[case::range_three_args("range(1, 10, 2)", true)] +#[case::repeat("repeat(\"x\", 3)", true)] +fn test_array_creation_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Dictionary Functions + +#[rstest] +#[case::keys("keys({\"a\": 1, \"b\": 2})", true)] +#[case::values("values({\"a\": 1, \"b\": 2})", true)] +#[case::entries("entries({\"a\": 1, \"b\": 2})", true)] +#[case::has_dict("has({\"a\": 1}, \"a\")", true)] +#[case::has_array("has([1, 2, 3], 1)", true)] +fn test_dict_query_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::pick("pick({\"a\": 1, \"b\": 2, \"c\": 3}, [\"a\", \"b\"])", true)] +#[case::omit("omit({\"a\": 1, \"b\": 2, \"c\": 3}, [\"a\"])", true)] +fn test_dict_pick_omit_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::get("get({\"a\": 1}, \"a\")", true)] +#[case::get_array("get([1, 2, 3], 0)", true)] +#[case::get_generic_binary("let d = dict() | get(d, \"key\")", true)] +#[case::get_generic_ternary("let d = dict() | get(d, \"key\")[\"name\"]", true)] +#[case::set("set({\"a\": 1}, \"b\", 2)", true)] +#[case::del("del({\"a\": 1, \"b\": 2}, \"a\")", true)] +#[case::update("update({\"a\": 1}, {\"b\": 2})", true)] +#[case::from_entries("from_entries([[\"a\", 1], [\"b\", 2]])", true)] +#[case::with_entries("with_entries({\"a\": 1}, fn(pair): [pair[0], pair[1] + 1];)", true)] +fn test_dict_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Type Conversion Functions + +#[rstest] +#[case::to_number("to_number(\"42\")", true)] +#[case::to_string("to_string(42)", true)] +#[case::to_array("to_array(42)", true)] +#[case::type_of("type(42)", true)] +#[case::to_boolean_string("to_boolean(\"true\")", true)] +#[case::to_boolean_bool("to_boolean(true)", true)] +#[case::to_boolean_number("to_boolean(42)", false)] // Should fail: wrong type +fn test_type_conversion_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Date/Time Functions + +#[rstest] +#[case::now("now()", true)] +#[case::from_date("from_date(\"2024-01-01\")", true)] +#[case::to_date("to_date(1704067200000, \"%Y-%m-%d\")", true)] +#[case::gmtime("gmtime(0)", true)] +#[case::localtime("localtime(0)", true)] +#[case::mktime("mktime([2024, 0, 1, 0, 0, 0, 1, 0])", true)] +#[case::strftime("strftime(0, \"%Y-%m-%d\")", true)] +#[case::strptime("strptime(\"2024-01-01\", \"%Y-%m-%d\")", true)] +#[case::date_add("date_add([2024, 0, 1, 0, 0, 0, 1, 0], 1, \"days\")", true)] +#[case::date_diff( + "date_diff([2024, 0, 1, 0, 0, 0, 1, 0], [2024, 0, 2, 0, 0, 0, 2, 1], \"days\")", + true +)] +#[case::gmtime_string("gmtime(\"x\")", false)] // Should fail: wrong type +#[case::mktime_string("mktime(\"x\")", false)] // Should fail: wrong type +#[case::strftime_swapped("strftime(\"x\", 1)", false)] // Should fail: wrong type +#[case::strptime_swapped("strptime(1, \"%Y-%m-%d\")", false)] // Should fail: wrong type +fn test_datetime_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// I/O And Utility Functions + +#[rstest] +#[case::print("print(42)", true)] +#[case::stderr("stderr(\"error\")", true)] +#[case::input("input()", true)] +fn test_io_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// File I/O And Path Functions + +#[rstest] +#[case::read_file("read_file(\"a.md\")", true)] +#[case::read_file_bytes("read_file_bytes(\"a.md\")", true)] +#[case::open_file("open_file(\"a.md\")", true)] +#[case::open_file_number("open_file(42)", false)] +#[case::file_exists("file_exists(\"a.md\")", true)] +#[case::file_size("file_size(\"a.md\")", true)] +#[case::file_info("file_info(\"a.md\")", true)] +#[case::collection("collection(\"docs\")", true)] +#[case::collection_respect_gitignore("collection(\"docs\", true)", true)] +#[case::collection_len("len(collection(\"docs\"))", true)] +#[case::basename("basename(\"a/b.md\")", true)] +#[case::dirname("dirname(\"a/b.md\")", true)] +#[case::extname("extname(\"a/b.md\")", true)] +#[case::stem("stem(\"a/b.md\")", true)] +#[case::path_join("path_join(\"a\", \"b.md\")", true)] +#[case::glob_match("glob_match(\"*.md\", \"a.md\")", true)] +#[case::write_file_string("write_file(\"a.md\", \"content\")", true)] +#[case::write_file_bytes("write_file(\"a.md\", to_bytes(\"content\"))", true)] +#[case::http_get("http(\"get\", \"https://example.invalid\")", true)] +#[case::http_get_symbol("http(:get, \"https://example.invalid\")", true)] +#[case::http_post("http(\"post\", \"https://example.invalid\", \"{}\")", true)] +#[case::http_post_symbol("http(:post, \"https://example.invalid\", \"{}\")", true)] +#[case::http_headers( + "http(\"get\", \"https://example.invalid\", {\"Accept\": \"application/json\"})", + true +)] +#[case::http_headers_symbol("http(:get, \"https://example.invalid\", {\"Accept\": \"application/json\"})", true)] +#[case::http_body_and_headers( + "http(\"post\", \"https://example.invalid\", \"{}\", {\"Content-Type\": \"application/json\"})", + true +)] +#[case::http_body_and_headers_symbol( + "http(:post, \"https://example.invalid\", \"{}\", {\"Content-Type\": \"application/json\"})", + true +)] +#[case::read_file_number("read_file(42)", false)] // Should fail: wrong type +#[case::file_exists_number("file_exists(42)", false)] // Should fail: wrong type +#[case::file_size_number("file_size(42)", false)] // Should fail: wrong type +#[case::file_info_number("file_info(42)", false)] // Should fail: wrong type +#[case::collection_number("collection(42)", false)] // Should fail: wrong type +#[case::collection_respect_gitignore_number("collection(\"docs\", 42)", false)] // Should fail: wrong type +#[case::basename_number("basename(42)", false)] // Should fail: wrong type +#[case::path_join_number("path_join(42, \"b\")", false)] // Should fail: wrong type +#[case::glob_match_number("glob_match(42, \"a.md\")", false)] // Should fail: wrong type +#[case::write_file_number("write_file(42, \"content\")", false)] // Should fail: wrong type +#[case::http_get_number("http(\"get\", 42)", false)] // Should fail: wrong type +#[case::http_headers_number("http(\"get\", \"https://example.invalid\", 42)", false)] // Should fail: wrong type +fn test_file_io_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Complex Expressions With Builtins + +#[rstest] +#[case::chained_string_ops("upcase(trim(\" hello \"))", true)] +#[case::math_expression("abs(min(-5, -10) + max(3, 7))", true)] +#[case::array_pipeline("len(reverse(sort([3, 1, 2])))", true)] +#[case::mixed_operations("to_string(len(split(\"a,b,c\", \",\")))", true)] +fn test_complex_builtin_expressions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Pipe Type Propagation + +#[rstest] +#[case::string_to_upcase("\"hello\" | upcase", true)] +#[case::string_to_ascii_upcase("\"hello\" | ascii_upcase", true)] +#[case::string_to_trim("\" hello \" | trim", true)] +#[case::number_to_abs("-42 | abs", true)] +#[case::string_to_len("\"hello\" | len", true)] +#[case::chained_pipes("\" hello \" | trim | upcase", true)] +#[case::chained_string_to_len("\"hello\" | upcase | len", true)] +#[case::chained_split_to_len("\"hello\" | split(\",\") | len", true)] +#[case::chained_array_reverse_first("[1,2,3] | reverse | first", true)] +#[case::number_to_upcase("42 | upcase", false)] // Number piped to string function +#[case::number_to_ascii_upcase("42 | ascii_upcase", false)] // Number piped to string function +fn test_pipe_type_propagation(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Type Error Cases +#[rstest] +#[case::add_bool_number("true + 1", false)] // bool + number is invalid +#[case::add_bool_string("true + \"s\"", false)] // bool + string is invalid +#[case::sub_strings("\"a\" - \"b\"", false)] // strings cannot be subtracted +#[case::sub_string_number("\"a\" - 1", false)] // string - number is invalid +#[case::mul_string_number("\"a\" * 3", true)] // string * number is invalid +#[case::div_strings("\"a\" / \"b\"", false)] // strings cannot be divided +#[case::mod_strings("\"a\" % \"b\"", false)] // strings cannot use modulo +#[case::sub_bool_bool("true - false", false)] // booleans cannot be subtracted +#[case::mul_bool_number("true * 2", false)] // bool * number is invalid +#[case::div_bool_number("true / 2", false)] // bool / number is invalid +#[case::mod_bool_number("true % 2", false)] // bool % number is invalid +#[case::mod_number_string("10 % \"2\"", false)] // number % string is invalid +fn test_arithmetic_type_errors(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::lt_string_number("\"a\" < 1", false)] // mixed types +#[case::gt_number_string("1 > \"a\"", false)] // mixed types +#[case::lte_bool_number("true <= 1", false)] // mixed types +#[case::gte_string_bool("\"a\" >= true", false)] // mixed types +#[case::lt_number_bool("1 < true", false)] // mixed types +#[case::gt_bool_string("true > \"x\"", false)] // mixed types +#[case::lte_string_number("\"a\" <= 1", false)] // mixed types +#[case::gte_number_bool("1 >= false", false)] // mixed types +fn test_comparison_type_errors(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::not_number("!42", true)] // number is truthy/falsy โ†’ bool +#[case::not_string("!\"hello\"", true)] // string is truthy/falsy โ†’ bool +#[case::not_array("![1, 2, 3]", true)] // array is truthy/falsy โ†’ bool +fn test_logical_type_errors(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::abs_string("abs(\"not a number\")", false)] // wrong argument type +#[case::downcase_number("downcase(42)", false)] // wrong argument type +#[case::ascii_downcase_number("ascii_downcase(42)", false)] // wrong argument type +#[case::ceil_string("ceil(\"hello\")", false)] // wrong argument type +#[case::floor_bool("floor(true)", false)] // wrong argument type +#[case::len_no_args("len()", true)] // valid: root-level dynamic piped input satisfies the arg +#[case::split_numbers("split(1, 2)", false)] // wrong argument types +#[case::join_numbers("join(1, 2)", false)] // wrong argument types +#[case::starts_with_numbers("starts_with(1, 2)", false)] // wrong argument types +#[case::round_string("round(\"hello\")", false)] // wrong argument type +#[case::trunc_string("trunc(\"hello\")", false)] // wrong argument type +#[case::abs_bool("abs(true)", false)] // wrong argument type +#[case::floor_string("floor(\"hello\")", false)] // wrong argument type +#[case::ceil_bool("ceil(false)", false)] // wrong argument type +#[case::upcase_number("upcase(42)", false)] // wrong argument type +#[case::ascii_upcase_number("ascii_upcase(42)", false)] // wrong argument type +#[case::trim_number("trim(42)", false)] // wrong argument type +#[case::ltrim_bool("ltrim(true)", false)] // wrong argument type +#[case::rtrim_bool("rtrim(true)", false)] // wrong argument type +#[case::explode_number("explode(42)", false)] // expects string +#[case::implode_string("implode(\"hello\")", false)] // expects array of numbers +#[case::split_wrong_sep("split(\"hello\", 42)", false)] // separator must be string +#[case::join_wrong_sep("join([\"a\", \"b\"], 42)", false)] // separator must be string +#[case::replace_wrong_sep("replace(\"hello\", 42, \"r\")", false)] // wrong separator type +#[case::replace_too_few_args("replace(\"l\")", false)] // 1 explicit + 1 piped = 2 args, needs 3 +#[case::replace_too_many_args("replace(\"hello\", \"l\", \"r\", \"extra\")", false)] // too many args +#[case::gsub_wrong_first("gsub(42, \"l\", \"r\")", false)] // expects string first arg +#[case::starts_with_num_sep("starts_with(\"hello\", 42)", false)] // expects string prefix +#[case::ends_with_num_sep("ends_with(\"hello\", 42)", false)] // expects string suffix +#[case::flatten_string("flatten(\"hello\")", false)] // expects array +#[case::sort_string("sort(\"hello\")", false)] // expects array +#[case::uniq_string("uniq(\"hello\")", false)] // expects array +#[case::compact_string("compact(\"hello\")", false)] // expects array +#[case::reverse_number("reverse(42)", false)] // expects array or string +#[case::values_number("values(42)", false)] // expects dict +#[case::values_string("values(\"hello\")", false)] // expects dict +#[case::entries_string("entries(\"hello\")", false)] // expects dict +#[case::range_string("range(\"a\", true)", false)] // expects number or string +#[case::repeat_bool("repeat(\"x\", true)", false)] // second arg must be number +fn test_function_type_errors(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::number_to_upcase("42 | upcase", false)] // number piped to string function +#[case::number_to_trim("42 | trim", false)] // number piped to string function +#[case::string_to_abs("\"hello\" | abs", false)] // string piped to number function +#[case::string_to_ceil("\"hello\" | ceil", false)] // string piped to number function +#[case::number_to_downcase("42 | downcase", false)] // number piped to string function +#[case::number_to_ascii_downcase("42 | ascii_downcase", false)] // number piped to string function +#[case::bool_to_upcase("true | upcase", false)] // bool piped to string function +#[case::bool_to_ascii_upcase("true | ascii_upcase", false)] // bool piped to string function +#[case::bool_to_trim("true | trim", false)] // bool piped to string function +#[case::bool_to_abs("true | abs", false)] // bool piped to number function +#[case::string_to_floor("\"hello\" | floor", false)] // string piped to number function +#[case::string_to_round("\"hello\" | round", false)] // string piped to number function +#[case::string_to_trunc("\"hello\" | trunc", false)] // string piped to number function +#[case::number_to_len("42 | len", false)] // number piped to len (expects array or string) +#[case::bool_to_len("true | len", false)] // bool piped to len +#[case::number_to_sort("42 | sort", false)] // number piped to sort (expects array) +#[case::number_to_reverse("42 | reverse", false)] // number piped to reverse +#[case::number_to_flatten("42 | flatten", false)] // number piped to flatten +#[case::number_to_uniq("42 | uniq", false)] // number piped to uniq +fn test_pipe_type_errors(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +#[case::piped_join("[\"a\", \"b\"] | join(\",\")", true)] // array piped as first arg to join +#[case::piped_split("\"a,b,c\" | split(\",\")", true)] // string piped as first arg to split +#[case::piped_starts_with("\"hello\" | starts_with(\"he\")", true)] +#[case::piped_ends_with("\"hello\" | ends_with(\"lo\")", true)] +#[case::piped_index("\"hello\" | index(\"ll\")", true)] +#[case::piped_replace("\"hello\" | replace(\"l\", \"r\")", true)] +#[case::piped_gsub("\"hello\" | gsub(\"l\", \"r\")", true)] +#[case::piped_gsub_variable_arg("def slugify(s, separator = \"-\"): s | gsub(\"[^a-z0-9]+\", separator) end", true)] // regression: default param used in gsub should not produce false error +#[case::replace_sstring_args("def f(x, v): x | replace(s\"old${v}\", s\"new${v}\") end", true)] // s-string args should not expand segments as separate arguments +#[case::replace_chained_sstring( + "def f(line, prev, ver): line | replace(s\"version = \\\"${prev}\\\"\", s\"version = \\\"${ver}\\\"\") | replace(s\"old${prev}\", s\"new${ver}\") end", + true +)] // chained replace with s-string args +#[case::piped_slice("[1, 2, 3, 4] | slice(1, 3)", true)] +#[case::piped_repeat("\"x\" | repeat(3)", true)] +#[case::piped_join_wrong_type("42 | join(\",\")", false)] // number piped to join (expects array) +#[case::piped_split_wrong_type("42 | split(\",\")", false)] // number piped to split (expects string) +fn test_piped_function_calls(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Collection Functions + +#[rstest] +#[case::first_array("first([1, 2, 3])", true)] +#[case::last_array("last([1, 2, 3])", true)] +#[case::first_string("first(\"hello\")", true)] +#[case::last_string("last(\"hello\")", true)] +#[case::contains_string("contains(\"hello world\", \"world\")", true)] +#[case::contains_array("contains([1, 2, 3], 2)", true)] +#[case::contains_dict("contains({\"a\": 1, \"b\": 2}, \"a\")", true)] +#[case::in_array("in([1, 2, 3], 1)", true)] +#[case::in_string("in(\"hello world\", \"world\")", true)] +#[case::in_sub_array("in([1, 2, 3, 4], [2, 3])", true)] +#[case::in_return_plus_number("in([1, 2, 3], 1) + 1", false)] // in returns bool; bool + number is invalid +#[case::contains_return_plus_number("contains(\"hello\", \"he\") + 1", false)] // contains returns bool; bool + number is invalid +#[case::bsearch("bsearch([1, 2, 3, 4], 2)", true)] +#[case::bsearch_other_type("bsearch([1, 2, 3, 4], \"2\")", true)] // the target is never found +fn test_collection_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Higher-Order Collection Functions + +#[rstest] +#[case::map_array("map([1, 2, 3], fn(x): x + 1;)", true)] +#[case::filter_array("filter([1, 2, 3], fn(x): x > 1;)", true)] +#[case::fold_array("fold([1, 2, 3], 0, fn(acc, x): acc + x;)", true)] +#[case::sort_by_array("sort_by([1, 2, 3], fn(x): x;)", true)] +#[case::flat_map_array("flat_map([1, 2, 3], fn(x): [x];)", true)] +fn test_higher_order_collection_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// None Propagation for Higher-Order Functions + +#[rstest] +#[case::filter_none_identity("filter(None, fn(x): x;)", true)] +#[case::filter_none_array_return("filter(None, fn(x): [x[0], x[1]];)", true)] +#[case::flat_map_none("flat_map(None, fn(x): [x];)", true)] +#[case::map_none_identity("map(None, fn(x): x;)", true)] +fn test_none_propagation_higher_order(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Type Check Functions + +#[rstest] +#[case::is_none("is_none(42)", true)] +#[case::is_array("is_array([1,2,3])", true)] +#[case::is_dict("is_dict({\"a\": 1})", true)] +#[case::is_string("is_string(\"hello\")", true)] +#[case::is_number("is_number(42)", true)] +#[case::is_bool("is_bool(true)", true)] +#[case::is_empty_string("is_empty(\"\")", true)] +#[case::is_empty_array("is_empty([])", true)] +fn test_type_check_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Type filter functions (`arrays`, `booleans`, etc.) are `def`-based builtins from +// builtin.mq, not registered in this file: hir.add_builtin() loads builtin.mq's +// source into the HIR, so their types are inferred from the function bodies just +// like any other user-defined function. +#[rstest] +#[case::arrays("arrays([1, 2])", true)] +#[case::markdowns("markdowns(to_hr())", true)] +#[case::booleans("booleans(true)", true)] +#[case::numbers("numbers(42)", true)] +#[case::strings("strings(\"a\")", true)] +#[case::dicts("dicts({\"a\": 1})", true)] +#[case::nones("nones(None)", true)] +#[case::bytes_filter("bytes(to_bytes(\"a\"))", true)] +#[case::iterables_array("iterables([1, 2])", true)] +#[case::iterables_dict("iterables({\"a\": 1})", true)] +#[case::scalars("scalars(1)", true)] +fn test_type_filter_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Utility Functions + +#[rstest] +#[case::coalesce("coalesce(1, 2)", true)] +#[case::error_func("error(\"message\")", true)] +#[case::halt_func("halt(1)", true)] +#[case::all_symbols("all_symbols()", true)] +#[case::intern("intern(\"symbol\")", true)] +#[case::is_debug_mode("is_debug_mode()", true)] +#[case::breakpoint("breakpoint()", true)] +#[case::assert_func("assert(42)", true)] +#[case::assert_func_bool("assert(true)", true)] +#[case::negate_func("negate(5)", true)] +#[case::pow_func("pow(2, 8)", true)] +#[case::convert_at_operator("\"hello\" @ :text", true)] +#[case::convert_func("convert(\"hello\", :text)", true)] +fn test_utility_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Numeric Bit-Shift Functions + +#[rstest] +#[case::shift_left_numbers("shift_left(3, 1)", true)] +#[case::shift_right_numbers("shift_right(8, 2)", true)] +#[case::shift_left_wrong_second_arg("shift_left(\"hello\", \"str\")", false)] +#[case::shift_right_wrong_second_arg("shift_right(\"hello\", \"str\")", false)] +fn test_bitshift_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Markdown Type Check Functions + +#[rstest] +#[case::is_h("to_markdown(\"# hello\") | first() | is_h()", true)] +#[case::is_p("to_markdown(\"hello\") | first() | is_p()", true)] +#[case::is_code("to_markdown(\"hello\") | first() | is_code()", true)] +#[case::is_code_block("to_markdown(\"hello\") | first() | is_code_block()", true)] +#[case::is_code_inline("to_markdown(\"hello\") | first() | is_code_inline()", true)] +#[case::is_em("to_markdown(\"hello\") | first() | is_em()", true)] +#[case::is_strong("to_markdown(\"hello\") | first() | is_strong()", true)] +#[case::is_link("to_markdown(\"hello\") | first() | is_link()", true)] +#[case::is_image("to_markdown(\"hello\") | first() | is_image()", true)] +#[case::is_list("to_markdown(\"hello\") | first() | is_list()", true)] +#[case::is_list_item("to_markdown(\"hello\") | first() | is_list_item()", true)] +#[case::is_table("to_markdown(\"hello\") | first() | is_table()", true)] +#[case::is_table_row("to_markdown(\"hello\") | first() | is_table_row()", true)] +#[case::is_table_cell("to_markdown(\"hello\") | first() | is_table_cell()", true)] +#[case::is_blockquote("to_markdown(\"hello\") | first() | is_blockquote()", true)] +#[case::is_hr("to_markdown(\"hello\") | first() | is_hr()", true)] +#[case::is_html("to_markdown(\"hello\") | first() | is_html()", true)] +#[case::is_text("to_markdown(\"hello\") | first() | is_text()", true)] +#[case::is_softbreak("to_markdown(\"hello\") | first() | is_softbreak()", true)] +#[case::is_hardbreak("to_markdown(\"hello\") | first() | is_hardbreak()", true)] +#[case::is_task_list_item("to_markdown(\"hello\") | first() | is_task_list_item()", true)] +#[case::is_footnote("to_markdown(\"hello\") | first() | is_footnote()", true)] +#[case::is_footnote_ref("to_markdown(\"hello\") | first() | is_footnote_ref()", true)] +#[case::is_strikethrough("to_markdown(\"hello\") | first() | is_strikethrough()", true)] +#[case::is_math("to_markdown(\"hello\") | first() | is_math()", true)] +#[case::is_math_inline("to_markdown(\"hello\") | first() | is_math_inline()", true)] +#[case::is_toml("to_markdown(\"hello\") | first() | is_toml()", true)] +#[case::is_yaml("to_markdown(\"hello\") | first() | is_yaml()", true)] +#[case::is_h_level("to_markdown(\"# hello\") | first() | is_h_level(1)", true)] +#[case::is_h_level_wrong_type("is_h_level(42, \"str\")", false)] +#[case::is_callout("to_markdown(\"hello\") | first() | is_callout()", true)] +#[case::is_table_align("to_markdown(\"hello\") | first() | is_table_align()", true)] +fn test_markdown_type_check_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Markdown Conversion Functions + +#[rstest] +#[case::from_html("\"

Hello

\" | from_html()", true)] +#[case::to_markdown("to_markdown(\"hello\")", true)] +#[case::to_text("to_markdown(\"hello\") | first() | to_text()", true)] +#[case::to_html("to_markdown(\"hello\") | first() | to_html()", true)] +#[case::to_markdown_string("to_markdown(\"hello\") | first() | to_markdown_string()", true)] +#[case::to_code("to_markdown(\"hello\") | first() | to_code(\"rust\")", true)] +#[case::to_code_inline("to_markdown(\"hello\") | first() | to_code_inline()", true)] +#[case::to_h("to_markdown(\"hello\") | first() | to_h(1)", true)] +#[case::to_hr("to_hr()", true)] +#[case::to_image("to_image(\"alt\", \"url\", \"title\")", true)] +#[case::to_link("to_link(\"text\", \"url\", \"title\")", true)] +#[case::to_md_list("to_markdown(\"hello\") | first() | to_md_list(1)", true)] +#[case::to_md_name("to_markdown(\"hello\") | first() | to_md_name()", true)] +#[case::to_md_table_cell("to_md_table_cell(\"hello\", 0, 0)", true)] +#[case::to_md_table_row("to_markdown(\"hello\") | first() | to_md_table_row()", true)] +#[case::to_md_text("to_markdown(\"hello\") | first() | to_md_text()", true)] +#[case::to_math("to_markdown(\"hello\") | first() | to_math()", true)] +#[case::to_math_inline("to_markdown(\"hello\") | first() | to_math_inline()", true)] +#[case::to_mdx("to_mdx(\"hello\")", true)] +#[case::to_strong("to_markdown(\"hello\") | first() | to_strong()", true)] +#[case::to_em("to_markdown(\"hello\") | first() | to_em()", true)] +#[case::to_blockquote("to_markdown(\"hello\") | first() | to_blockquote()", true)] +#[case::to_delete("to_markdown(\"hello\") | first() | to_delete()", true)] +#[case::to_callout("to_markdown(\"hello\") | first() | to_callout(\"note\", \"Note\")", true)] +#[case::to_md_fragment_markdown("to_markdown(\"hello\") | first() | to_md_fragment()", true)] +#[case::to_md_fragment_array("to_md_fragment([\"a\", \"b\"])", true)] +#[case::to_md_table_align("to_md_table_align([\"left\", \"right\"])", true)] +fn test_markdown_conversion_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Markdown Manipulation Functions + +#[rstest] +#[case::attr("to_markdown(\"[link](url)\") | first() | attr(\"url\")", true)] +#[case::set_attr("to_markdown(\"[link](url)\") | first() | set_attr(\"href\", \"new\")", true)] +#[case::set_children("to_markdown(\"# heading\") | first() | set_children([\"new\"])", true)] +#[case::get_title("to_markdown(\"[link](url)\") | first() | get_title", true)] +#[case::get_url("to_markdown(\"[link](url)\") | first() | get_url", true)] +#[case::get_location("to_markdown(\"# heading\") | first() | get_location", true)] +#[case::set_check("to_markdown(\"- [ ] task\") | first() | set_check(true)", true)] +#[case::set_list_ordered("to_markdown(\"- item\") | first() | set_list_ordered(false)", true)] +#[case::set_code_block_lang("to_markdown(\"```\\ncode\\n```\") | first() | set_code_block_lang(\"rust\")", true)] +#[case::set_ref("to_markdown(\"[link][ref]\") | first() | set_ref(\"ref\")", true)] +fn test_markdown_manipulation_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Dict/Array Constructor Functions + +#[rstest] +#[case::dict_nullary("dict()", true)] +#[case::array_func("array(42)", true)] +fn test_constructor_functions(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +// Higher-Order Function Lambda Type Errors + +#[rstest] +#[case::map_lambda_sub_error("map([1, 2, 3], fn(x): x - \"str\";)", false, "map lambda body type error")] +#[case::map_lambda_bool_op("map([1, 2, 3], fn(x): x - true;)", false, "map lambda bool operand")] +#[case::filter_lambda_sub_error("filter([1, 2, 3], fn(x): x - \"str\";)", false, "filter lambda body type error")] +#[case::fold_lambda_type_error( + "fold([1, 2, 3], 0, fn(acc, x): acc - \"str\";)", + false, + "fold lambda body type error" +)] +#[case::map_lambda_valid("map([1, 2, 3], fn(x): x + 1;)", true, "map lambda valid")] +#[case::filter_lambda_valid("filter([1, 2, 3], fn(x): x > 1;)", true, "filter lambda valid")] +#[case::fold_lambda_valid("fold([1, 2, 3], 0, fn(acc, x): acc + x;)", true, "fold lambda valid")] +fn test_higher_order_lambda_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "{}: Code='{}' Result={:?}", + description, + code, + result + ); +} + +// Bit-Shift Operator Type Errors + +#[rstest] +#[case::shift_left_bool_rhs("3 << true", false, "shift left with bool right operand")] +#[case::shift_right_bool_rhs("8 >> true", false, "shift right with bool right operand")] +#[case::shift_left_string_rhs("3 << \"1\"", false, "shift left with string right operand")] +#[case::shift_right_string_rhs("8 >> \"2\"", false, "shift right with string right operand")] +#[case::shift_left_valid("3 << 1", true, "shift left with numbers is valid")] +#[case::shift_right_valid("8 >> 2", true, "shift right with numbers is valid")] +fn test_bitshift_op_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "{}: Code='{}' Result={:?}", + description, + code, + result + ); +} + +// Markdown Function Type Errors + +#[rstest] +#[case::to_markdown_number("to_markdown(42)", true, "to_markdown accepts any type")] +#[case::to_markdown_bool("to_markdown(true)", true, "to_markdown accepts any type")] +#[case::to_h_wrong_level("to_markdown(\"hello\") | to_h(\"one\")", false, "to_h expects number level")] +#[case::to_md_list_wrong_type("to_markdown(\"hello\") | to_md_list(\"x\")", false, "to_md_list expects number")] +#[case::to_md_table_cell_wrong_col( + "to_md_table_cell(\"hello\", \"col\", 0)", + false, + "to_md_table_cell expects number col" +)] +#[case::to_md_table_cell_wrong_row( + "to_md_table_cell(\"hello\", 0, \"row\")", + false, + "to_md_table_cell expects number row" +)] +#[case::set_check_wrong_type("to_markdown(\"- [ ] task\") | set_check(42)", false, "set_check expects bool")] +#[case::set_children_wrong_type( + "to_markdown(\"# heading\") | set_children(\"not_an_array\")", + false, + "set_children expects array" +)] +#[case::set_list_ordered_wrong_type( + "to_markdown(\"- item\") | set_list_ordered(42)", + false, + "set_list_ordered expects bool" +)] +#[case::set_code_block_lang_wrong_type( + "to_markdown(\"hello\") | set_code_block_lang(42)", + false, + "set_code_block_lang expects string" +)] +#[case::to_markdown_valid("to_markdown(\"# hello\")", true, "to_markdown with string is valid")] +#[case::to_h_valid("to_markdown(\"hello\") | to_h(1)", true, "to_h with number is valid")] +#[case::to_callout_wrong_kind_type( + "to_markdown(\"hello\") | first() | to_callout(42, \"Note\")", + false, + "to_callout expects string kind" +)] +#[case::to_callout_wrong_title_type( + "to_markdown(\"hello\") | first() | to_callout(\"note\", 42)", + false, + "to_callout expects string title" +)] +#[case::to_md_table_align_wrong_type("to_md_table_align(\"left\")", false, "to_md_table_align expects an array")] +fn test_markdown_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "{}: Code='{}' Result={:?}", + description, + code, + result + ); +} + +// Piped Wrong-Type Comprehensive Cases + +#[rstest] +#[case::number_to_explode("42 | explode", false, "number piped to explode (expects string)")] +#[case::number_to_downcase("42 | downcase", false, "number piped to downcase")] +#[case::number_to_ascii_downcase("42 | ascii_downcase", false, "number piped to ascii_downcase")] +#[case::number_to_ltrim("42 | ltrim", false, "number piped to ltrim")] +#[case::number_to_rtrim("42 | rtrim", false, "number piped to rtrim")] +#[case::bool_to_floor("true | floor", false, "bool piped to floor")] +#[case::bool_to_round("true | round", false, "bool piped to round")] +#[case::bool_to_trunc("true | trunc", false, "bool piped to trunc")] +#[case::bool_to_sort("true | sort", false, "bool piped to sort")] +#[case::string_to_flatten("\"hello\" | flatten", false, "string piped to flatten")] +#[case::string_to_sort("\"hello\" | sort", false, "string piped to sort")] +#[case::string_to_uniq("\"hello\" | uniq", false, "string piped to uniq")] +#[case::string_to_compact("\"hello\" | compact", false, "string piped to compact")] +fn test_pipe_wrong_type_comprehensive(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "{}: Code='{}' Result={:?}", + description, + code, + result + ); +} + +// Bytes builtin type signatures + +#[rstest] +// to_bytes: string -> bytes +#[case::to_bytes_string(r#"to_bytes("hello")"#, true)] +// to_bytes: [number] -> bytes +#[case::to_bytes_array("to_bytes([104, 101, 108, 108, 111])", true)] +// to_bytes: bytes -> bytes (identity) +#[case::to_bytes_bytes(r#"to_bytes("hi") | to_bytes"#, true)] +// _cbor_stringify produces bytes +#[case::cbor_stringify_produces_bytes(r#"{"k": 1} | _cbor_stringify | to_hex"#, true)] +// bytes add/concatenation +#[case::bytes_add(r#"to_bytes("ab") | add(to_bytes("cd"))"#, true)] +#[case::bytes_add_op(r#"to_bytes("ab") + to_bytes("cd")"#, true)] +// slice on bytes +#[case::bytes_slice(r#"to_bytes("hello") | slice(1, 3)"#, true)] +// reverse on bytes +#[case::bytes_reverse(r#"to_bytes("hello") | reverse"#, true)] +// len on bytes +#[case::bytes_len(r#"to_bytes("hello") | len"#, true)] +// base64 on bytes produces string +#[case::bytes_base64(r#"to_bytes("hello") | base64"#, true)] +// to_hex: bytes -> string +#[case::to_hex(r#"to_bytes("hello") | to_hex"#, true)] +// hexdump: bytes -> string +#[case::hexdump(r#"to_bytes("hello") | hexdump"#, true)] +// from_hex: string -> bytes +#[case::from_hex(r#"from_hex("deadbeef")"#, true)] +// utf8: bytes -> string +#[case::utf8(r#"to_bytes("hello") | utf8"#, true)] +// xor: (bytes, bytes) -> bytes +#[case::xor(r#"to_bytes("ab") | xor(to_bytes("cd"))"#, true)] +// sha512: string -> string +#[case::sha512_string(r#"sha512("hello")"#, true)] +// sha512: bytes -> string +#[case::sha512_bytes(r#"to_bytes("hello") | sha512"#, true)] +// bytes comparisons +#[case::bytes_gt(r#"gt(to_bytes("b"), to_bytes("a"))"#, true)] +#[case::bytes_gte(r#"gte(to_bytes("a"), to_bytes("a"))"#, true)] +#[case::bytes_lt(r#"lt(to_bytes("a"), to_bytes("b"))"#, true)] +#[case::bytes_lte(r#"lte(to_bytes("a"), to_bytes("a"))"#, true)] +// is_bytes: (a) -> bool +#[case::is_bytes(r#"to_bytes("hello") | is_bytes"#, true)] +// base64d_bytes: string -> bytes, usable where bytes are expected +#[case::base64d_bytes_xor(r#"xor(base64d_bytes("aGVsbG8="), to_bytes("world"))"#, true)] +#[case::base64urld_bytes_is_bytes(r#"base64urld_bytes("aGVsbG8") | is_bytes"#, true)] +fn test_bytes_builtin_signatures(#[case] code: &str, #[case] should_succeed: bool) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "Code: {}\nResult: {:?}", + code, + result + ); +} + +#[rstest] +// to_hex requires bytes, not string +#[case::to_hex_string(r#"to_hex("hello")"#, false, "to_hex expects bytes not string")] +// utf8 requires bytes, not string +#[case::utf8_string(r#"utf8("hello")"#, false, "utf8 expects bytes not string")] +// hexdump requires bytes, not string +#[case::hexdump_string(r#"hexdump("hello")"#, false, "hexdump expects bytes not string")] +// from_hex requires string, not bytes +#[case::from_hex_bytes(r#"to_bytes("hello") | from_hex"#, false, "from_hex expects string not bytes")] +fn test_bytes_type_errors(#[case] code: &str, #[case] should_succeed: bool, #[case] description: &str) { + let result = check_types(code); + assert_eq!( + result.is_empty(), + should_succeed, + "{}: Code='{}' Result={:?}", + description, + code, + result + ); +} diff --git a/crates/mq-check/src/certainty.rs b/crates/mq-check/src/certainty.rs index 4b1390290..631d48c12 100644 --- a/crates/mq-check/src/certainty.rs +++ b/crates/mq-check/src/certainty.rs @@ -3,7 +3,7 @@ //! The checker infers types flow-insensitively, so a diagnostic that rests on one of those //! approximations may be wrong for a program that runs fine. A diagnostic is //! [`Certainty::Definite`] only when every operand it is about is *exact*: a value whose type is -//! known for certain, from the rules in [`Prover`]. Everything else is [`Certainty::Uncertain`], so +//! known for certain, from the rules in `Prover`. Everything else is [`Certainty::Uncertain`], so //! a case the rules do not cover can only make a diagnostic less certain, never more. use mq_hir::{Hir, SymbolId}; diff --git a/crates/mq-check/src/exhaustiveness.rs b/crates/mq-check/src/exhaustiveness.rs index 90f0201e1..df4fa4602 100644 --- a/crates/mq-check/src/exhaustiveness.rs +++ b/crates/mq-check/src/exhaustiveness.rs @@ -268,7 +268,7 @@ fn missing_cases(ty: &Type, arms: &[ArmInfo], ctx: &mut InferenceContext) -> Opt /// Checks all match expressions in the HIR for exhaustiveness and returns a list of errors. /// -/// `children_index` is passed in to avoid rebuilding it โ€” it is already constructed +/// `children_index` is passed in to avoid rebuilding it; it is already constructed /// by `generate_constraints` and reused here to save an O(N) full-HIR scan. pub(crate) fn check_match_exhaustiveness( hir: &Hir, @@ -284,7 +284,7 @@ pub(crate) fn check_match_exhaustiveness( let children = get_children(children_index, match_id); if children.len() < 2 { - // No arms โ€” nothing to check. + // No arms: nothing to check. continue; } diff --git a/crates/mq-check/src/types.rs b/crates/mq-check/src/types.rs index 49a7718ac..b2f6de6e8 100644 --- a/crates/mq-check/src/types.rs +++ b/crates/mq-check/src/types.rs @@ -60,17 +60,17 @@ pub enum Type { /// - `{a: number, b: string}` โ†’ `Record({"a": Number, "b": String}, RowEmpty)` /// - `{a: number | r}` โ†’ `Record({"a": Number}, Var(r))` Record(BTreeMap, Box), - /// Empty row โ€” marks a closed record with no additional fields + /// Empty row: marks a closed record with no additional fields RowEmpty, /// Type variable for inference Var(TypeVarId), - /// Never type (bottom type) โ€” represents unreachable code paths. + /// Never type (bottom type): represents unreachable code paths. /// /// Produced when type narrowing eliminates all possible types from a union. /// For example, `Union(Array(String), Array(Number))` minus `is_array` predicate /// leaves no viable types, producing `Never` for the else-branch. Never, - /// Dynamic type โ€” intentionally unknown at compile time (gradual typing). + /// Dynamic type: intentionally unknown at compile time (gradual typing). /// /// Unlike `Var`, `Dynamic` never participates in unification and is always /// compatible with any type. Used for the implicit pipe input (`.`) and @@ -503,10 +503,10 @@ impl Type { // Two type variables are compatible (unification will sort them out) (Type::Var(_), Type::Var(_)) => true, - // Var vs concrete โ€” unknown; do NOT assume compatible + // Var vs concrete: unknown; do NOT assume compatible (Type::Var(_), _) | (_, Type::Var(_)) => false, - // Union types: strict โ€” any member must strictly match + // Union types are strict: any member must strictly match (Type::Union(types), other) => types.iter().any(|t| t.can_branch_unify_with(other)), (other, Type::Union(types)) => types.iter().any(|t| other.can_branch_unify_with(t)), @@ -876,7 +876,7 @@ impl fmt::Display for Type { /// Type scheme for polymorphic types (generalized types) /// /// A type scheme represents a polymorphic type by quantifying over type variables. -/// For example: forall a b. (a -> b) -> [a] -> [b] +/// For example: `forall a b. (a -> b) -> [a] -> [b]` #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypeScheme { /// Quantified type variables @@ -926,10 +926,10 @@ impl TypeScheme { impl fmt::Display for TypeScheme { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.quantified.is_empty() { - // Monomorphic type โ€” renumber for clean display + // Monomorphic type: renumber for clean display write!(f, "{}", self.ty.display_renumbered()) } else { - // Polymorphic type โ€” renumber quantified vars to 'a, 'b, ... + // Polymorphic type: renumber quantified vars to 'a, 'b, ... let mut var_map: FxHashMap = FxHashMap::default(); for (i, var) in self.quantified.iter().enumerate() { var_map.insert(*var, i); diff --git a/crates/mq-check/src/unify.rs b/crates/mq-check/src/unify.rs index c75015857..9c28107e4 100644 --- a/crates/mq-check/src/unify.rs +++ b/crates/mq-check/src/unify.rs @@ -75,7 +75,7 @@ pub fn unify( // Binding would create an infinite type (e.g. Var(a) = Tuple(Var(a))). // This arises from valid dynamic patterns like `if (is_array(x)): x else: [x]` // where type-narrowing hasn't yet constrained the then-branch to Array. - // Skip the binding silently โ€” the variable remains free/polymorphic, which is + // Skip the binding silently: the variable remains free/polymorphic, which is // acceptable for mq's dynamic type system. return; } @@ -274,7 +274,7 @@ fn unify_records( match (only1.is_empty(), only2.is_empty()) { (true, true) => { - // No unique fields โ€” just unify the row tails + // No unique fields: just unify the row tails unify(ctx, r1, r2, range, origin); } (false, true) => { @@ -584,13 +584,13 @@ mod tests { unify(&mut ctx, &union, &Type::Bool, None, &ConstraintOrigin::General); assert!(!ctx.take_errors().is_empty()); - // Unify union with another union that has different members โ€” should fail. + // Unify union with another union that has different members: should fail. // Union(Number, String) vs Union(String, Bool): Number has no match in [String, Bool]. let union2 = Type::union(vec![Type::String, Type::Bool]); unify(&mut ctx, &union, &union2, None, &ConstraintOrigin::General); assert!(!ctx.take_errors().is_empty()); - // Unify identical unions โ€” should succeed. + // Unify identical unions: should succeed. let union3 = Type::union(vec![Type::Number, Type::String]); unify(&mut ctx, &union, &union3, None, &ConstraintOrigin::General); assert!(ctx.take_errors().is_empty()); diff --git a/crates/mq-check/tests/corpus.snap b/crates/mq-check/tests/corpus.snap index cb3ce4d43..0cff07201 100644 --- a/crates/mq-check/tests/corpus.snap +++ b/crates/mq-check/tests/corpus.snap @@ -1,7 +1,8 @@ crates/mq-lang/benches/mq_benches.mq: syntax=0 type=0 unresolved=0 dynamic=0 crates/mq-lang/builtin.mq: syntax=0 type=0 unresolved=534 dynamic=16 unresolved: _json_parse, _regex_replace_matches, _sort_by_impl, abs, and, ceil, compact, del, dict, downcase, ends_with, entries, error, explode, flatten, floor, get, gsub, halt, has, http, implode, index, is_nan, is_regex_match, join, keys, len, log10, max, min, next, not, open_file, open_http, or, pow, print, range, read_bytes, read_file, read_line, regex_escape, regex_match, repeat, replace, reverse, round, scan, set, set_children, shift_left, shift_right, slice, sort, split, sqrt, starts_with, stderr, sub, to_array, to_date, to_markdown, to_md_name, to_number, to_string, to_text, trim, truncate, type, upcase -crates/mq-lang/builtin_tests.mq: syntax=0 type=4 unresolved=0 dynamic=0 +crates/mq-lang/builtin_tests.mq: syntax=0 type=5 unresolved=0 dynamic=0 + typechecker::unification_error (uncertain) typechecker::unification_error (uncertain) typechecker::unification_error (uncertain) typechecker::unification_error (uncertain) @@ -36,4 +37,4 @@ crates/mq-lang/modules/yaml.mq: syntax=0 type=0 unresolved=0 dynamic=0 crates/mq-lang/modules/yaml_test.mq: syntax=0 type=0 unresolved=0 dynamic=0 scripts/bump_version.mq: syntax=0 type=0 unresolved=0 dynamic=0 scripts/sitemap.mq: syntax=0 type=0 unresolved=0 dynamic=0 -TOTAL: type=4 unresolved=534 dynamic=16 +TOTAL: type=5 unresolved=534 dynamic=16 diff --git a/crates/mq-check/tests/error_location_test.rs b/crates/mq-check/tests/error_location_test.rs index bfeb51584..3cfd71db3 100644 --- a/crates/mq-check/tests/error_location_test.rs +++ b/crates/mq-check/tests/error_location_test.rs @@ -22,7 +22,7 @@ fn check_types(code: &str) -> Vec { #[test] fn test_heterogeneous_array_allowed() { - // mq is dynamically typed โ€” heterogeneous arrays (used as tuples) are valid + // mq is dynamically typed: heterogeneous arrays (used as tuples) are valid let code = r#"[1, 2, "string"]"#; let errors = check_types(code); assert!( diff --git a/crates/mq-crawler/src/crawler.rs b/crates/mq-crawler/src/crawler.rs index e227fb009..6f8d5666e 100644 --- a/crates/mq-crawler/src/crawler.rs +++ b/crates/mq-crawler/src/crawler.rs @@ -127,7 +127,7 @@ pub struct CrawlCheckpoint { } impl CrawlCheckpoint { - /// Loads a checkpoint previously written by [`Crawler::save_checkpoint`]. + /// Loads a checkpoint previously written by `Crawler::save_checkpoint`. pub fn load(path: &str) -> Result { let data = fs::read_to_string(path).map_err(|e| format!("Failed to read checkpoint file '{}': {}", path, e))?; serde_json::from_str(&data).map_err(|e| format!("Failed to parse checkpoint file '{}': {}", path, e)) @@ -210,27 +210,58 @@ pub struct Crawler { visited: Arc>, } +/// Settings for [`Crawler::new`]. +#[derive(Debug, Default)] +pub struct CrawlerOptions { + /// Delay between requests to the same domain, in seconds. + pub crawl_delay_secs: f64, + /// Path of a robots.txt file to use instead of fetching one. + pub custom_robots_path: Option, + /// mq query applied to each page. Defaults to `identity()`. + pub mq_query: Option, + /// Directory to write converted pages to. Pages go to stdout when unset. + pub output_path: Option, + /// Maximum number of concurrent requests. Values below 1 are treated as 1. + pub concurrency: usize, + /// Format of the crawl statistics written to stderr. + pub format: OutputFormat, + /// HTML to Markdown conversion options. + pub conversion_options: mq_markdown::ConversionOptions, + /// Maximum link depth from the start URL. Unlimited when unset. + pub depth_limit: Option, + /// Domains the crawl may visit: the start domain only when unset, any domain when empty. + pub allowed_domains: Option>, + /// Extra URLs queued at depth 0 next to the start URL. + pub additional_seed_urls: Vec, + /// Maximum number of pages to crawl. Unlimited when unset. + pub max_pages: Option, + /// File the crawl progress is periodically saved to. + pub checkpoint_path: Option, + /// Pages crawled between checkpoint saves. Values below 1 are treated as 1. + pub checkpoint_interval_pages: usize, + /// Checkpoint to continue from instead of starting at `start_url`. + pub resume_checkpoint: Option, +} + impl Crawler { - #[allow(clippy::too_many_arguments)] /// Creates a crawler for `start_url`. - pub async fn new( - http_client: HttpClient, - start_url: Url, - crawl_delay_secs: f64, - custom_robots_path: Option, - mq_query: Option, - output_path: Option, - concurrency: usize, - format: OutputFormat, - conversion_options: mq_markdown::ConversionOptions, - depth_limit: Option, - allowed_domains: Option>, - additional_seed_urls: Vec, - max_pages: Option, - checkpoint_path: Option, - checkpoint_interval_pages: usize, - resume_checkpoint: Option, - ) -> Result { + pub async fn new(http_client: HttpClient, start_url: Url, options: CrawlerOptions) -> Result { + let CrawlerOptions { + crawl_delay_secs, + custom_robots_path, + mq_query, + output_path, + concurrency, + format, + conversion_options, + depth_limit, + allowed_domains, + additional_seed_urls, + max_pages, + checkpoint_path, + checkpoint_interval_pages, + resume_checkpoint, + } = options; let initial_domain = start_url .domain() .ok_or_else(|| "Start URL has no domain".to_string())? @@ -868,20 +899,13 @@ mod tests { Crawler::new( http_client, start_url, - 0.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - depth_limit, - allowed_domains, - Vec::new(), - None, - None, - 1, - None, + CrawlerOptions { + concurrency: 1, + depth_limit, + allowed_domains, + checkpoint_interval_pages: 1, + ..Default::default() + }, ) .await .unwrap() @@ -936,20 +960,13 @@ mod tests { let crawler = Crawler::new( http_client, start_url, - 1.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - Some(2), - None, - Vec::new(), - None, - None, - 1, - None, + CrawlerOptions { + crawl_delay_secs: 1.0, + concurrency: 1, + depth_limit: Some(2), + checkpoint_interval_pages: 1, + ..Default::default() + }, ) .await .unwrap(); @@ -964,20 +981,12 @@ mod tests { let crawler = Crawler::new( http_client, start_url, - 1.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - None, - None, - Vec::new(), - None, - None, - 1, - None, + CrawlerOptions { + crawl_delay_secs: 1.0, + concurrency: 1, + checkpoint_interval_pages: 1, + ..Default::default() + }, ) .await .unwrap(); @@ -996,20 +1005,12 @@ mod tests { let crawler = Crawler::new( http_client, start_url.clone(), - 0.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - None, - None, - seed_urls.clone(), - None, - None, - 1, - None, + CrawlerOptions { + concurrency: 1, + additional_seed_urls: seed_urls.clone(), + checkpoint_interval_pages: 1, + ..Default::default() + }, ) .await .unwrap(); @@ -1058,20 +1059,12 @@ mod tests { let mut crawler = Crawler::new( http_client, start_url, - 0.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - None, - None, - Vec::new(), - Some(2), - None, - 1, - None, + CrawlerOptions { + concurrency: 1, + max_pages: Some(2), + checkpoint_interval_pages: 1, + ..Default::default() + }, ) .await .unwrap(); @@ -1094,20 +1087,12 @@ mod tests { let crawler = Crawler::new( http_client, start_url.clone(), - 0.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - None, - None, - Vec::new(), - None, - Some(checkpoint_path.clone()), - 1, - None, + CrawlerOptions { + concurrency: 1, + checkpoint_path: Some(checkpoint_path.clone()), + checkpoint_interval_pages: 1, + ..Default::default() + }, ) .await .unwrap(); @@ -1154,20 +1139,12 @@ mod tests { let crawler = Crawler::new( http_client, start_url.clone(), - 0.0, - None, - None, - None, - 1, - OutputFormat::Text, - mq_markdown::ConversionOptions::default(), - None, - None, - Vec::new(), - None, - None, - 1, - Some(checkpoint), + CrawlerOptions { + concurrency: 1, + checkpoint_interval_pages: 1, + resume_checkpoint: Some(checkpoint), + ..Default::default() + }, ) .await .unwrap(); diff --git a/crates/mq-crawler/src/http_client.rs b/crates/mq-crawler/src/http_client.rs index 6760000c6..6930ab5f8 100644 --- a/crates/mq-crawler/src/http_client.rs +++ b/crates/mq-crawler/src/http_client.rs @@ -194,7 +194,7 @@ impl HttpClient { } /// Create a headless Chrome client that launches Chrome/Chromium automatically. - /// No external WebDriver server is required โ€” only Chrome/Chromium must be installed. + /// No external WebDriver server is required; only Chrome/Chromium must be installed. /// If `chrome_path` is `None`, the system Chrome is auto-detected. /// /// Pages are fetched after the browser's `load` event fires, which includes @@ -224,7 +224,7 @@ impl HttpClient { .map_err(|e| format!("Failed to launch Chrome: {}", e))?; // Run the browser event loop in a background task. - // Errors from individual events are logged but do not stop the loop โ€” + // Errors from individual events are logged but do not stop the loop: // breaking early would drop the receiver and cause all subsequent // page operations to fail with "send failed because receiver is gone". tokio::spawn(async move { @@ -393,7 +393,7 @@ impl HttpClient { } } - // Strategy 3: fixed delay โ€” applied on top of other strategies. + // Strategy 3: fixed delay, applied on top of other strategies. if !config.fixed_delay.is_zero() { tokio::time::sleep(config.fixed_delay).await; } diff --git a/crates/mq-crawler/src/lib.rs b/crates/mq-crawler/src/lib.rs index c22fb316c..d175e357a 100644 --- a/crates/mq-crawler/src/lib.rs +++ b/crates/mq-crawler/src/lib.rs @@ -20,15 +20,16 @@ //! # Usage //! //! ```rust,ignore -//! use mq_crawler::crawler::Crawler; +//! use mq_crawler::crawler::{Crawler, CrawlerOptions}; +//! use mq_crawler::http_client::HttpClient; //! use url::Url; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { //! let start_url = Url::parse("https://your-target-site.com")?; -//! let crawler = Crawler::new(start_url, None, 10); -//! let result = crawler.crawl().await?; -//! println!("Crawled {} pages", result.pages_crawled); +//! let http_client = HttpClient::new_reqwest(30.0)?; +//! let mut crawler = Crawler::new(http_client, start_url, CrawlerOptions::default()).await?; +//! crawler.run().await?; //! Ok(()) //! } //! ``` diff --git a/crates/mq-crawler/src/main.rs b/crates/mq-crawler/src/main.rs index 4e17b229a..a72073b38 100644 --- a/crates/mq-crawler/src/main.rs +++ b/crates/mq-crawler/src/main.rs @@ -2,7 +2,7 @@ use clap::Parser; use fantoccini::wd::TimeoutConfiguration; -use mq_crawler::crawler::Crawler; +use mq_crawler::crawler::{Crawler, CrawlerOptions}; use url::Url; #[derive(Clone, Debug, Default, clap::ValueEnum)] @@ -377,25 +377,27 @@ async fn main() { match Crawler::new( client, args.url.clone(), - args.crawl_delay, - args.robots_path.clone(), - args.mq_query.clone(), - args.output, - args.concurrency, - format, - mq_markdown::ConversionOptions { - extract_scripts_as_code_blocks: args.conversion.extract_scripts_as_code_blocks, - generate_front_matter: args.conversion.generate_front_matter, - use_title_as_h1: args.conversion.use_title_as_h1, - base_url: None, + CrawlerOptions { + crawl_delay_secs: args.crawl_delay, + custom_robots_path: args.robots_path.clone(), + mq_query: args.mq_query.clone(), + output_path: args.output, + concurrency: args.concurrency, + format, + conversion_options: mq_markdown::ConversionOptions { + extract_scripts_as_code_blocks: args.conversion.extract_scripts_as_code_blocks, + generate_front_matter: args.conversion.generate_front_matter, + use_title_as_h1: args.conversion.use_title_as_h1, + base_url: None, + }, + depth_limit: args.depth, + allowed_domains: effective_allowed, + additional_seed_urls: sitemap_seed_urls, + max_pages: args.max_pages, + checkpoint_path: args.checkpoint_path, + checkpoint_interval_pages: args.checkpoint_interval_pages, + resume_checkpoint, }, - args.depth, - effective_allowed, - sitemap_seed_urls, - args.max_pages, - args.checkpoint_path, - args.checkpoint_interval_pages, - resume_checkpoint, ) .await { diff --git a/crates/mq-crawler/src/robots.rs b/crates/mq-crawler/src/robots.rs index 8571b7e09..d68db2ead 100644 --- a/crates/mq-crawler/src/robots.rs +++ b/crates/mq-crawler/src/robots.rs @@ -15,7 +15,7 @@ pub struct RobotsTxt { impl RobotsTxt { /// Fetches robots.txt for a given domain. /// If a custom_robots_path is provided and is a valid URL, it fetches from there. - /// Otherwise, it constructs the default robots.txt URL (e.g., http://example.com/robots.txt). + /// Otherwise, it constructs the default robots.txt URL (e.g., `http://example.com/robots.txt`). pub async fn fetch( http_client: &HttpClient, target_url: &Url, diff --git a/crates/mq-dap/src/adapter.rs b/crates/mq-dap/src/adapter.rs index 0c6f9e27b..57f75edc8 100644 --- a/crates/mq-dap/src/adapter.rs +++ b/crates/mq-dap/src/adapter.rs @@ -723,1360 +723,4 @@ impl MqAdapter { } #[cfg(test)] -mod tests { - use super::*; - use dap::server::Server; - use mq_lang::Shared; - use std::io::{BufReader, BufWriter, Cursor}; - use std::time::Duration; - - #[test] - fn test_adapter_new_and_default() { - let adapter = MqAdapter::new(); - assert!(adapter.debugger_message_rx.is_some()); - assert!(adapter.debugger_message_tx.is_some()); - assert!(adapter.dap_command_tx.is_some()); - assert!(adapter.query_file.is_none()); - assert!(adapter.current_debug_context.is_none()); - - let adapter_default = MqAdapter::default(); - assert!(adapter_default.debugger_message_rx.is_some()); - } - - #[test] - fn test_get_local_variables_from_context_empty() { - let adapter = MqAdapter::new(); - let variables = adapter.get_local_variables_from_context(); - assert!(variables.is_empty()); - } - - #[test] - fn test_get_global_variables_from_context_empty() { - let adapter = MqAdapter::new(); - let variables = adapter.get_global_variables_from_context(); - assert!(variables.is_empty()); - } - - #[test] - fn test_get_source_with_query_file() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/path/to/test.mq".to_string()); - - let source = adapter.get_source(); - assert!(source.is_some()); - let source = source.unwrap(); - assert_eq!(source.name, Some("test.mq".to_string())); - assert_eq!(source.path, Some("/path/to/test.mq".to_string())); - } - - #[test] - fn test_get_source_without_query_file() { - let adapter = MqAdapter::new(); - let source = adapter.get_source(); - assert!(source.is_none()); - } - - #[test] - fn test_get_source_file_name_with_query_file() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/path/to/test.mq".to_string()); - - let name = adapter.get_source_file_name(Some(mq_lang::Module::TOP_LEVEL_MODULE_ID)); - assert_eq!(name, "test.mq"); - } - - #[test] - fn test_get_source_file_name_without_query_file() { - let adapter = MqAdapter::new(); - let name = adapter.get_source_file_name(Some(mq_lang::Module::TOP_LEVEL_MODULE_ID)); - assert_eq!(name, "unknown"); - } - - #[test] - fn test_send_debugger_command() { - let adapter = MqAdapter::new(); - let result = adapter.send_debugger_command(DapCommand::Continue); - assert!(result.is_ok()); - } - - #[test] - fn test_eval_without_context() { - let mut adapter = MqAdapter::new(); - let result = adapter.eval("1 + 1"); - assert!(result.is_err()); - assert!(result.unwrap_err().to_string().contains("Current context not found")); - } - - #[test] - fn test_send_log_output() { - let adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let result = adapter.send_log_output("Test message", &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_debugger_message_terminated() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let message = DebuggerMessage::Terminated; - let result = adapter.handle_debugger_message(message, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_debugger_message_breakpoint_hit() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let context = mq_lang::DebugContext::default(); - let breakpoint = mq_lang::Breakpoint { - id: 1, - line: 1, - column: None, - enabled: true, - source: None, - ..Default::default() - }; - - let message = DebuggerMessage::BreakpointHit { - thread_id: 1, - line: 1, - context, - breakpoint, - }; - - let result = adapter.handle_debugger_message(message, &mut server); - assert!(result.is_ok()); - assert!(adapter.current_debug_context.is_some()); - } - - #[test] - fn test_handle_debugger_message_log_point_does_not_stop_execution() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let message = DebuggerMessage::LogPoint { - message: "x is 3".to_string(), - }; - - let result = adapter.handle_debugger_message(message, &mut server); - assert!(result.is_ok()); - // A logpoint must not be treated as a stop: no debug context should be recorded. - assert!(adapter.current_debug_context.is_none()); - } - - #[test] - fn test_handle_debugger_message_exception_paused() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let message = DebuggerMessage::ExceptionPaused { - thread_id: 1, - message: "boom".to_string(), - context: mq_lang::DebugContext::default(), - }; - - let result = adapter.handle_debugger_message(message, &mut server); - assert!(result.is_ok()); - assert!(adapter.current_debug_context.is_some()); - } - - #[test] - fn test_handle_debugger_message_step_completed() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let context = mq_lang::DebugContext::default(); - - let message = DebuggerMessage::StepCompleted { - thread_id: 1, - line: 1, - context, - }; - - let result = adapter.handle_debugger_message(message, &mut server); - assert!(result.is_ok()); - assert!(adapter.current_debug_context.is_some()); - } - - #[test] - fn test_handle_request_threads() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Threads, - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_configuration_done() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::ConfigurationDone, - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_continue() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Continue(dap::requests::ContinueArguments { - thread_id: 1, - single_thread: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_next() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Next(dap::requests::NextArguments { - thread_id: 1, - single_thread: None, - granularity: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_step_in() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::StepIn(dap::requests::StepInArguments { - thread_id: 1, - single_thread: None, - target_id: None, - granularity: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_step_out() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::StepOut(dap::requests::StepOutArguments { - thread_id: 1, - single_thread: None, - granularity: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_scopes() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Scopes(dap::requests::ScopesArguments { frame_id: 0 }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_variables() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Variables(dap::requests::VariablesArguments { - variables_reference: 1, - filter: None, - start: None, - count: None, - format: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_stack_trace() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::StackTrace(dap::requests::StackTraceArguments { - thread_id: 1, - start_frame: None, - levels: None, - format: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_evaluate() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Evaluate(dap::requests::EvaluateArguments { - expression: "1 + 1".to_string(), - frame_id: None, - context: None, - format: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - /// 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. - #[test] - fn test_stopped_frame_exposes_live_bindings_to_dap() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter.engine.debugger().write().unwrap().add_breakpoint_with_options( - 2, - None, - None, - Some("x == 41".to_string()), - None, - None, - ); - - // A source line can contain more than one statement boundary. Queue enough - // continue commands for those boundaries so this test stays synchronous and - // deterministic while still exercising the normal DAP handler. - for _ in 0..16 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - let values = adapter - .engine - .eval("let x = 41 |\nx + 1", mq_lang::null_input().into_iter()) - .unwrap(); - assert_eq!(values[0], mq_lang::RuntimeValue::Number(42.into())); - - let context = adapter - .debugger_message_rx - .as_ref() - .unwrap() - .try_iter() - .find_map(|message| match message { - DebuggerMessage::BreakpointHit { context, .. } - if context.global_variables().iter().any(|variable| variable.name == "x") => - { - Some(context) - } - _ => None, - }) - .expect("the breakpoint should expose the live x binding to DAP"); - assert!( - context - .global_variables() - .iter() - .any(|variable| variable.name == "x" && variable.value == "41") - ); - - adapter.current_debug_context = Some(context); - assert_eq!( - adapter.eval("x + 1").unwrap()[0], - mq_lang::RuntimeValue::Number(42.into()) - ); - assert!( - adapter - .get_global_variables_from_context() - .iter() - .any(|variable| variable.name == "x" && variable.value == "41") - ); - } - - #[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))] - #[case::captured_global_set_variable( - ( - "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", - 3, 1, "outer", "10", "40", false, 42 - ) - )] - #[case::local_set_variable( - ( - "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", - 3, 2, "inner", "2", "41", false, 51 - ) - )] - #[case::captured_set_expression( - ( - "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", - 3, 2, "outer", "10", "40", true, 42 - ) - )] - fn test_dap_mutation_updates_paused_tarn_vm_frame( - #[case] case: (&'static str, usize, i64, &str, &str, &str, bool, i64), - ) { - let ( - code, - breakpoint_line, - variables_reference, - name, - initial_value, - updated_value, - use_set_expression, - expected, - ) = case; - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter.engine.debugger().write().unwrap().add_breakpoint_with_options( - breakpoint_line, - None, - None, - None, - None, - None, - ); - - let mut engine = adapter.engine.clone(); - let evaluation = thread::spawn(move || engine.eval(code, mq_lang::null_input().into_iter())); - let message = adapter - .debugger_message_rx() - .as_ref() - .expect("the DAP message receiver must be configured") - .recv_timeout(Duration::from_secs(5)) - .expect("the VM should stop at the configured breakpoint"); - - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - adapter.handle_debugger_message(message, &mut server).unwrap(); - assert!( - adapter - .get_global_variables_from_context() - .iter() - .chain(adapter.get_local_variables_from_context().iter()) - .any(|variable| variable.name == name && variable.value == initial_value) - ); - - let command = if use_set_expression { - Command::SetExpression(dap::requests::SetExpressionArguments { - expression: name.to_string(), - value: updated_value.to_string(), - frame_id: None, - format: None, - }) - } else { - Command::SetVariable(dap::requests::SetVariableArguments { - variables_reference, - name: name.to_string(), - value: updated_value.to_string(), - format: None, - }) - }; - adapter - .handle_request(Request { seq: 1, command }, &mut server) - .unwrap(); - // The compiler can emit multiple source boundaries for the function-body line. The - // first command resumes the frame whose binding we changed; the rest keep this focused - // integration test from blocking on a later boundary at the same breakpoint. - for _ in 0..16 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - assert_eq!( - evaluation.join().unwrap().unwrap()[0], - mq_lang::RuntimeValue::Number(expected.into()) - ); - assert!( - !adapter.current_debug_context.as_ref().unwrap().set_vm_variable( - name, - mq_lang::RuntimeValue::Number(99.into()), - false - ), - "a context from a resumed VM frame must reject stale writes" - ); - } - - /// A stopped nested frame keeps parameters in LOCAL and captures in GLOBAL, matching - /// the scope split presented by the tree-walking debugger. - #[test] - fn test_stopped_nested_frame_preserves_dap_scope_split() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter.engine.debugger().write().unwrap().add_breakpoint_with_options( - 3, - None, - None, - Some("inner == 2 && outer == 10".to_string()), - None, - None, - ); - - for _ in 0..16 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - let values = adapter - .engine - .eval( - "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", - mq_lang::null_input().into_iter(), - ) - .unwrap(); - assert_eq!(values[0], mq_lang::RuntimeValue::Number(12.into())); - - let context = adapter - .debugger_message_rx - .as_ref() - .unwrap() - .try_iter() - .find_map(|message| match message { - DebuggerMessage::BreakpointHit { context, .. } - if context - .local_variables() - .iter() - .any(|variable| variable.name == "inner") => - { - Some(context) - } - _ => None, - }) - .expect("the nested frame should produce a DAP breakpoint message"); - adapter.current_debug_context = Some(context); - - assert!( - adapter - .get_local_variables_from_context() - .iter() - .any(|variable| variable.name == "inner" && variable.value == "2") - ); - let global_variables = adapter.get_global_variables_from_context(); - assert!( - global_variables - .iter() - .any(|variable| variable.name == "outer" && variable.value == "10"), - "expected captured outer in GLOBAL scope, got {global_variables:?}" - ); - } - - fn find_breakpoint_context_with_local(adapter: &MqAdapter, local_name: &str) -> mq_lang::DebugContext { - adapter - .debugger_message_rx - .as_ref() - .unwrap() - .try_iter() - .find_map(|message| match message { - DebuggerMessage::BreakpointHit { context, .. } - if context.local_variables().iter().any(|v| v.name == local_name) => - { - Some(context) - } - _ => None, - }) - .unwrap_or_else(|| panic!("no breakpoint hit exposed local `{local_name}`")) - } - - #[test] - fn test_build_stack_frames_without_context() { - let adapter = MqAdapter::new(); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 1); - assert_eq!(frames[0].id, 0); - assert_eq!(frames[0].name, "unknown"); - assert_eq!(frames[0].line, 1); - assert_eq!(frames[0].column, 1); - } - - #[test] - fn test_build_stack_frames_with_context_and_empty_call_stack() { - let mut adapter = MqAdapter::new(); - let mut context = mq_lang::DebugContext::default(); - context.current_node = Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("top_level"), Default::default()), - token_id: 0u32.into(), - }); - context.range = mq_lang::Range { - start: mq_lang::Position::new(9, 2), - end: mq_lang::Position::new(9, 3), - }; - context.module_id = mq_lang::ModuleId::new(0); - adapter.current_debug_context = Some(context); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 1); - assert_eq!(frames[0].id, 0); - assert_eq!(frames[0].name, "top_level() (unknown:9)"); - assert_eq!(frames[0].line, 9); - assert_eq!(frames[0].column, 2); - } - - #[test] - fn test_build_stack_frames_with_single_call_stack_frame_uses_current_token_for_location() { - let mut adapter = MqAdapter::new(); - let mut context = mq_lang::DebugContext::default(); - context.range = mq_lang::Range { - start: mq_lang::Position::new(4, 7), - end: mq_lang::Position::new(4, 8), - }; - context.module_id = mq_lang::ModuleId::new(0); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("callee"), Default::default()), - token_id: 0u32.into(), - })); - adapter.current_debug_context = Some(context); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 1); - assert_eq!(frames[0].id, 1); - assert_eq!(frames[0].name, "callee() (unknown:4)"); - assert_eq!(frames[0].line, 4); - assert_eq!(frames[0].column, 7); - } - - #[test] - fn test_build_stack_frames_orders_frames_innermost_first() { - let mut adapter = MqAdapter::new(); - - let outer_token_id = adapter.engine.token_arena().write().unwrap().alloc(mq_lang::Token { - kind: mq_lang::TokenKind::Eof, - range: mq_lang::Range { - start: mq_lang::Position::new(10, 1), - end: mq_lang::Position::new(10, 2), - }, - module_id: mq_lang::ModuleId::new(0), - }); - let middle_token_id = adapter.engine.token_arena().write().unwrap().alloc(mq_lang::Token { - kind: mq_lang::TokenKind::Eof, - range: mq_lang::Range { - start: mq_lang::Position::new(20, 3), - end: mq_lang::Position::new(20, 4), - }, - module_id: mq_lang::ModuleId::new(0), - }); - - let mut context = mq_lang::DebugContext::default(); - context.range = mq_lang::Range { - start: mq_lang::Position::new(30, 5), - end: mq_lang::Position::new(30, 6), - }; - context.module_id = mq_lang::ModuleId::new(0); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("outer"), Default::default()), - token_id: outer_token_id, - })); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("middle"), Default::default()), - token_id: middle_token_id, - })); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("inner"), Default::default()), - token_id: 0u32.into(), - })); - adapter.current_debug_context = Some(context); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 3); - - assert_eq!(frames[0].id, 1); - assert_eq!(frames[0].name, "inner() (unknown:30)"); - assert_eq!(frames[0].line, 30); - assert_eq!(frames[0].column, 5); - - assert_eq!(frames[1].id, 2); - assert_eq!(frames[1].name, "middle() (unknown:20)"); - assert_eq!(frames[1].line, 20); - assert_eq!(frames[1].column, 3); - - assert_eq!(frames[2].id, 3); - assert_eq!(frames[2].name, "outer() (unknown:10)"); - assert_eq!(frames[2].line, 10); - assert_eq!(frames[2].column, 1); - } - - #[test] - fn test_build_stack_frames_uses_query_file_name_when_set() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/tmp/query.mq".to_string()); - let mut context = mq_lang::DebugContext::default(); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("callee"), Default::default()), - token_id: 0u32.into(), - })); - adapter.current_debug_context = Some(context); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 1); - assert!(frames[0].name.contains("query.mq")); - assert_eq!( - frames[0].source.as_ref().unwrap().path.as_deref(), - Some("/tmp/query.mq") - ); - } - - #[test] - fn test_build_stack_frames_current_location_in_an_imported_module_does_not_use_the_query_file_source() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/tmp/query.mq".to_string()); - let mut context = mq_lang::DebugContext::default(); - context.range = mq_lang::Range { - start: mq_lang::Position::new(12, 1), - end: mq_lang::Position::new(12, 2), - }; - context.module_id = mq_lang::ModuleId::new(1); - adapter.current_debug_context = Some(context); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 1); - assert_ne!( - frames[0].source.as_ref().unwrap().path.as_deref(), - Some("/tmp/query.mq"), - "a frame paused inside an imported module must not report the query file as its source" - ); - } - - #[test] - fn test_build_stack_frames_outer_call_stack_entry_from_an_imported_module_does_not_use_the_query_file_source() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/tmp/query.mq".to_string()); - - let outer_token_id = adapter.engine.token_arena().write().unwrap().alloc(mq_lang::Token { - kind: mq_lang::TokenKind::Eof, - range: mq_lang::Range { - start: mq_lang::Position::new(12, 1), - end: mq_lang::Position::new(12, 2), - }, - module_id: mq_lang::ModuleId::new(1), - }); - - let mut context = mq_lang::DebugContext::default(); - context.range = mq_lang::Range { - start: mq_lang::Position::new(3, 1), - end: mq_lang::Position::new(3, 2), - }; - context.module_id = mq_lang::ModuleId::new(0); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("helper"), Default::default()), - token_id: outer_token_id, - })); - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("inner"), Default::default()), - token_id: 0u32.into(), - })); - adapter.current_debug_context = Some(context); - - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 2); - assert_ne!( - frames[1].source.as_ref().unwrap().path.as_deref(), - Some("/tmp/query.mq"), - "an outer call-stack frame belonging to an imported module must not report the query file as its source" - ); - } - - #[test] - fn test_stack_trace_reflects_single_level_call() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter - .engine - .debugger() - .write() - .unwrap() - .add_breakpoint_with_options(2, None, None, None, None, None); - for _ in 0..16 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - let code = "let f = fn(x):\n x + 1; |\nf(1)"; - let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); - assert_eq!(values[0], mq_lang::RuntimeValue::Number(2.into())); - - adapter.current_debug_context = Some(find_breakpoint_context_with_local(&adapter, "x")); - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 1); - assert_eq!(frames[0].id, 1); - assert_eq!(frames[0].name, "f(1) (unknown:2)"); - assert_eq!(frames[0].line, 2); - } - - #[test] - fn test_stack_trace_reflects_two_level_nested_call() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter - .engine - .debugger() - .write() - .unwrap() - .add_breakpoint_with_options(2, None, None, None, None, None); - for _ in 0..16 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - let code = "let g = fn(y):\n y + 1; |\nlet f = fn(x):\n g(x) + 1; |\nf(1)"; - let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); - assert_eq!(values[0], mq_lang::RuntimeValue::Number(3.into())); - - adapter.current_debug_context = Some(find_breakpoint_context_with_local(&adapter, "y")); - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 2); - assert_eq!(frames[0].id, 1); - assert_eq!(frames[0].name, "g(x) (unknown:2)"); - assert_eq!(frames[0].line, 2); - assert_eq!(frames[1].id, 2); - assert_eq!(frames[1].name, "f(1) (unknown:5)"); - assert_eq!(frames[1].line, 5); - } - - #[test] - fn test_stack_trace_reflects_three_level_nested_call() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter - .engine - .debugger() - .write() - .unwrap() - .add_breakpoint_with_options(2, None, None, None, None, None); - for _ in 0..32 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - let code = "let h = fn(z):\n z + 1; |\nlet g = fn(y):\n h(y) + 1; |\nlet f = fn(x):\n g(x) + 1; |\nf(1)"; - let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); - assert_eq!(values[0], mq_lang::RuntimeValue::Number(4.into())); - - adapter.current_debug_context = Some(find_breakpoint_context_with_local(&adapter, "z")); - let frames = adapter.build_stack_frames(); - - assert_eq!(frames.len(), 3); - assert_eq!(frames[0].id, 1); - assert_eq!(frames[0].name, "h(y) (unknown:2)"); - assert_eq!(frames[0].line, 2); - assert_eq!(frames[1].id, 2); - assert_eq!(frames[1].name, "g(x) (unknown:6)"); - assert_eq!(frames[1].line, 6); - assert_eq!(frames[2].id, 3); - assert_eq!(frames[2].name, "f(1) (unknown:7)"); - assert_eq!(frames[2].line, 7); - } - - #[test] - fn test_stack_trace_call_stack_does_not_grow_across_coroutine_resumes() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - adapter.engine.debugger().write().unwrap().add_breakpoint(2, None, None); - for _ in 0..32 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - let code = "def g():\nlet inc = fn(n): n + 1; |\nyield: inc(1) |\nyield: inc(2);\n|\nlet s = g() |\nlet first = next(s) |\nlet second = next(s) |\n[get(first, \"value\"), get(second, \"value\")]"; - let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); - assert_eq!( - values[0], - mq_lang::RuntimeValue::Array(Shared::new(vec![ - mq_lang::RuntimeValue::Number(2.into()), - mq_lang::RuntimeValue::Number(3.into()), - ])) - ); - - let call_stack_lengths: Vec = adapter - .debugger_message_rx - .as_ref() - .unwrap() - .try_iter() - .filter_map(|message| match message { - DebuggerMessage::BreakpointHit { context, .. } - if context.local_variables().iter().any(|v| v.name == "n") => - { - Some(context.call_stack.len()) - } - _ => None, - }) - .collect(); - - assert_eq!( - call_stack_lengths.len(), - 2, - "expected a breakpoint hit for each generator resume" - ); - assert!( - call_stack_lengths.iter().all(|&len| len == 1), - "each pause inside inc() should show exactly one call-stack frame regardless of resume count, got {call_stack_lengths:?}" - ); - } - - #[test] - fn test_handle_request_set_variable() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::SetVariable(dap::requests::SetVariableArguments { - variables_reference: 1, - name: "test_var".to_string(), - value: "42".to_string(), - format: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_err()); - } - - #[test] - fn test_handle_request_disconnect() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Disconnect(dap::requests::DisconnectArguments { - restart: None, - terminate_debuggee: None, - suspend_debuggee: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_err()); // Should return error to indicate shutdown - } - - #[test] - fn test_handle_request_set_breakpoints() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/path/to/test.mq".to_string()); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let source = dap::types::Source { - name: Some("test.mq".to_string()), - path: Some("/path/to/test.mq".to_string()), - adapter_data: None, - source_reference: None, - presentation_hint: None, - origin: None, - checksums: None, - sources: None, - }; - - let breakpoints = vec![ - dap::types::SourceBreakpoint { - line: 10, - column: Some(5), - condition: None, - hit_condition: None, - log_message: None, - }, - dap::types::SourceBreakpoint { - line: 20, - column: None, - condition: None, - hit_condition: None, - log_message: None, - }, - ]; - - #[allow(deprecated)] - let req = Request { - seq: 1, - command: Command::SetBreakpoints(dap::requests::SetBreakpointsArguments { - source, - breakpoints: Some(breakpoints), - lines: None, - source_modified: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_handle_request_set_breakpoints_forwards_condition_hit_condition_and_log_message() { - let mut adapter = MqAdapter::new(); - adapter.query_file = Some("/path/to/test.mq".to_string()); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let source = dap::types::Source { - name: Some("test.mq".to_string()), - path: Some("/path/to/test.mq".to_string()), - adapter_data: None, - source_reference: None, - presentation_hint: None, - origin: None, - checksums: None, - sources: None, - }; - - let breakpoints = vec![dap::types::SourceBreakpoint { - line: 10, - column: None, - condition: Some("x > 1".to_string()), - hit_condition: Some(">= 2".to_string()), - log_message: Some("x is {x}".to_string()), - }]; - - #[allow(deprecated)] - let req = Request { - seq: 1, - command: Command::SetBreakpoints(dap::requests::SetBreakpointsArguments { - source, - breakpoints: Some(breakpoints), - lines: None, - source_modified: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - - let debugger = adapter.engine.debugger(); - let debugger = debugger.read().unwrap(); - let stored = debugger.list_breakpoints(); - assert_eq!(stored.len(), 1); - assert_eq!(stored[0].condition.as_deref(), Some("x > 1")); - assert_eq!(stored[0].hit_condition.as_deref(), Some(">= 2")); - assert_eq!(stored[0].log_message.as_deref(), Some("x is {x}")); - } - - #[test] - fn test_handle_request_set_exception_breakpoints_enables_uncaught_filter() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::SetExceptionBreakpoints(dap::requests::SetExceptionBreakpointsArguments { - filters: vec![UNCAUGHT_EXCEPTIONS_FILTER.to_string()], - filter_options: None, - exception_options: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - assert!(adapter.exception_breakpoints_enabled.load(SeqCst)); - } - - #[test] - fn test_handle_request_set_exception_breakpoints_disables_when_filter_absent() { - let mut adapter = MqAdapter::new(); - adapter.exception_breakpoints_enabled.store(true, SeqCst); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::SetExceptionBreakpoints(dap::requests::SetExceptionBreakpointsArguments { - filters: vec![], - filter_options: None, - exception_options: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - assert!(!adapter.exception_breakpoints_enabled.load(SeqCst)); - } - - #[test] - fn test_handle_request_set_expression() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::SetExpression(dap::requests::SetExpressionArguments { - expression: "test_var".to_string(), - value: "42".to_string(), - frame_id: None, - format: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - // No active debug context exists, so either backend must reject this request rather - // than treating it as an unknown command. - assert!(result.is_err()); - assert!(!matches!( - result.unwrap_err().downcast_ref::(), - Some(MqAdapterError::UnhandledCommand(_)) - )); - } - - #[test] - fn test_handle_request_unhandled_command() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let req = Request { - seq: 1, - command: Command::Initialize(dap::requests::InitializeArguments { - client_id: None, - client_name: None, - adapter_id: "test".to_string(), - locale: None, - lines_start_at1: None, - columns_start_at1: None, - path_format: None, - supports_variable_type: None, - supports_variable_paging: None, - supports_run_in_terminal_request: None, - supports_memory_references: None, - supports_progress_reporting: None, - supports_invalidated_event: None, - supports_memory_event: None, - supports_args_can_be_interpreted_by_shell: None, - supports_start_debugging_request: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_err()); // Should fail with UnhandledCommand error - } - - #[test] - fn test_handle_request_launch_success() { - let mut adapter = MqAdapter::new(); - let input = BufReader::new(Cursor::new(Vec::new())); - let output = BufWriter::new(Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let launch_args = LaunchArgs { - query_file: "/tmp/test_query.mq".to_string(), - input_file: None, - }; - - let mut additional_data = serde_json::Map::new(); - let value = serde_json::to_value(&launch_args).unwrap(); - if let serde_json::Value::Object(map) = value { - for (k, v) in map { - additional_data.insert(k, v); - } - } - - let req = Request { - seq: 1, - command: Command::Launch(dap::requests::LaunchRequestArguments { - no_debug: None, - restart_data: None, - additional_data: Some(serde_json::Value::Object(additional_data)), - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - assert_eq!(adapter.query_file, Some("/tmp/test_query.mq".to_string())); - } - - #[test] - fn test_handle_request_stack_trace_with_context() { - let mut adapter = MqAdapter::new(); - let input = std::io::BufReader::new(std::io::Cursor::new(Vec::new())); - let output = std::io::BufWriter::new(std::io::Cursor::new(Vec::new())); - let mut server = Server::new(input, output); - - let mut context = mq_lang::DebugContext::default(); - - context.call_stack.push(Shared::new(mq_lang::AstNode { - expr: mq_lang::AstExpr::Literal(mq_lang::AstLiteral::Number(42.into())), - token_id: 0u32.into(), - })); - adapter.current_debug_context = Some(context); - - let req = Request { - seq: 1, - command: Command::StackTrace(dap::requests::StackTraceArguments { - thread_id: 1, - start_frame: None, - levels: None, - format: None, - }), - }; - - let result = adapter.handle_request(req, &mut server); - assert!(result.is_ok()); - } - - #[test] - fn test_breakpoint_inside_coroutine_body_hits_on_each_resume() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - // line 2: `yield: 1 |`, line 3: `yield: 2;` - adapter.engine.debugger().write().unwrap().add_breakpoint(2, None, None); - adapter.engine.debugger().write().unwrap().add_breakpoint(3, None, None); - - for _ in 0..32 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - let code = "def g():\nyield: 1 |\nyield: 2;\n|\nlet s = g() |\nlet first = next(s) |\nlet second = next(s) |\n[get(first, \"value\"), get(second, \"value\")]"; - let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); - assert_eq!( - values[0], - mq_lang::RuntimeValue::Array(Shared::new(vec![ - mq_lang::RuntimeValue::Number(1.into()), - mq_lang::RuntimeValue::Number(2.into()), - ])) - ); - - let hit_lines = adapter - .debugger_message_rx - .as_ref() - .unwrap() - .try_iter() - .filter_map(|message| match message { - DebuggerMessage::BreakpointHit { line, .. } => Some(line), - _ => None, - }) - .collect::>(); - - // Both yields must be observed, in order, though each may be reported more than once. - let mut distinct_in_order = hit_lines.clone(); - distinct_in_order.dedup(); - assert_eq!( - distinct_in_order, - vec![2, 3], - "expected a breakpoint hit on each yield, in resume order; got {hit_lines:?}" - ); - } - - #[test] - fn test_variables_inside_resumed_coroutine_see_the_sent_value() { - let mut adapter = MqAdapter::new(); - adapter.engine.debugger().write().unwrap().activate(); - // line 3: `yield: value;`, reached only after `send(s, 99)` resumes the coroutine and - // binds `value` from the first `yield`. - adapter.engine.debugger().write().unwrap().add_breakpoint(3, None, None); - for _ in 0..32 { - adapter.send_debugger_command(DapCommand::Continue).unwrap(); - } - - let code = - "def g():\nlet value = yield: 1 |\nyield: value;\n|\nlet s = g() |\nnext(s) |\nget(send(s, 99), \"value\")"; - let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); - assert_eq!(values[0], mq_lang::RuntimeValue::Number(99.into())); - - let context = adapter - .debugger_message_rx - .as_ref() - .unwrap() - .try_iter() - .find_map(|message| match message { - DebuggerMessage::BreakpointHit { line: 3, context, .. } => Some(context), - _ => None, - }) - .expect("the coroutine should stop at the breakpoint on `yield: value;`"); - - assert!( - context - .local_variables() - .iter() - .any(|variable| variable.name == "value" && variable.value == "99"), - "expected the coroutine's own `value` local (from send()) to be visible while paused, got {:?}", - context.local_variables() - ); - } -} +mod tests; diff --git a/crates/mq-dap/src/adapter/tests.rs b/crates/mq-dap/src/adapter/tests.rs new file mode 100644 index 000000000..840eeb5e1 --- /dev/null +++ b/crates/mq-dap/src/adapter/tests.rs @@ -0,0 +1,1347 @@ +use super::*; +use dap::server::Server; +use mq_lang::Shared; +use std::io::{BufReader, BufWriter, Cursor}; +use std::time::Duration; + +#[test] +fn test_adapter_new_and_default() { + let adapter = MqAdapter::new(); + assert!(adapter.debugger_message_rx.is_some()); + assert!(adapter.debugger_message_tx.is_some()); + assert!(adapter.dap_command_tx.is_some()); + assert!(adapter.query_file.is_none()); + assert!(adapter.current_debug_context.is_none()); + + let adapter_default = MqAdapter::default(); + assert!(adapter_default.debugger_message_rx.is_some()); +} + +#[test] +fn test_get_local_variables_from_context_empty() { + let adapter = MqAdapter::new(); + let variables = adapter.get_local_variables_from_context(); + assert!(variables.is_empty()); +} + +#[test] +fn test_get_global_variables_from_context_empty() { + let adapter = MqAdapter::new(); + let variables = adapter.get_global_variables_from_context(); + assert!(variables.is_empty()); +} + +#[test] +fn test_get_source_with_query_file() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/path/to/test.mq".to_string()); + + let source = adapter.get_source(); + assert!(source.is_some()); + let source = source.unwrap(); + assert_eq!(source.name, Some("test.mq".to_string())); + assert_eq!(source.path, Some("/path/to/test.mq".to_string())); +} + +#[test] +fn test_get_source_without_query_file() { + let adapter = MqAdapter::new(); + let source = adapter.get_source(); + assert!(source.is_none()); +} + +#[test] +fn test_get_source_file_name_with_query_file() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/path/to/test.mq".to_string()); + + let name = adapter.get_source_file_name(Some(mq_lang::Module::TOP_LEVEL_MODULE_ID)); + assert_eq!(name, "test.mq"); +} + +#[test] +fn test_get_source_file_name_without_query_file() { + let adapter = MqAdapter::new(); + let name = adapter.get_source_file_name(Some(mq_lang::Module::TOP_LEVEL_MODULE_ID)); + assert_eq!(name, "unknown"); +} + +#[test] +fn test_send_debugger_command() { + let adapter = MqAdapter::new(); + let result = adapter.send_debugger_command(DapCommand::Continue); + assert!(result.is_ok()); +} + +#[test] +fn test_eval_without_context() { + let mut adapter = MqAdapter::new(); + let result = adapter.eval("1 + 1"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Current context not found")); +} + +#[test] +fn test_send_log_output() { + let adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let result = adapter.send_log_output("Test message", &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_debugger_message_terminated() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let message = DebuggerMessage::Terminated; + let result = adapter.handle_debugger_message(message, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_debugger_message_breakpoint_hit() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let context = mq_lang::DebugContext::default(); + let breakpoint = mq_lang::Breakpoint { + id: 1, + line: 1, + column: None, + enabled: true, + source: None, + ..Default::default() + }; + + let message = DebuggerMessage::BreakpointHit { + thread_id: 1, + line: 1, + context, + breakpoint, + }; + + let result = adapter.handle_debugger_message(message, &mut server); + assert!(result.is_ok()); + assert!(adapter.current_debug_context.is_some()); +} + +#[test] +fn test_handle_debugger_message_log_point_does_not_stop_execution() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let message = DebuggerMessage::LogPoint { + message: "x is 3".to_string(), + }; + + let result = adapter.handle_debugger_message(message, &mut server); + assert!(result.is_ok()); + // A logpoint must not be treated as a stop: no debug context should be recorded. + assert!(adapter.current_debug_context.is_none()); +} + +#[test] +fn test_handle_debugger_message_exception_paused() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let message = DebuggerMessage::ExceptionPaused { + thread_id: 1, + message: "boom".to_string(), + context: mq_lang::DebugContext::default(), + }; + + let result = adapter.handle_debugger_message(message, &mut server); + assert!(result.is_ok()); + assert!(adapter.current_debug_context.is_some()); +} + +#[test] +fn test_handle_debugger_message_step_completed() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let context = mq_lang::DebugContext::default(); + + let message = DebuggerMessage::StepCompleted { + thread_id: 1, + line: 1, + context, + }; + + let result = adapter.handle_debugger_message(message, &mut server); + assert!(result.is_ok()); + assert!(adapter.current_debug_context.is_some()); +} + +#[test] +fn test_handle_request_threads() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Threads, + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_configuration_done() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::ConfigurationDone, + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_continue() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Continue(dap::requests::ContinueArguments { + thread_id: 1, + single_thread: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_next() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Next(dap::requests::NextArguments { + thread_id: 1, + single_thread: None, + granularity: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_step_in() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::StepIn(dap::requests::StepInArguments { + thread_id: 1, + single_thread: None, + target_id: None, + granularity: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_step_out() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::StepOut(dap::requests::StepOutArguments { + thread_id: 1, + single_thread: None, + granularity: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_scopes() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Scopes(dap::requests::ScopesArguments { frame_id: 0 }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_variables() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Variables(dap::requests::VariablesArguments { + variables_reference: 1, + filter: None, + start: None, + count: None, + format: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_stack_trace() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::StackTrace(dap::requests::StackTraceArguments { + thread_id: 1, + start_frame: None, + levels: None, + format: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_evaluate() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Evaluate(dap::requests::EvaluateArguments { + expression: "1 + 1".to_string(), + frame_id: None, + context: None, + format: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +/// 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. +#[test] +fn test_stopped_frame_exposes_live_bindings_to_dap() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter.engine.debugger().write().unwrap().add_breakpoint_with_options( + 2, + None, + None, + Some("x == 41".to_string()), + None, + None, + ); + + // A source line can contain more than one statement boundary. Queue enough + // continue commands for those boundaries so this test stays synchronous and + // deterministic while still exercising the normal DAP handler. + for _ in 0..16 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + let values = adapter + .engine + .eval("let x = 41 |\nx + 1", mq_lang::null_input().into_iter()) + .unwrap(); + assert_eq!(values[0], mq_lang::RuntimeValue::Number(42.into())); + + let context = adapter + .debugger_message_rx + .as_ref() + .unwrap() + .try_iter() + .find_map(|message| match message { + DebuggerMessage::BreakpointHit { context, .. } + if context.global_variables().iter().any(|variable| variable.name == "x") => + { + Some(context) + } + _ => None, + }) + .expect("the breakpoint should expose the live x binding to DAP"); + assert!( + context + .global_variables() + .iter() + .any(|variable| variable.name == "x" && variable.value == "41") + ); + + adapter.current_debug_context = Some(context); + assert_eq!( + adapter.eval("x + 1").unwrap()[0], + mq_lang::RuntimeValue::Number(42.into()) + ); + assert!( + adapter + .get_global_variables_from_context() + .iter() + .any(|variable| variable.name == "x" && variable.value == "41") + ); +} + +#[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))] +#[case::captured_global_set_variable( + ( + "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", + 3, 1, "outer", "10", "40", false, 42 + ) + )] +#[case::local_set_variable( + ( + "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", + 3, 2, "inner", "2", "41", false, 51 + ) + )] +#[case::captured_set_expression( + ( + "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", + 3, 2, "outer", "10", "40", true, 42 + ) + )] +fn test_dap_mutation_updates_paused_tarn_vm_frame( + #[case] case: (&'static str, usize, i64, &str, &str, &str, bool, i64), +) { + let (code, breakpoint_line, variables_reference, name, initial_value, updated_value, use_set_expression, expected) = + case; + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter.engine.debugger().write().unwrap().add_breakpoint_with_options( + breakpoint_line, + None, + None, + None, + None, + None, + ); + + let mut engine = adapter.engine.clone(); + let evaluation = thread::spawn(move || engine.eval(code, mq_lang::null_input().into_iter())); + let message = adapter + .debugger_message_rx() + .as_ref() + .expect("the DAP message receiver must be configured") + .recv_timeout(Duration::from_secs(5)) + .expect("the VM should stop at the configured breakpoint"); + + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + adapter.handle_debugger_message(message, &mut server).unwrap(); + assert!( + adapter + .get_global_variables_from_context() + .iter() + .chain(adapter.get_local_variables_from_context().iter()) + .any(|variable| variable.name == name && variable.value == initial_value) + ); + + let command = if use_set_expression { + Command::SetExpression(dap::requests::SetExpressionArguments { + expression: name.to_string(), + value: updated_value.to_string(), + frame_id: None, + format: None, + }) + } else { + Command::SetVariable(dap::requests::SetVariableArguments { + variables_reference, + name: name.to_string(), + value: updated_value.to_string(), + format: None, + }) + }; + adapter + .handle_request(Request { seq: 1, command }, &mut server) + .unwrap(); + // The compiler can emit multiple source boundaries for the function-body line. The + // first command resumes the frame whose binding we changed; the rest keep this focused + // integration test from blocking on a later boundary at the same breakpoint. + for _ in 0..16 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + assert_eq!( + evaluation.join().unwrap().unwrap()[0], + mq_lang::RuntimeValue::Number(expected.into()) + ); + assert!( + !adapter.current_debug_context.as_ref().unwrap().set_vm_variable( + name, + mq_lang::RuntimeValue::Number(99.into()), + false + ), + "a context from a resumed VM frame must reject stale writes" + ); +} + +/// A stopped nested frame keeps parameters in LOCAL and captures in GLOBAL, matching +/// the scope split presented by the tree-walking debugger. +#[test] +fn test_stopped_nested_frame_preserves_dap_scope_split() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter.engine.debugger().write().unwrap().add_breakpoint_with_options( + 3, + None, + None, + Some("inner == 2 && outer == 10".to_string()), + None, + None, + ); + + for _ in 0..16 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + let values = adapter + .engine + .eval( + "let outer = 10 |\nlet add_outer = fn(inner):\n outer + inner; |\nadd_outer(2)", + mq_lang::null_input().into_iter(), + ) + .unwrap(); + assert_eq!(values[0], mq_lang::RuntimeValue::Number(12.into())); + + let context = adapter + .debugger_message_rx + .as_ref() + .unwrap() + .try_iter() + .find_map(|message| match message { + DebuggerMessage::BreakpointHit { context, .. } + if context + .local_variables() + .iter() + .any(|variable| variable.name == "inner") => + { + Some(context) + } + _ => None, + }) + .expect("the nested frame should produce a DAP breakpoint message"); + adapter.current_debug_context = Some(context); + + assert!( + adapter + .get_local_variables_from_context() + .iter() + .any(|variable| variable.name == "inner" && variable.value == "2") + ); + let global_variables = adapter.get_global_variables_from_context(); + assert!( + global_variables + .iter() + .any(|variable| variable.name == "outer" && variable.value == "10"), + "expected captured outer in GLOBAL scope, got {global_variables:?}" + ); +} + +fn find_breakpoint_context_with_local(adapter: &MqAdapter, local_name: &str) -> mq_lang::DebugContext { + adapter + .debugger_message_rx + .as_ref() + .unwrap() + .try_iter() + .find_map(|message| match message { + DebuggerMessage::BreakpointHit { context, .. } + if context.local_variables().iter().any(|v| v.name == local_name) => + { + Some(context) + } + _ => None, + }) + .unwrap_or_else(|| panic!("no breakpoint hit exposed local `{local_name}`")) +} + +#[test] +fn test_build_stack_frames_without_context() { + let adapter = MqAdapter::new(); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].id, 0); + assert_eq!(frames[0].name, "unknown"); + assert_eq!(frames[0].line, 1); + assert_eq!(frames[0].column, 1); +} + +#[test] +fn test_build_stack_frames_with_context_and_empty_call_stack() { + let mut adapter = MqAdapter::new(); + let mut context = mq_lang::DebugContext::default(); + context.current_node = Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("top_level"), Default::default()), + token_id: 0u32.into(), + }); + context.range = mq_lang::Range { + start: mq_lang::Position::new(9, 2), + end: mq_lang::Position::new(9, 3), + }; + context.module_id = mq_lang::ModuleId::new(0); + adapter.current_debug_context = Some(context); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].id, 0); + assert_eq!(frames[0].name, "top_level() (unknown:9)"); + assert_eq!(frames[0].line, 9); + assert_eq!(frames[0].column, 2); +} + +#[test] +fn test_build_stack_frames_with_single_call_stack_frame_uses_current_token_for_location() { + let mut adapter = MqAdapter::new(); + let mut context = mq_lang::DebugContext::default(); + context.range = mq_lang::Range { + start: mq_lang::Position::new(4, 7), + end: mq_lang::Position::new(4, 8), + }; + context.module_id = mq_lang::ModuleId::new(0); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("callee"), Default::default()), + token_id: 0u32.into(), + })); + adapter.current_debug_context = Some(context); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].id, 1); + assert_eq!(frames[0].name, "callee() (unknown:4)"); + assert_eq!(frames[0].line, 4); + assert_eq!(frames[0].column, 7); +} + +#[test] +fn test_build_stack_frames_orders_frames_innermost_first() { + let mut adapter = MqAdapter::new(); + + let outer_token_id = adapter.engine.token_arena().write().unwrap().alloc(mq_lang::Token { + kind: mq_lang::TokenKind::Eof, + range: mq_lang::Range { + start: mq_lang::Position::new(10, 1), + end: mq_lang::Position::new(10, 2), + }, + module_id: mq_lang::ModuleId::new(0), + }); + let middle_token_id = adapter.engine.token_arena().write().unwrap().alloc(mq_lang::Token { + kind: mq_lang::TokenKind::Eof, + range: mq_lang::Range { + start: mq_lang::Position::new(20, 3), + end: mq_lang::Position::new(20, 4), + }, + module_id: mq_lang::ModuleId::new(0), + }); + + let mut context = mq_lang::DebugContext::default(); + context.range = mq_lang::Range { + start: mq_lang::Position::new(30, 5), + end: mq_lang::Position::new(30, 6), + }; + context.module_id = mq_lang::ModuleId::new(0); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("outer"), Default::default()), + token_id: outer_token_id, + })); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("middle"), Default::default()), + token_id: middle_token_id, + })); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("inner"), Default::default()), + token_id: 0u32.into(), + })); + adapter.current_debug_context = Some(context); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 3); + + assert_eq!(frames[0].id, 1); + assert_eq!(frames[0].name, "inner() (unknown:30)"); + assert_eq!(frames[0].line, 30); + assert_eq!(frames[0].column, 5); + + assert_eq!(frames[1].id, 2); + assert_eq!(frames[1].name, "middle() (unknown:20)"); + assert_eq!(frames[1].line, 20); + assert_eq!(frames[1].column, 3); + + assert_eq!(frames[2].id, 3); + assert_eq!(frames[2].name, "outer() (unknown:10)"); + assert_eq!(frames[2].line, 10); + assert_eq!(frames[2].column, 1); +} + +#[test] +fn test_build_stack_frames_uses_query_file_name_when_set() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/tmp/query.mq".to_string()); + let mut context = mq_lang::DebugContext::default(); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("callee"), Default::default()), + token_id: 0u32.into(), + })); + adapter.current_debug_context = Some(context); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 1); + assert!(frames[0].name.contains("query.mq")); + assert_eq!( + frames[0].source.as_ref().unwrap().path.as_deref(), + Some("/tmp/query.mq") + ); +} + +#[test] +fn test_build_stack_frames_current_location_in_an_imported_module_does_not_use_the_query_file_source() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/tmp/query.mq".to_string()); + let mut context = mq_lang::DebugContext::default(); + context.range = mq_lang::Range { + start: mq_lang::Position::new(12, 1), + end: mq_lang::Position::new(12, 2), + }; + context.module_id = mq_lang::ModuleId::new(1); + adapter.current_debug_context = Some(context); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 1); + assert_ne!( + frames[0].source.as_ref().unwrap().path.as_deref(), + Some("/tmp/query.mq"), + "a frame paused inside an imported module must not report the query file as its source" + ); +} + +#[test] +fn test_build_stack_frames_outer_call_stack_entry_from_an_imported_module_does_not_use_the_query_file_source() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/tmp/query.mq".to_string()); + + let outer_token_id = adapter.engine.token_arena().write().unwrap().alloc(mq_lang::Token { + kind: mq_lang::TokenKind::Eof, + range: mq_lang::Range { + start: mq_lang::Position::new(12, 1), + end: mq_lang::Position::new(12, 2), + }, + module_id: mq_lang::ModuleId::new(1), + }); + + let mut context = mq_lang::DebugContext::default(); + context.range = mq_lang::Range { + start: mq_lang::Position::new(3, 1), + end: mq_lang::Position::new(3, 2), + }; + context.module_id = mq_lang::ModuleId::new(0); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("helper"), Default::default()), + token_id: outer_token_id, + })); + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Call(mq_lang::IdentWithToken::new("inner"), Default::default()), + token_id: 0u32.into(), + })); + adapter.current_debug_context = Some(context); + + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 2); + assert_ne!( + frames[1].source.as_ref().unwrap().path.as_deref(), + Some("/tmp/query.mq"), + "an outer call-stack frame belonging to an imported module must not report the query file as its source" + ); +} + +#[test] +fn test_stack_trace_reflects_single_level_call() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter + .engine + .debugger() + .write() + .unwrap() + .add_breakpoint_with_options(2, None, None, None, None, None); + for _ in 0..16 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + let code = "let f = fn(x):\n x + 1; |\nf(1)"; + let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); + assert_eq!(values[0], mq_lang::RuntimeValue::Number(2.into())); + + adapter.current_debug_context = Some(find_breakpoint_context_with_local(&adapter, "x")); + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].id, 1); + assert_eq!(frames[0].name, "f(1) (unknown:2)"); + assert_eq!(frames[0].line, 2); +} + +#[test] +fn test_stack_trace_reflects_two_level_nested_call() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter + .engine + .debugger() + .write() + .unwrap() + .add_breakpoint_with_options(2, None, None, None, None, None); + for _ in 0..16 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + let code = "let g = fn(y):\n y + 1; |\nlet f = fn(x):\n g(x) + 1; |\nf(1)"; + let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); + assert_eq!(values[0], mq_lang::RuntimeValue::Number(3.into())); + + adapter.current_debug_context = Some(find_breakpoint_context_with_local(&adapter, "y")); + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].id, 1); + assert_eq!(frames[0].name, "g(x) (unknown:2)"); + assert_eq!(frames[0].line, 2); + assert_eq!(frames[1].id, 2); + assert_eq!(frames[1].name, "f(1) (unknown:5)"); + assert_eq!(frames[1].line, 5); +} + +#[test] +fn test_stack_trace_reflects_three_level_nested_call() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter + .engine + .debugger() + .write() + .unwrap() + .add_breakpoint_with_options(2, None, None, None, None, None); + for _ in 0..32 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + let code = "let h = fn(z):\n z + 1; |\nlet g = fn(y):\n h(y) + 1; |\nlet f = fn(x):\n g(x) + 1; |\nf(1)"; + let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); + assert_eq!(values[0], mq_lang::RuntimeValue::Number(4.into())); + + adapter.current_debug_context = Some(find_breakpoint_context_with_local(&adapter, "z")); + let frames = adapter.build_stack_frames(); + + assert_eq!(frames.len(), 3); + assert_eq!(frames[0].id, 1); + assert_eq!(frames[0].name, "h(y) (unknown:2)"); + assert_eq!(frames[0].line, 2); + assert_eq!(frames[1].id, 2); + assert_eq!(frames[1].name, "g(x) (unknown:6)"); + assert_eq!(frames[1].line, 6); + assert_eq!(frames[2].id, 3); + assert_eq!(frames[2].name, "f(1) (unknown:7)"); + assert_eq!(frames[2].line, 7); +} + +#[test] +fn test_stack_trace_call_stack_does_not_grow_across_coroutine_resumes() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + adapter.engine.debugger().write().unwrap().add_breakpoint(2, None, None); + for _ in 0..32 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + let code = "def g():\nlet inc = fn(n): n + 1; |\nyield: inc(1) |\nyield: inc(2);\n|\nlet s = g() |\nlet first = next(s) |\nlet second = next(s) |\n[get(first, \"value\"), get(second, \"value\")]"; + let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); + assert_eq!( + values[0], + mq_lang::RuntimeValue::Array(Shared::new(vec![ + mq_lang::RuntimeValue::Number(2.into()), + mq_lang::RuntimeValue::Number(3.into()), + ])) + ); + + let call_stack_lengths: Vec = adapter + .debugger_message_rx + .as_ref() + .unwrap() + .try_iter() + .filter_map(|message| match message { + DebuggerMessage::BreakpointHit { context, .. } + if context.local_variables().iter().any(|v| v.name == "n") => + { + Some(context.call_stack.len()) + } + _ => None, + }) + .collect(); + + assert_eq!( + call_stack_lengths.len(), + 2, + "expected a breakpoint hit for each generator resume" + ); + assert!( + call_stack_lengths.iter().all(|&len| len == 1), + "each pause inside inc() should show exactly one call-stack frame regardless of resume count, got {call_stack_lengths:?}" + ); +} + +#[test] +fn test_handle_request_set_variable() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::SetVariable(dap::requests::SetVariableArguments { + variables_reference: 1, + name: "test_var".to_string(), + value: "42".to_string(), + format: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_err()); +} + +#[test] +fn test_handle_request_disconnect() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Disconnect(dap::requests::DisconnectArguments { + restart: None, + terminate_debuggee: None, + suspend_debuggee: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_err()); // Should return error to indicate shutdown +} + +#[test] +fn test_handle_request_set_breakpoints() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/path/to/test.mq".to_string()); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let source = dap::types::Source { + name: Some("test.mq".to_string()), + path: Some("/path/to/test.mq".to_string()), + adapter_data: None, + source_reference: None, + presentation_hint: None, + origin: None, + checksums: None, + sources: None, + }; + + let breakpoints = vec![ + dap::types::SourceBreakpoint { + line: 10, + column: Some(5), + condition: None, + hit_condition: None, + log_message: None, + }, + dap::types::SourceBreakpoint { + line: 20, + column: None, + condition: None, + hit_condition: None, + log_message: None, + }, + ]; + + #[allow(deprecated)] + let req = Request { + seq: 1, + command: Command::SetBreakpoints(dap::requests::SetBreakpointsArguments { + source, + breakpoints: Some(breakpoints), + lines: None, + source_modified: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_handle_request_set_breakpoints_forwards_condition_hit_condition_and_log_message() { + let mut adapter = MqAdapter::new(); + adapter.query_file = Some("/path/to/test.mq".to_string()); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let source = dap::types::Source { + name: Some("test.mq".to_string()), + path: Some("/path/to/test.mq".to_string()), + adapter_data: None, + source_reference: None, + presentation_hint: None, + origin: None, + checksums: None, + sources: None, + }; + + let breakpoints = vec![dap::types::SourceBreakpoint { + line: 10, + column: None, + condition: Some("x > 1".to_string()), + hit_condition: Some(">= 2".to_string()), + log_message: Some("x is {x}".to_string()), + }]; + + #[allow(deprecated)] + let req = Request { + seq: 1, + command: Command::SetBreakpoints(dap::requests::SetBreakpointsArguments { + source, + breakpoints: Some(breakpoints), + lines: None, + source_modified: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); + + let debugger = adapter.engine.debugger(); + let debugger = debugger.read().unwrap(); + let stored = debugger.list_breakpoints(); + assert_eq!(stored.len(), 1); + assert_eq!(stored[0].condition.as_deref(), Some("x > 1")); + assert_eq!(stored[0].hit_condition.as_deref(), Some(">= 2")); + assert_eq!(stored[0].log_message.as_deref(), Some("x is {x}")); +} + +#[test] +fn test_handle_request_set_exception_breakpoints_enables_uncaught_filter() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::SetExceptionBreakpoints(dap::requests::SetExceptionBreakpointsArguments { + filters: vec![UNCAUGHT_EXCEPTIONS_FILTER.to_string()], + filter_options: None, + exception_options: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); + assert!(adapter.exception_breakpoints_enabled.load(SeqCst)); +} + +#[test] +fn test_handle_request_set_exception_breakpoints_disables_when_filter_absent() { + let mut adapter = MqAdapter::new(); + adapter.exception_breakpoints_enabled.store(true, SeqCst); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::SetExceptionBreakpoints(dap::requests::SetExceptionBreakpointsArguments { + filters: vec![], + filter_options: None, + exception_options: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); + assert!(!adapter.exception_breakpoints_enabled.load(SeqCst)); +} + +#[test] +fn test_handle_request_set_expression() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::SetExpression(dap::requests::SetExpressionArguments { + expression: "test_var".to_string(), + value: "42".to_string(), + frame_id: None, + format: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + // No active debug context exists, so either backend must reject this request rather + // than treating it as an unknown command. + assert!(result.is_err()); + assert!(!matches!( + result.unwrap_err().downcast_ref::(), + Some(MqAdapterError::UnhandledCommand(_)) + )); +} + +#[test] +fn test_handle_request_unhandled_command() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let req = Request { + seq: 1, + command: Command::Initialize(dap::requests::InitializeArguments { + client_id: None, + client_name: None, + adapter_id: "test".to_string(), + locale: None, + lines_start_at1: None, + columns_start_at1: None, + path_format: None, + supports_variable_type: None, + supports_variable_paging: None, + supports_run_in_terminal_request: None, + supports_memory_references: None, + supports_progress_reporting: None, + supports_invalidated_event: None, + supports_memory_event: None, + supports_args_can_be_interpreted_by_shell: None, + supports_start_debugging_request: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_err()); // Should fail with UnhandledCommand error +} + +#[test] +fn test_handle_request_launch_success() { + let mut adapter = MqAdapter::new(); + let input = BufReader::new(Cursor::new(Vec::new())); + let output = BufWriter::new(Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let launch_args = LaunchArgs { + query_file: "/tmp/test_query.mq".to_string(), + input_file: None, + }; + + let mut additional_data = serde_json::Map::new(); + let value = serde_json::to_value(&launch_args).unwrap(); + if let serde_json::Value::Object(map) = value { + for (k, v) in map { + additional_data.insert(k, v); + } + } + + let req = Request { + seq: 1, + command: Command::Launch(dap::requests::LaunchRequestArguments { + no_debug: None, + restart_data: None, + additional_data: Some(serde_json::Value::Object(additional_data)), + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); + assert_eq!(adapter.query_file, Some("/tmp/test_query.mq".to_string())); +} + +#[test] +fn test_handle_request_stack_trace_with_context() { + let mut adapter = MqAdapter::new(); + let input = std::io::BufReader::new(std::io::Cursor::new(Vec::new())); + let output = std::io::BufWriter::new(std::io::Cursor::new(Vec::new())); + let mut server = Server::new(input, output); + + let mut context = mq_lang::DebugContext::default(); + + context.call_stack.push(Shared::new(mq_lang::AstNode { + expr: mq_lang::AstExpr::Literal(mq_lang::AstLiteral::Number(42.into())), + token_id: 0u32.into(), + })); + adapter.current_debug_context = Some(context); + + let req = Request { + seq: 1, + command: Command::StackTrace(dap::requests::StackTraceArguments { + thread_id: 1, + start_frame: None, + levels: None, + format: None, + }), + }; + + let result = adapter.handle_request(req, &mut server); + assert!(result.is_ok()); +} + +#[test] +fn test_breakpoint_inside_coroutine_body_hits_on_each_resume() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + // line 2: `yield: 1 |`, line 3: `yield: 2;` + adapter.engine.debugger().write().unwrap().add_breakpoint(2, None, None); + adapter.engine.debugger().write().unwrap().add_breakpoint(3, None, None); + + for _ in 0..32 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + let code = "def g():\nyield: 1 |\nyield: 2;\n|\nlet s = g() |\nlet first = next(s) |\nlet second = next(s) |\n[get(first, \"value\"), get(second, \"value\")]"; + let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); + assert_eq!( + values[0], + mq_lang::RuntimeValue::Array(Shared::new(vec![ + mq_lang::RuntimeValue::Number(1.into()), + mq_lang::RuntimeValue::Number(2.into()), + ])) + ); + + let hit_lines = adapter + .debugger_message_rx + .as_ref() + .unwrap() + .try_iter() + .filter_map(|message| match message { + DebuggerMessage::BreakpointHit { line, .. } => Some(line), + _ => None, + }) + .collect::>(); + + // Both yields must be observed, in order, though each may be reported more than once. + let mut distinct_in_order = hit_lines.clone(); + distinct_in_order.dedup(); + assert_eq!( + distinct_in_order, + vec![2, 3], + "expected a breakpoint hit on each yield, in resume order; got {hit_lines:?}" + ); +} + +#[test] +fn test_variables_inside_resumed_coroutine_see_the_sent_value() { + let mut adapter = MqAdapter::new(); + adapter.engine.debugger().write().unwrap().activate(); + // line 3: `yield: value;`, reached only after `send(s, 99)` resumes the coroutine and + // binds `value` from the first `yield`. + adapter.engine.debugger().write().unwrap().add_breakpoint(3, None, None); + for _ in 0..32 { + adapter.send_debugger_command(DapCommand::Continue).unwrap(); + } + + let code = + "def g():\nlet value = yield: 1 |\nyield: value;\n|\nlet s = g() |\nnext(s) |\nget(send(s, 99), \"value\")"; + let values = adapter.engine.eval(code, mq_lang::null_input().into_iter()).unwrap(); + assert_eq!(values[0], mq_lang::RuntimeValue::Number(99.into())); + + let context = adapter + .debugger_message_rx + .as_ref() + .unwrap() + .try_iter() + .find_map(|message| match message { + DebuggerMessage::BreakpointHit { line: 3, context, .. } => Some(context), + _ => None, + }) + .expect("the coroutine should stop at the breakpoint on `yield: value;`"); + + assert!( + context + .local_variables() + .iter() + .any(|variable| variable.name == "value" && variable.value == "99"), + "expected the coroutine's own `value` local (from send()) to be visible while paused, got {:?}", + context.local_variables() + ); +} diff --git a/crates/mq-ffi/src/lib.rs b/crates/mq-ffi/src/lib.rs index a2e804313..894f5fec5 100644 --- a/crates/mq-ffi/src/lib.rs +++ b/crates/mq-ffi/src/lib.rs @@ -747,881 +747,4 @@ pub extern "C" fn mq_clear_http_cache_all(engine_ptr: *mut MqContext) -> *mut c_ } #[cfg(test)] -mod tests { - use super::*; - - // Helper function to create a C string for testing - fn make_c_string(s: &str) -> *const c_char { - CString::new(s).unwrap().into_raw() - } - - // Helper function to convert C string back to Rust string for assertions - unsafe fn c_string_to_rust_string(ptr: *mut c_char) -> String { - if ptr.is_null() { - return String::new(); - } - let c_str = unsafe { CStr::from_ptr(ptr) }; - c_str.to_string_lossy().into_owned() - } - - #[test] - fn test_engine_create_and_destroy() { - // Test engine creation - let engine = mq_create(); - assert!(!engine.is_null()); - - // Test engine destruction - mq_destroy(engine); - - // Test destroying null engine (should not crash) - mq_destroy(ptr::null_mut()); - } - - #[test] - fn test_eval_with_null_engine() { - let code = make_c_string(".h"); - let input = make_c_string("test"); - let format = make_c_string("text"); - - let result = unsafe { mq_eval(ptr::null_mut(), code, input, format) }; - - assert!(result.values.is_null()); - assert_eq!(result.values_len, 0); - assert!(!result.error_msg.is_null()); - - let error_msg = unsafe { c_string_to_rust_string(result.error_msg) }; - assert_eq!(error_msg, "Engine pointer is null"); - - mq_free_result(result); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_eval_with_text_input() { - let engine = mq_create(); - let code = make_c_string(r#"select(contains("line"))"#); - let input = make_c_string("# line1\n## line2\n### line3"); - let format = make_c_string("text"); - let result = unsafe { mq_eval(engine, code, input, format) }; - assert!(result.error_msg.is_null()); - assert!(!result.values.is_null()); - assert_eq!(result.values_len, 3); - - // Verify the values - unsafe { - let values_slice = std::slice::from_raw_parts(result.values, result.values_len); - let value1 = c_string_to_rust_string(values_slice[0]); - let value2 = c_string_to_rust_string(values_slice[1]); - let value3 = c_string_to_rust_string(values_slice[2]); - - assert_eq!(value1, "# line1"); - assert_eq!(value2, "## line2"); - assert_eq!(value3, "### line3"); - } - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_eval_with_markdown_input() { - let engine = mq_create(); - let code = make_c_string(".h"); - let input = make_c_string("# Header\n\nSome text\n\n## Subheader"); - let format = make_c_string("markdown"); - - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.error_msg.is_null()); - assert!(!result.values.is_null()); - assert!(result.values_len > 0); - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_eval_with_invalid_code() { - let engine = mq_create(); - let code = make_c_string("invalid_function()"); - let input = make_c_string("test"); - let format = make_c_string("text"); - - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.values.is_null()); - assert_eq!(result.values_len, 0); - assert!(!result.error_msg.is_null()); - - let error_msg = unsafe { c_string_to_rust_string(result.error_msg) }; - assert!(error_msg.contains("Error evaluating query")); - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_eval_with_unsupported_format() { - let engine = mq_create(); - let code = make_c_string(".h"); - let input = make_c_string("test"); - let format = make_c_string("json"); - - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.values.is_null()); - assert_eq!(result.values_len, 0); - assert!(!result.error_msg.is_null()); - - let error_msg = unsafe { c_string_to_rust_string(result.error_msg) }; - assert!(error_msg.contains("Unsupported input format: json")); - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_eval_with_null_parameters() { - let engine = mq_create(); - - // Test with null code - let result = unsafe { mq_eval(engine, ptr::null(), make_c_string("test"), make_c_string("text")) }; - assert!(result.error_msg.is_null()); - mq_free_result(result); - - // Test with null input - let result = unsafe { mq_eval(engine, make_c_string(".a"), ptr::null(), make_c_string("text")) }; - assert!(!result.error_msg.is_null()); - mq_free_result(result); - - // Test with null format - let result = unsafe { mq_eval(engine, make_c_string(".a"), make_c_string("test"), ptr::null()) }; - assert!(!result.error_msg.is_null()); - mq_free_result(result); - - mq_destroy(engine); - } - - #[test] - fn test_format_case_insensitive() { - let engine = mq_create(); - let code = make_c_string(".h"); - let input = make_c_string("test"); - - // Test uppercase format - let format_upper = make_c_string("TEXT"); - let result = unsafe { mq_eval(engine, code, input, format_upper) }; - assert!(result.error_msg.is_null()); - mq_free_result(result); - - // Test mixed case format - let format_mixed = make_c_string("MarkDown"); - let input_md = make_c_string("# Test"); - let result = unsafe { mq_eval(engine, code, input_md, format_mixed) }; - assert!(result.error_msg.is_null()); - mq_free_result(result); - - mq_destroy(engine); - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(input_md as *mut c_char); - mq_free_string(format_upper as *mut c_char); - mq_free_string(format_mixed as *mut c_char); - } - } - - #[test] - fn test_free_functions() { - unsafe { - // Test freeing null string (should not crash) - mq_free_string(ptr::null_mut()); - - // Test freeing valid string - let test_string = to_c_string("test".to_string()); - mq_free_string(test_string); - } - - // Test freeing empty result - let empty_result = MqResult { - values: ptr::null_mut(), - values_len: 0, - error_msg: ptr::null_mut(), - }; - mq_free_result(empty_result); - } - - #[test] - fn test_empty_input() { - let engine = mq_create(); - let code = make_c_string(".h"); - let input = make_c_string(""); - let format = make_c_string("text"); - - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.error_msg.is_null()); - assert!(result.values.is_null()); - assert_eq!(result.values_len, 0); - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_filter_query() { - let engine = mq_create(); - let code = make_c_string("select(gt(len(), 5))"); - let input = make_c_string("short\nthis is a longer line\ntest\nanother long line here"); - let format = make_c_string("text"); - - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.error_msg.is_null()); - assert!(!result.values.is_null()); - assert_eq!(result.values_len, 4); // Only lines longer than 5 characters - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_map_query() { - let engine = mq_create(); - let code = make_c_string("len()"); - let input = make_c_string("a\nbb\nccc"); - let format = make_c_string("text"); - - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.error_msg.is_null()); - assert!(!result.values.is_null()); - assert_eq!(result.values_len, 3); - - unsafe { - let values_slice = std::slice::from_raw_parts(result.values, result.values_len); - let len1 = c_string_to_rust_string(values_slice[0]); - let len2 = c_string_to_rust_string(values_slice[1]); - let len3 = c_string_to_rust_string(values_slice[2]); - - assert_eq!(len1, "1"); - assert_eq!(len2, "2"); - assert_eq!(len3, "3"); - } - - mq_free_result(result); - mq_destroy(engine); - - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_c_str_to_rust_str_slice() { - // Test with valid C string - let c_string = CString::new("test").unwrap(); - let c_ptr = c_string.as_ptr(); - let result = unsafe { c_str_to_rust_str_slice(c_ptr) }; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), "test"); - - // Test with null pointer - let result = unsafe { c_str_to_rust_str_slice(ptr::null()) }; - assert!(result.is_ok()); - assert_eq!(result.unwrap(), ""); - } - - #[test] - fn test_to_c_string() { - // Test with valid string - let rust_string = "test".to_string(); - let c_ptr = to_c_string(rust_string); - assert!(!c_ptr.is_null()); - - let result = unsafe { CStr::from_ptr(c_ptr).to_str().unwrap() }; - assert_eq!(result, "test"); - - unsafe { - mq_free_string(c_ptr); - } - - // Test with string containing null bytes (should return null) - let invalid_string = "test\0with\0nulls".to_string(); - let c_ptr = to_c_string(invalid_string); - assert!(c_ptr.is_null()); - } - - #[test] - fn test_html_to_markdown_simple() { - let html = make_c_string("

Hello, World!

"); - let options = MqConversionOptions::default(); - let mut error_msg: *mut c_char = ptr::null_mut(); - - let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; - - assert!(error_msg.is_null()); - assert!(!result.is_null()); - - let markdown_str = unsafe { c_string_to_rust_string(result) }; - assert_eq!(markdown_str.trim(), "Hello, World!"); - - unsafe { - mq_free_string(result); - mq_free_string(html as *mut c_char); - } - } - - #[test] - fn test_html_to_markdown_with_options() { - let html = make_c_string("Test Page

Content

"); - let options = MqConversionOptions { - extract_scripts_as_code_blocks: false, - generate_front_matter: true, - use_title_as_h1: true, - base_url: ptr::null(), - }; - let mut error_msg: *mut c_char = ptr::null_mut(); - - let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; - - assert!(error_msg.is_null()); - assert!(!result.is_null()); - - let markdown_str = unsafe { c_string_to_rust_string(result) }; - assert!(markdown_str.contains("Test Page")); - assert!(markdown_str.contains("Content")); - - unsafe { - mq_free_string(result); - mq_free_string(html as *mut c_char); - } - } - - #[test] - fn test_html_to_markdown_null_input() { - let options = MqConversionOptions::default(); - let mut error_msg: *mut c_char = ptr::null_mut(); - - let result = unsafe { mq_html_to_markdown(ptr::null(), options, &mut error_msg) }; - - assert!(result.is_null()); - assert!(!error_msg.is_null()); - - let error_str = unsafe { c_string_to_rust_string(error_msg) }; - assert_eq!(error_str, "HTML input pointer is null"); - - unsafe { - mq_free_string(error_msg); - } - } - - #[test] - fn test_html_to_markdown_empty_input() { - let html = make_c_string(""); - let options = MqConversionOptions::default(); - let mut error_msg: *mut c_char = ptr::null_mut(); - - let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; - - assert!(error_msg.is_null()); - assert!(!result.is_null()); - - let markdown_str = unsafe { c_string_to_rust_string(result) }; - assert_eq!(markdown_str, ""); - - unsafe { - mq_free_string(result); - mq_free_string(html as *mut c_char); - } - } - - #[test] - fn test_html_to_markdown_complex_html() { - let html = make_c_string( - r#" - -

Title

-

Paragraph 1

-
    -
  • Item 1
  • -
  • Item 2
  • -
- - "#, - ); - let options = MqConversionOptions::default(); - let mut error_msg: *mut c_char = ptr::null_mut(); - - let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; - - assert!(error_msg.is_null()); - assert!(!result.is_null()); - - let markdown_str = unsafe { c_string_to_rust_string(result) }; - assert!(markdown_str.contains("# Title")); - assert!(markdown_str.contains("Paragraph 1")); - assert!(markdown_str.contains("Item 1")); - assert!(markdown_str.contains("Item 2")); - - unsafe { - mq_free_string(result); - mq_free_string(html as *mut c_char); - } - } - - #[test] - fn test_conversion_options_default() { - let options = MqConversionOptions::default(); - assert!(!options.extract_scripts_as_code_blocks); - assert!(!options.generate_front_matter); - assert!(!options.use_title_as_h1); - } - - #[test] - fn test_conversion_options_into() { - let mq_options = MqConversionOptions { - extract_scripts_as_code_blocks: true, - generate_front_matter: true, - use_title_as_h1: true, - base_url: ptr::null(), - }; - - let rust_options: ConversionOptions = mq_options.into(); - assert!(rust_options.extract_scripts_as_code_blocks); - assert!(rust_options.generate_front_matter); - assert!(rust_options.use_title_as_h1); - } - - #[test] - fn test_mq_version() { - let version_c = mq_version(); - assert!(!version_c.is_null()); - let version = unsafe { CStr::from_ptr(version_c) }.to_str().unwrap(); - assert!(!version.is_empty()); - assert_eq!(version, env!("CARGO_PKG_VERSION")); - } - - #[test] - fn test_mq_version_is_stable_across_calls() { - // The pointer must remain valid and comparable across multiple calls, - // since callers are told not to free it. - let first = unsafe { CStr::from_ptr(mq_version()) }.to_str().unwrap(); - let second = unsafe { CStr::from_ptr(mq_version()) }.to_str().unwrap(); - assert_eq!(first, second); - } - - #[test] - fn test_set_max_call_stack_depth() { - let engine = mq_create(); - mq_set_max_call_stack_depth(engine, 16); - - // Should not crash when the engine pointer is null. - mq_set_max_call_stack_depth(ptr::null_mut(), 16); - - mq_destroy(engine); - } - - #[test] - fn test_set_max_call_stack_depth_enforced() { - let engine = mq_create(); - mq_set_max_call_stack_depth(engine, 2); - - // 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) }; - - assert!(!result.error_msg.is_null()); - - mq_free_result(result); - mq_destroy(engine); - unsafe { - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_define_string_value_and_use_in_eval() { - let engine = mq_create(); - let name = make_c_string("greeting"); - let value = make_c_string("hello"); - - unsafe { - mq_define_string_value(engine, name, value); - } - - let code = make_c_string("greeting"); - let input = make_c_string("test"); - let format = make_c_string("text"); - let result = unsafe { mq_eval(engine, code, input, format) }; - - assert!(result.error_msg.is_null()); - assert_eq!(result.values_len, 1); - unsafe { - let values_slice = std::slice::from_raw_parts(result.values, result.values_len); - assert_eq!(c_string_to_rust_string(values_slice[0]), "hello"); - } - - mq_free_result(result); - mq_destroy(engine); - unsafe { - mq_free_string(name as *mut c_char); - mq_free_string(value as *mut c_char); - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_load_module_with_search_paths() { - use std::fs::File; - use std::io::Write; - - let temp_dir = std::env::temp_dir(); - let module_path = temp_dir.join("mq_ffi_test_module.mq"); - let mut file = File::create(&module_path).unwrap(); - file.write_all(b"def double(x): x * 2;").unwrap(); - - let engine = mq_create(); - let temp_dir_c = make_c_string(temp_dir.to_str().unwrap()); - let paths: [*const c_char; 1] = [temp_dir_c]; - - unsafe { - mq_set_search_paths(engine, paths.as_ptr(), paths.len()); - } - - let module_name = make_c_string("mq_ffi_test_module"); - let error_msg = unsafe { mq_load_module(engine, module_name) }; - assert!(error_msg.is_null()); - - std::fs::remove_file(&module_path).ok(); - mq_destroy(engine); - unsafe { - mq_free_string(temp_dir_c as *mut c_char); - mq_free_string(module_name as *mut c_char); - } - } - - #[test] - fn test_import_module_missing_returns_error() { - let engine = mq_create(); - let module_name = make_c_string("nonexistent_module_for_test"); - - let error_msg = unsafe { mq_import_module(engine, module_name) }; - assert!(!error_msg.is_null()); - - unsafe { - mq_free_string(error_msg); - mq_free_string(module_name as *mut c_char); - } - mq_destroy(engine); - } - - #[test] - fn test_import_module_null_engine() { - let module_name = make_c_string("anything"); - let error_msg = unsafe { mq_import_module(ptr::null_mut(), module_name) }; - assert!(!error_msg.is_null()); - - let error_str = unsafe { c_string_to_rust_string(error_msg) }; - assert_eq!(error_str, "Engine pointer is null"); - - unsafe { - mq_free_string(error_msg); - mq_free_string(module_name as *mut c_char); - } - } - - #[test] - fn test_load_module_null_engine() { - let module_name = make_c_string("anything"); - let error_msg = unsafe { mq_load_module(ptr::null_mut(), module_name) }; - assert!(!error_msg.is_null()); - - let error_str = unsafe { c_string_to_rust_string(error_msg) }; - assert_eq!(error_str, "Engine pointer is null"); - - unsafe { - mq_free_string(error_msg); - mq_free_string(module_name as *mut c_char); - } - } - - #[test] - fn test_load_module_missing_returns_error() { - let engine = mq_create(); - let module_name = make_c_string("nonexistent_module_for_test_load"); - - let error_msg = unsafe { mq_load_module(engine, module_name) }; - assert!(!error_msg.is_null()); - - unsafe { - mq_free_string(error_msg); - mq_free_string(module_name as *mut c_char); - } - mq_destroy(engine); - } - - #[test] - fn test_import_module_with_search_paths() { - use std::fs::File; - use std::io::Write; - - let temp_dir = std::env::temp_dir(); - let module_path = temp_dir.join("mq_ffi_test_import_module.mq"); - let mut file = File::create(&module_path).unwrap(); - file.write_all(b"def triple(x): x * 3;").unwrap(); - - let engine = mq_create(); - let temp_dir_c = make_c_string(temp_dir.to_str().unwrap()); - let paths: [*const c_char; 1] = [temp_dir_c]; - - unsafe { - mq_set_search_paths(engine, paths.as_ptr(), paths.len()); - } - - let module_name = make_c_string("mq_ffi_test_import_module"); - let error_msg = unsafe { mq_import_module(engine, module_name) }; - assert!(error_msg.is_null()); - - // Imported modules are namespaced, unlike `mq_load_module` which defines - // functions directly in the calling scope. - let code = make_c_string("mq_ffi_test_import_module::triple(2)"); - let input = make_c_string("test"); - let format = make_c_string("text"); - let result = unsafe { mq_eval(engine, code, input, format) }; - assert!(result.error_msg.is_null()); - unsafe { - let values_slice = std::slice::from_raw_parts(result.values, result.values_len); - assert_eq!(c_string_to_rust_string(values_slice[0]), "6"); - } - - std::fs::remove_file(&module_path).ok(); - mq_free_result(result); - mq_destroy(engine); - unsafe { - mq_free_string(temp_dir_c as *mut c_char); - mq_free_string(module_name as *mut c_char); - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - #[test] - fn test_set_search_paths_null_engine_does_not_crash() { - let path = make_c_string("/tmp"); - let paths: [*const c_char; 1] = [path]; - unsafe { - mq_set_search_paths(ptr::null_mut(), paths.as_ptr(), paths.len()); - mq_free_string(path as *mut c_char); - } - } - - #[test] - fn test_set_search_paths_empty_does_not_crash() { - let engine = mq_create(); - unsafe { - mq_set_search_paths(engine, ptr::null(), 0); - } - mq_destroy(engine); - } - - #[test] - fn test_define_string_value_null_engine_does_not_crash() { - let name = make_c_string("x"); - let value = make_c_string("y"); - unsafe { - mq_define_string_value(ptr::null_mut(), name, value); - mq_free_string(name as *mut c_char); - mq_free_string(value as *mut c_char); - } - } - - #[test] - fn test_define_string_value_invalid_utf8_does_not_crash() { - let engine = mq_create(); - let invalid = [0x66, 0x6f, 0x80, 0x6f, 0x00]; // "fo\x80o" - let value = make_c_string("y"); - - unsafe { - mq_define_string_value(engine, invalid.as_ptr() as *const c_char, value); - mq_free_string(value as *mut c_char); - } - - mq_destroy(engine); - } - - #[test] - fn test_define_string_value_overwrites_previous() { - let engine = mq_create(); - let name = make_c_string("v"); - let first = make_c_string("first"); - let second = make_c_string("second"); - - unsafe { - mq_define_string_value(engine, name, first); - mq_define_string_value(engine, name, second); - } - - let code = make_c_string("v"); - let input = make_c_string("test"); - let format = make_c_string("text"); - let result = unsafe { mq_eval(engine, code, input, format) }; - assert!(result.error_msg.is_null()); - unsafe { - let values_slice = std::slice::from_raw_parts(result.values, result.values_len); - assert_eq!(c_string_to_rust_string(values_slice[0]), "second"); - } - - mq_free_result(result); - mq_destroy(engine); - unsafe { - mq_free_string(name as *mut c_char); - mq_free_string(first as *mut c_char); - mq_free_string(second as *mut c_char); - mq_free_string(code as *mut c_char); - mq_free_string(input as *mut c_char); - mq_free_string(format as *mut c_char); - } - } - - // These run regardless of the `http-import` feature: the symbols are always - // exported (so linking against `mq.h` succeeds either way), but their - // behavior differs depending on whether HTTP module support was compiled in. - - #[test] - fn test_set_http_allowed_domains_does_not_crash() { - let engine = mq_create(); - let domain = make_c_string("example.com"); - let domains: [*const c_char; 1] = [domain]; - - unsafe { - mq_set_http_allowed_domains(engine, domains.as_ptr(), domains.len()); - mq_set_http_allowed_domains(engine, ptr::null(), 0); - mq_set_http_allowed_domains(ptr::null_mut(), domains.as_ptr(), domains.len()); - mq_free_string(domain as *mut c_char); - } - - mq_destroy(engine); - } - - #[test] - fn test_clear_http_cache() { - let engine = mq_create(); - let error_msg = mq_clear_http_cache(engine); - - if cfg!(feature = "http-import") { - assert!(error_msg.is_null()); - } else { - assert!(!error_msg.is_null()); - unsafe { mq_free_string(error_msg) }; - } - - // Should not crash when the engine pointer is null. - let error_msg_null = mq_clear_http_cache(ptr::null_mut()); - if !error_msg_null.is_null() { - unsafe { mq_free_string(error_msg_null) }; - } - - mq_destroy(engine); - } - - #[test] - fn test_clear_http_cache_all() { - let engine = mq_create(); - let error_msg = mq_clear_http_cache_all(engine); - - if cfg!(feature = "http-import") { - assert!(error_msg.is_null()); - } else { - assert!(!error_msg.is_null()); - unsafe { mq_free_string(error_msg) }; - } - - // Should not crash when the engine pointer is null. - let error_msg_null = mq_clear_http_cache_all(ptr::null_mut()); - if !error_msg_null.is_null() { - unsafe { mq_free_string(error_msg_null) }; - } - - mq_destroy(engine); - } - - #[test] - fn test_panic_message_str_payload() { - let payload = std::panic::catch_unwind(|| panic!("boom")).unwrap_err(); - assert_eq!(panic_message(&*payload), "boom"); - } - - #[test] - fn test_panic_message_string_payload() { - let payload = std::panic::catch_unwind(|| panic!("boom {}", 42)).unwrap_err(); - assert_eq!(panic_message(&*payload), "boom 42"); - } - - #[test] - fn test_panic_message_unknown_payload() { - let payload: Box = Box::new(123u32); - assert_eq!(panic_message(&*payload), "unknown panic"); - } - - #[test] - fn test_panic_to_c_string() { - let payload = std::panic::catch_unwind(|| panic!("boom")).unwrap_err(); - let c_str = panic_to_c_string(payload); - assert!(!c_str.is_null()); - let msg = unsafe { c_string_to_rust_string(c_str) }; - assert_eq!(msg, "Internal error: boom"); - unsafe { mq_free_string(c_str) }; - } -} +mod tests; diff --git a/crates/mq-ffi/src/tests.rs b/crates/mq-ffi/src/tests.rs new file mode 100644 index 000000000..7c1c7836f --- /dev/null +++ b/crates/mq-ffi/src/tests.rs @@ -0,0 +1,876 @@ +use super::*; + +// Helper function to create a C string for testing +fn make_c_string(s: &str) -> *const c_char { + CString::new(s).unwrap().into_raw() +} + +// Helper function to convert C string back to Rust string for assertions +unsafe fn c_string_to_rust_string(ptr: *mut c_char) -> String { + if ptr.is_null() { + return String::new(); + } + let c_str = unsafe { CStr::from_ptr(ptr) }; + c_str.to_string_lossy().into_owned() +} + +#[test] +fn test_engine_create_and_destroy() { + // Test engine creation + let engine = mq_create(); + assert!(!engine.is_null()); + + // Test engine destruction + mq_destroy(engine); + + // Test destroying null engine (should not crash) + mq_destroy(ptr::null_mut()); +} + +#[test] +fn test_eval_with_null_engine() { + let code = make_c_string(".h"); + let input = make_c_string("test"); + let format = make_c_string("text"); + + let result = unsafe { mq_eval(ptr::null_mut(), code, input, format) }; + + assert!(result.values.is_null()); + assert_eq!(result.values_len, 0); + assert!(!result.error_msg.is_null()); + + let error_msg = unsafe { c_string_to_rust_string(result.error_msg) }; + assert_eq!(error_msg, "Engine pointer is null"); + + mq_free_result(result); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_eval_with_text_input() { + let engine = mq_create(); + let code = make_c_string(r#"select(contains("line"))"#); + let input = make_c_string("# line1\n## line2\n### line3"); + let format = make_c_string("text"); + let result = unsafe { mq_eval(engine, code, input, format) }; + assert!(result.error_msg.is_null()); + assert!(!result.values.is_null()); + assert_eq!(result.values_len, 3); + + // Verify the values + unsafe { + let values_slice = std::slice::from_raw_parts(result.values, result.values_len); + let value1 = c_string_to_rust_string(values_slice[0]); + let value2 = c_string_to_rust_string(values_slice[1]); + let value3 = c_string_to_rust_string(values_slice[2]); + + assert_eq!(value1, "# line1"); + assert_eq!(value2, "## line2"); + assert_eq!(value3, "### line3"); + } + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_eval_with_markdown_input() { + let engine = mq_create(); + let code = make_c_string(".h"); + let input = make_c_string("# Header\n\nSome text\n\n## Subheader"); + let format = make_c_string("markdown"); + + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.error_msg.is_null()); + assert!(!result.values.is_null()); + assert!(result.values_len > 0); + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_eval_with_invalid_code() { + let engine = mq_create(); + let code = make_c_string("invalid_function()"); + let input = make_c_string("test"); + let format = make_c_string("text"); + + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.values.is_null()); + assert_eq!(result.values_len, 0); + assert!(!result.error_msg.is_null()); + + let error_msg = unsafe { c_string_to_rust_string(result.error_msg) }; + assert!(error_msg.contains("Error evaluating query")); + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_eval_with_unsupported_format() { + let engine = mq_create(); + let code = make_c_string(".h"); + let input = make_c_string("test"); + let format = make_c_string("json"); + + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.values.is_null()); + assert_eq!(result.values_len, 0); + assert!(!result.error_msg.is_null()); + + let error_msg = unsafe { c_string_to_rust_string(result.error_msg) }; + assert!(error_msg.contains("Unsupported input format: json")); + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_eval_with_null_parameters() { + let engine = mq_create(); + + // Test with null code + let result = unsafe { mq_eval(engine, ptr::null(), make_c_string("test"), make_c_string("text")) }; + assert!(result.error_msg.is_null()); + mq_free_result(result); + + // Test with null input + let result = unsafe { mq_eval(engine, make_c_string(".a"), ptr::null(), make_c_string("text")) }; + assert!(!result.error_msg.is_null()); + mq_free_result(result); + + // Test with null format + let result = unsafe { mq_eval(engine, make_c_string(".a"), make_c_string("test"), ptr::null()) }; + assert!(!result.error_msg.is_null()); + mq_free_result(result); + + mq_destroy(engine); +} + +#[test] +fn test_format_case_insensitive() { + let engine = mq_create(); + let code = make_c_string(".h"); + let input = make_c_string("test"); + + // Test uppercase format + let format_upper = make_c_string("TEXT"); + let result = unsafe { mq_eval(engine, code, input, format_upper) }; + assert!(result.error_msg.is_null()); + mq_free_result(result); + + // Test mixed case format + let format_mixed = make_c_string("MarkDown"); + let input_md = make_c_string("# Test"); + let result = unsafe { mq_eval(engine, code, input_md, format_mixed) }; + assert!(result.error_msg.is_null()); + mq_free_result(result); + + mq_destroy(engine); + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(input_md as *mut c_char); + mq_free_string(format_upper as *mut c_char); + mq_free_string(format_mixed as *mut c_char); + } +} + +#[test] +fn test_free_functions() { + unsafe { + // Test freeing null string (should not crash) + mq_free_string(ptr::null_mut()); + + // Test freeing valid string + let test_string = to_c_string("test".to_string()); + mq_free_string(test_string); + } + + // Test freeing empty result + let empty_result = MqResult { + values: ptr::null_mut(), + values_len: 0, + error_msg: ptr::null_mut(), + }; + mq_free_result(empty_result); +} + +#[test] +fn test_empty_input() { + let engine = mq_create(); + let code = make_c_string(".h"); + let input = make_c_string(""); + let format = make_c_string("text"); + + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.error_msg.is_null()); + assert!(result.values.is_null()); + assert_eq!(result.values_len, 0); + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_filter_query() { + let engine = mq_create(); + let code = make_c_string("select(gt(len(), 5))"); + let input = make_c_string("short\nthis is a longer line\ntest\nanother long line here"); + let format = make_c_string("text"); + + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.error_msg.is_null()); + assert!(!result.values.is_null()); + assert_eq!(result.values_len, 4); // Only lines longer than 5 characters + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_map_query() { + let engine = mq_create(); + let code = make_c_string("len()"); + let input = make_c_string("a\nbb\nccc"); + let format = make_c_string("text"); + + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.error_msg.is_null()); + assert!(!result.values.is_null()); + assert_eq!(result.values_len, 3); + + unsafe { + let values_slice = std::slice::from_raw_parts(result.values, result.values_len); + let len1 = c_string_to_rust_string(values_slice[0]); + let len2 = c_string_to_rust_string(values_slice[1]); + let len3 = c_string_to_rust_string(values_slice[2]); + + assert_eq!(len1, "1"); + assert_eq!(len2, "2"); + assert_eq!(len3, "3"); + } + + mq_free_result(result); + mq_destroy(engine); + + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_c_str_to_rust_str_slice() { + // Test with valid C string + let c_string = CString::new("test").unwrap(); + let c_ptr = c_string.as_ptr(); + let result = unsafe { c_str_to_rust_str_slice(c_ptr) }; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "test"); + + // Test with null pointer + let result = unsafe { c_str_to_rust_str_slice(ptr::null()) }; + assert!(result.is_ok()); + assert_eq!(result.unwrap(), ""); +} + +#[test] +fn test_to_c_string() { + // Test with valid string + let rust_string = "test".to_string(); + let c_ptr = to_c_string(rust_string); + assert!(!c_ptr.is_null()); + + let result = unsafe { CStr::from_ptr(c_ptr).to_str().unwrap() }; + assert_eq!(result, "test"); + + unsafe { + mq_free_string(c_ptr); + } + + // Test with string containing null bytes (should return null) + let invalid_string = "test\0with\0nulls".to_string(); + let c_ptr = to_c_string(invalid_string); + assert!(c_ptr.is_null()); +} + +#[test] +fn test_html_to_markdown_simple() { + let html = make_c_string("

Hello, World!

"); + let options = MqConversionOptions::default(); + let mut error_msg: *mut c_char = ptr::null_mut(); + + let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; + + assert!(error_msg.is_null()); + assert!(!result.is_null()); + + let markdown_str = unsafe { c_string_to_rust_string(result) }; + assert_eq!(markdown_str.trim(), "Hello, World!"); + + unsafe { + mq_free_string(result); + mq_free_string(html as *mut c_char); + } +} + +#[test] +fn test_html_to_markdown_with_options() { + let html = make_c_string("Test Page

Content

"); + let options = MqConversionOptions { + extract_scripts_as_code_blocks: false, + generate_front_matter: true, + use_title_as_h1: true, + base_url: ptr::null(), + }; + let mut error_msg: *mut c_char = ptr::null_mut(); + + let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; + + assert!(error_msg.is_null()); + assert!(!result.is_null()); + + let markdown_str = unsafe { c_string_to_rust_string(result) }; + assert!(markdown_str.contains("Test Page")); + assert!(markdown_str.contains("Content")); + + unsafe { + mq_free_string(result); + mq_free_string(html as *mut c_char); + } +} + +#[test] +fn test_html_to_markdown_null_input() { + let options = MqConversionOptions::default(); + let mut error_msg: *mut c_char = ptr::null_mut(); + + let result = unsafe { mq_html_to_markdown(ptr::null(), options, &mut error_msg) }; + + assert!(result.is_null()); + assert!(!error_msg.is_null()); + + let error_str = unsafe { c_string_to_rust_string(error_msg) }; + assert_eq!(error_str, "HTML input pointer is null"); + + unsafe { + mq_free_string(error_msg); + } +} + +#[test] +fn test_html_to_markdown_empty_input() { + let html = make_c_string(""); + let options = MqConversionOptions::default(); + let mut error_msg: *mut c_char = ptr::null_mut(); + + let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; + + assert!(error_msg.is_null()); + assert!(!result.is_null()); + + let markdown_str = unsafe { c_string_to_rust_string(result) }; + assert_eq!(markdown_str, ""); + + unsafe { + mq_free_string(result); + mq_free_string(html as *mut c_char); + } +} + +#[test] +fn test_html_to_markdown_complex_html() { + let html = make_c_string( + r#" + +

Title

+

Paragraph 1

+
    +
  • Item 1
  • +
  • Item 2
  • +
+ + "#, + ); + let options = MqConversionOptions::default(); + let mut error_msg: *mut c_char = ptr::null_mut(); + + let result = unsafe { mq_html_to_markdown(html, options, &mut error_msg) }; + + assert!(error_msg.is_null()); + assert!(!result.is_null()); + + let markdown_str = unsafe { c_string_to_rust_string(result) }; + assert!(markdown_str.contains("# Title")); + assert!(markdown_str.contains("Paragraph 1")); + assert!(markdown_str.contains("Item 1")); + assert!(markdown_str.contains("Item 2")); + + unsafe { + mq_free_string(result); + mq_free_string(html as *mut c_char); + } +} + +#[test] +fn test_conversion_options_default() { + let options = MqConversionOptions::default(); + assert!(!options.extract_scripts_as_code_blocks); + assert!(!options.generate_front_matter); + assert!(!options.use_title_as_h1); +} + +#[test] +fn test_conversion_options_into() { + let mq_options = MqConversionOptions { + extract_scripts_as_code_blocks: true, + generate_front_matter: true, + use_title_as_h1: true, + base_url: ptr::null(), + }; + + let rust_options: ConversionOptions = mq_options.into(); + assert!(rust_options.extract_scripts_as_code_blocks); + assert!(rust_options.generate_front_matter); + assert!(rust_options.use_title_as_h1); +} + +#[test] +fn test_mq_version() { + let version_c = mq_version(); + assert!(!version_c.is_null()); + let version = unsafe { CStr::from_ptr(version_c) }.to_str().unwrap(); + assert!(!version.is_empty()); + assert_eq!(version, env!("CARGO_PKG_VERSION")); +} + +#[test] +fn test_mq_version_is_stable_across_calls() { + // The pointer must remain valid and comparable across multiple calls, + // since callers are told not to free it. + let first = unsafe { CStr::from_ptr(mq_version()) }.to_str().unwrap(); + let second = unsafe { CStr::from_ptr(mq_version()) }.to_str().unwrap(); + assert_eq!(first, second); +} + +#[test] +fn test_set_max_call_stack_depth() { + let engine = mq_create(); + mq_set_max_call_stack_depth(engine, 16); + + // Should not crash when the engine pointer is null. + mq_set_max_call_stack_depth(ptr::null_mut(), 16); + + mq_destroy(engine); +} + +#[test] +fn test_set_max_call_stack_depth_enforced() { + let engine = mq_create(); + mq_set_max_call_stack_depth(engine, 2); + + // 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) }; + + assert!(!result.error_msg.is_null()); + + mq_free_result(result); + mq_destroy(engine); + unsafe { + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_define_string_value_and_use_in_eval() { + let engine = mq_create(); + let name = make_c_string("greeting"); + let value = make_c_string("hello"); + + unsafe { + mq_define_string_value(engine, name, value); + } + + let code = make_c_string("greeting"); + let input = make_c_string("test"); + let format = make_c_string("text"); + let result = unsafe { mq_eval(engine, code, input, format) }; + + assert!(result.error_msg.is_null()); + assert_eq!(result.values_len, 1); + unsafe { + let values_slice = std::slice::from_raw_parts(result.values, result.values_len); + assert_eq!(c_string_to_rust_string(values_slice[0]), "hello"); + } + + mq_free_result(result); + mq_destroy(engine); + unsafe { + mq_free_string(name as *mut c_char); + mq_free_string(value as *mut c_char); + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_load_module_with_search_paths() { + use std::fs::File; + use std::io::Write; + + let temp_dir = std::env::temp_dir(); + let module_path = temp_dir.join("mq_ffi_test_module.mq"); + let mut file = File::create(&module_path).unwrap(); + file.write_all(b"def double(x): x * 2;").unwrap(); + + let engine = mq_create(); + let temp_dir_c = make_c_string(temp_dir.to_str().unwrap()); + let paths: [*const c_char; 1] = [temp_dir_c]; + + unsafe { + mq_set_search_paths(engine, paths.as_ptr(), paths.len()); + } + + let module_name = make_c_string("mq_ffi_test_module"); + let error_msg = unsafe { mq_load_module(engine, module_name) }; + assert!(error_msg.is_null()); + + std::fs::remove_file(&module_path).ok(); + mq_destroy(engine); + unsafe { + mq_free_string(temp_dir_c as *mut c_char); + mq_free_string(module_name as *mut c_char); + } +} + +#[test] +fn test_import_module_missing_returns_error() { + let engine = mq_create(); + let module_name = make_c_string("nonexistent_module_for_test"); + + let error_msg = unsafe { mq_import_module(engine, module_name) }; + assert!(!error_msg.is_null()); + + unsafe { + mq_free_string(error_msg); + mq_free_string(module_name as *mut c_char); + } + mq_destroy(engine); +} + +#[test] +fn test_import_module_null_engine() { + let module_name = make_c_string("anything"); + let error_msg = unsafe { mq_import_module(ptr::null_mut(), module_name) }; + assert!(!error_msg.is_null()); + + let error_str = unsafe { c_string_to_rust_string(error_msg) }; + assert_eq!(error_str, "Engine pointer is null"); + + unsafe { + mq_free_string(error_msg); + mq_free_string(module_name as *mut c_char); + } +} + +#[test] +fn test_load_module_null_engine() { + let module_name = make_c_string("anything"); + let error_msg = unsafe { mq_load_module(ptr::null_mut(), module_name) }; + assert!(!error_msg.is_null()); + + let error_str = unsafe { c_string_to_rust_string(error_msg) }; + assert_eq!(error_str, "Engine pointer is null"); + + unsafe { + mq_free_string(error_msg); + mq_free_string(module_name as *mut c_char); + } +} + +#[test] +fn test_load_module_missing_returns_error() { + let engine = mq_create(); + let module_name = make_c_string("nonexistent_module_for_test_load"); + + let error_msg = unsafe { mq_load_module(engine, module_name) }; + assert!(!error_msg.is_null()); + + unsafe { + mq_free_string(error_msg); + mq_free_string(module_name as *mut c_char); + } + mq_destroy(engine); +} + +#[test] +fn test_import_module_with_search_paths() { + use std::fs::File; + use std::io::Write; + + let temp_dir = std::env::temp_dir(); + let module_path = temp_dir.join("mq_ffi_test_import_module.mq"); + let mut file = File::create(&module_path).unwrap(); + file.write_all(b"def triple(x): x * 3;").unwrap(); + + let engine = mq_create(); + let temp_dir_c = make_c_string(temp_dir.to_str().unwrap()); + let paths: [*const c_char; 1] = [temp_dir_c]; + + unsafe { + mq_set_search_paths(engine, paths.as_ptr(), paths.len()); + } + + let module_name = make_c_string("mq_ffi_test_import_module"); + let error_msg = unsafe { mq_import_module(engine, module_name) }; + assert!(error_msg.is_null()); + + // Imported modules are namespaced, unlike `mq_load_module` which defines + // functions directly in the calling scope. + let code = make_c_string("mq_ffi_test_import_module::triple(2)"); + let input = make_c_string("test"); + let format = make_c_string("text"); + let result = unsafe { mq_eval(engine, code, input, format) }; + assert!(result.error_msg.is_null()); + unsafe { + let values_slice = std::slice::from_raw_parts(result.values, result.values_len); + assert_eq!(c_string_to_rust_string(values_slice[0]), "6"); + } + + std::fs::remove_file(&module_path).ok(); + mq_free_result(result); + mq_destroy(engine); + unsafe { + mq_free_string(temp_dir_c as *mut c_char); + mq_free_string(module_name as *mut c_char); + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +#[test] +fn test_set_search_paths_null_engine_does_not_crash() { + let path = make_c_string("/tmp"); + let paths: [*const c_char; 1] = [path]; + unsafe { + mq_set_search_paths(ptr::null_mut(), paths.as_ptr(), paths.len()); + mq_free_string(path as *mut c_char); + } +} + +#[test] +fn test_set_search_paths_empty_does_not_crash() { + let engine = mq_create(); + unsafe { + mq_set_search_paths(engine, ptr::null(), 0); + } + mq_destroy(engine); +} + +#[test] +fn test_define_string_value_null_engine_does_not_crash() { + let name = make_c_string("x"); + let value = make_c_string("y"); + unsafe { + mq_define_string_value(ptr::null_mut(), name, value); + mq_free_string(name as *mut c_char); + mq_free_string(value as *mut c_char); + } +} + +#[test] +fn test_define_string_value_invalid_utf8_does_not_crash() { + let engine = mq_create(); + let invalid = [0x66, 0x6f, 0x80, 0x6f, 0x00]; // "fo\x80o" + let value = make_c_string("y"); + + unsafe { + mq_define_string_value(engine, invalid.as_ptr() as *const c_char, value); + mq_free_string(value as *mut c_char); + } + + mq_destroy(engine); +} + +#[test] +fn test_define_string_value_overwrites_previous() { + let engine = mq_create(); + let name = make_c_string("v"); + let first = make_c_string("first"); + let second = make_c_string("second"); + + unsafe { + mq_define_string_value(engine, name, first); + mq_define_string_value(engine, name, second); + } + + let code = make_c_string("v"); + let input = make_c_string("test"); + let format = make_c_string("text"); + let result = unsafe { mq_eval(engine, code, input, format) }; + assert!(result.error_msg.is_null()); + unsafe { + let values_slice = std::slice::from_raw_parts(result.values, result.values_len); + assert_eq!(c_string_to_rust_string(values_slice[0]), "second"); + } + + mq_free_result(result); + mq_destroy(engine); + unsafe { + mq_free_string(name as *mut c_char); + mq_free_string(first as *mut c_char); + mq_free_string(second as *mut c_char); + mq_free_string(code as *mut c_char); + mq_free_string(input as *mut c_char); + mq_free_string(format as *mut c_char); + } +} + +// These run regardless of the `http-import` feature: the symbols are always +// exported (so linking against `mq.h` succeeds either way), but their +// behavior differs depending on whether HTTP module support was compiled in. + +#[test] +fn test_set_http_allowed_domains_does_not_crash() { + let engine = mq_create(); + let domain = make_c_string("example.com"); + let domains: [*const c_char; 1] = [domain]; + + unsafe { + mq_set_http_allowed_domains(engine, domains.as_ptr(), domains.len()); + mq_set_http_allowed_domains(engine, ptr::null(), 0); + mq_set_http_allowed_domains(ptr::null_mut(), domains.as_ptr(), domains.len()); + mq_free_string(domain as *mut c_char); + } + + mq_destroy(engine); +} + +#[test] +fn test_clear_http_cache() { + let engine = mq_create(); + let error_msg = mq_clear_http_cache(engine); + + if cfg!(feature = "http-import") { + assert!(error_msg.is_null()); + } else { + assert!(!error_msg.is_null()); + unsafe { mq_free_string(error_msg) }; + } + + // Should not crash when the engine pointer is null. + let error_msg_null = mq_clear_http_cache(ptr::null_mut()); + if !error_msg_null.is_null() { + unsafe { mq_free_string(error_msg_null) }; + } + + mq_destroy(engine); +} + +#[test] +fn test_clear_http_cache_all() { + let engine = mq_create(); + let error_msg = mq_clear_http_cache_all(engine); + + if cfg!(feature = "http-import") { + assert!(error_msg.is_null()); + } else { + assert!(!error_msg.is_null()); + unsafe { mq_free_string(error_msg) }; + } + + // Should not crash when the engine pointer is null. + let error_msg_null = mq_clear_http_cache_all(ptr::null_mut()); + if !error_msg_null.is_null() { + unsafe { mq_free_string(error_msg_null) }; + } + + mq_destroy(engine); +} + +#[test] +fn test_panic_message_str_payload() { + let payload = std::panic::catch_unwind(|| panic!("boom")).unwrap_err(); + assert_eq!(panic_message(&*payload), "boom"); +} + +#[test] +fn test_panic_message_string_payload() { + let payload = std::panic::catch_unwind(|| panic!("boom {}", 42)).unwrap_err(); + assert_eq!(panic_message(&*payload), "boom 42"); +} + +#[test] +fn test_panic_message_unknown_payload() { + let payload: Box = Box::new(123u32); + assert_eq!(panic_message(&*payload), "unknown panic"); +} + +#[test] +fn test_panic_to_c_string() { + let payload = std::panic::catch_unwind(|| panic!("boom")).unwrap_err(); + let c_str = panic_to_c_string(payload); + assert!(!c_str.is_null()); + let msg = unsafe { c_string_to_rust_string(c_str) }; + assert_eq!(msg, "Internal error: boom"); + unsafe { mq_free_string(c_str) }; +} diff --git a/crates/mq-formatter/src/formatter.rs b/crates/mq-formatter/src/formatter.rs index f1b3804ae..8915122c7 100644 --- a/crates/mq-formatter/src/formatter.rs +++ b/crates/mq-formatter/src/formatter.rs @@ -1908,1739 +1908,4 @@ impl Formatter { } #[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - #[case::empty("", "")] - #[case::if_( - "if(test): - test - else: - test2", - "if (test): - test -else: - test2 -" - )] - #[case::def_( - "def name(test): - test | test2;", - "def name(test): - test | test2; -" - )] - #[case::foreach_( - "foreach(x,array(1, 2, 3)): - add(x, 1);", - "foreach (x, array(1, 2, 3)): - add(x, 1); -" - )] - #[case::def_( - "def test(): - test1 - |test2 - |test3;", - "def test(): - test1 - | test2 - | test3; -" - )] - #[case("test()|test2()", "test() | test2()")] - #[case( - "if(test): - test - elif(test2): - test2 - else: - test3", - "if (test): - test -elif (test2): - test2 -else: - test3 -" - )] - #[case::if_( - "if(test): - test - elif(test2): - test2 - else: - test3", - "if (test): - test -elif (test2): - test2 -else: - test3 -" - )] - #[case::if_( - "if(test): - test - else: - test2", - "if (test): - test -else: - test2 -" - )] - #[case::if_else( - "if(test): - test - else: do - test2 - end", - "if (test): - test -else: do - test2 - end -" - )] - #[case::one_line("if(test): test else: test2", "if (test): test else: test2")] - #[case::one_line( - "if(test): test elif(test2): test2 else: test3", - "if (test): test elif (test2): test2 else: test3" - )] - #[case::foreach_one_line("foreach(x,array(1,2,3)):add(x,1);", "foreach (x, array(1, 2, 3)): add(x, 1);")] - #[case::foreach_one_line( - "foreach(x,array(1,2,3)):add(x,1);|add(1,2);", - "foreach (x, array(1, 2, 3)): add(x, 1); | add(1, 2);" - )] - #[case::foreach_one_line(".[]|upcase()", ".[] | upcase()")] - #[case::while_multiline( - "while(condition()): - process();", - "while (condition()): - process(); -" - )] - #[case::while_oneline("while(condition()): process();", "while (condition()): process();")] - #[case::while_with_pipe( - "while(check_condition()): - data - | process() - | output();", - "while (check_condition()): - data - | process() - | output(); -" - )] - #[case::loop_multiline( - "loop: - process();", - "loop: - process(); -" - )] - #[case::loop_oneline("loop: process();", "loop: process();")] - #[case::loop_with_pipe( - "loop: - data - | process() - | output();", - "loop: - data - | process() - | output(); -" - )] - #[case::loop_with_break( - "loop: - add(self,1) - |if(gt(self,5)):break;;", - "loop: - add(self, 1) - | if (gt(self, 5)): break;; -" - )] - #[case::until_multiline( - "until(condition()): - process();", - "until (condition()): - process(); -" - )] - #[case::until_oneline("until(condition()): process();", "until (condition()): process();")] - #[case::until_with_pipe( - "until(check_condition()): - data - | process() - | output();", - "until (check_condition()): - data - | process() - | output(); -" - )] - #[case::until_with_break( - "until(false): - add(self,1) - |if(gt(self,5)):break;;", - "until (false): - add(self, 1) - | if (gt(self, 5)): break;; -" - )] - #[case::unless_multiline( - "unless(test): - test", - "unless (test): - test -" - )] - #[case::unless_oneline("unless(test): test", "unless (test): test")] - #[case::def( - r##".h -| let link = to_link(add("#", to_text(self)), to_text(self), ""); -| if (eq(to_md_name(), "h1")): -to_md_list(link, 1) -elif (eq(to_md_name(),"h2")): -to_md_list(link, 2) -elif (eq(to_md_name(), "h3")): -to_md_list(link, 3) -elif (eq(to_md_name(), "h4")): -to_md_list(link, 4) -elif (eq(to_md_name(), "h5")): -to_md_list(link, 5) -else: -None"##, - r##".h -| let link = to_link(add("#", to_text(self)), to_text(self), ""); -| if (eq(to_md_name(), "h1")): - to_md_list(link, 1) - elif (eq(to_md_name(), "h2")): - to_md_list(link, 2) - elif (eq(to_md_name(), "h3")): - to_md_list(link, 3) - elif (eq(to_md_name(), "h4")): - to_md_list(link, 4) - elif (eq(to_md_name(), "h5")): - to_md_list(link, 5) - else: - None -"## - )] - #[case::def( - r#"def snake_to_camel(x): - let words = split(x, "_") - | foreach (word, words): - let first_char = upcase(first(word)) - | let rest_str = downcase(slice(word, 1, len(word))) - | s"${first_char}${rest_str}"; - | join(""); -| snake_to_camel()"#, - r#"def snake_to_camel(x): - let words = split(x, "_") - | foreach (word, words): - let first_char = upcase(first(word)) - | let rest_str = downcase(slice(word, 1, len(word))) - | s"${first_char}${rest_str}"; - | join(""); -| snake_to_camel() -"# - )] - #[case::def( - r#"def snake_to_camel(x): let words = split(x, "_") | foreach (word, words): let first_char = upcase(first(word)) | let rest_str = downcase(slice(word, 1, len(word))) | add(first_char, rest_str); | join("");| snake_to_camel()"#, - r#"def snake_to_camel(x): let words = split(x, "_") | foreach (word, words): let first_char = upcase(first(word)) | let rest_str = downcase(slice(word, 1, len(word))) | add(first_char, rest_str); | join(""); | snake_to_camel()"# - )] - #[case::let_(r#"let test = "test""#, r#"let test = "test""#)] - #[case::call( - r#"test( -"test")"#, - r#"test( - "test") -"# - )] - #[case::call( - r#"test( -"test" - )"#, - r#"test( - "test" -) -"# - )] - #[case::call( - r#"test( -"test" -,"test" -,true - )"#, - r#"test( - "test" - ,"test" - ,true -) -"# - )] - #[case::interpolated_string( - r#"test( -s"test${val1}" - )"#, - r#"test( - s"test${val1}" -) -"# - )] - #[case::include("include \"test.mq\"", "include \"test.mq\"")] - #[case::import("import \"test.mq\"", "import \"test.mq\"")] - #[case::import_as("import \"test.mq\" as m", "import \"test.mq\" as m")] - #[case::nodes("nodes|nodes", "nodes | nodes")] - #[case::fn_("fn(): program;", "fn(): program;")] - #[case::fn_multiline( - "fn(arg1,arg2): - program;", - "fn(arg1, arg2): - program; -" - )] - #[case::fn_args("map( fn():program;)", "map(fn(): program;)")] - #[case::array_trailing_comma("[1, 2,]", "[1, 2,]")] - #[case::dict_trailing_comma("{\"a\": 1,}", "{\"a\": 1,}")] - #[case::dict_trailing_comma_multiline("{\n \"a\": 1,\n \"b\": 2,\n}", "{\n \"a\": 1,\n \"b\": 2,\n}\n")] - #[case::array_comment_before_close("[\n 1,\n # c\n]", "[\n 1,\n # c\n]\n")] - #[case::dict_comment_before_close("{\n \"a\": 1\n # c\n}", "{\n \"a\": 1\n # c\n}\n")] - #[case::array_same_line_comment_before_close("[1, 2, # c\n]", "[1, 2, # c\n]\n")] - #[case::array_comments_before_close("[\n 1,\n\n # a\n # b\n]", "[\n 1,\n # a\n # b\n]\n")] - #[case::number_small_fraction("1e-9", "0.000000001")] - #[case::number_long_fraction("3.14159265358979", "3.14159265358979")] - #[case::number_integral_float("2.0", "2")] - #[case::array_empty("[]", "[]")] - #[case::array_single_element("[1]", "[1]")] - #[case::array_multiple_elements("[1,2,3]", "[1, 2, 3]")] - #[case::array_mixed_types("[1,\"test\",true]", "[1, \"test\", true]")] - #[case::array_nested("[[1,2],[3,4]]", "[[1, 2], [3, 4]]")] - #[case::array_with_spaces("[ 1 , 2 , 3 ]", "[1, 2, 3]")] - #[case::array_multiline( - r#"[ - 1, - 2, - 3 - ]"#, - "[\n 1,\n 2,\n 3\n]\n" - )] - #[case::let_with_array(r#"let arr = [1, 2, 3]"#, r#"let arr = [1, 2, 3]"#)] - #[case::let_with_array_multiline( - r#"let arr = [ -1, -2, -3 -]"#, - "let arr = [\n 1,\n 2,\n 3\n]\n" - )] - #[case::dict_empty("{}", "{}")] - #[case::def_with_let_and_array( - r#"def foo(): - let arr = [ - 1, - 2, - 3]; -"#, - r#"def foo(): - let arr = [ - 1, - 2, - 3 - ]; -"# - )] - #[case::dict_single_pair("{\"key\": \"value\"}", "{\"key\": \"value\"}")] - #[case::dict_multiple_pairs( - "{\"key1\": \"value1\", \"key2\": \"value2\"}", - "{\"key1\": \"value1\", \"key2\": \"value2\"}" - )] - #[case::dict_with_spaces("{ \"key\" : \"value\" }", "{\"key\": \"value\"}")] - #[case::dict_mixed_types( - "{\"str\": \"value\", \"num\": 42, \"bool\": true}", - "{\"str\": \"value\", \"num\": 42, \"bool\": true}" - )] - #[case::dict_nested("{\"outer\": {\"inner\": \"value\"}}", "{\"outer\": {\"inner\": \"value\"}}")] - #[case::dict_multiline( - r#"{ -"key1": "value1", -"key2": "value2" -}"#, - "{\n \"key1\": \"value1\",\n \"key2\": \"value2\"\n}\n" - )] - #[case::dict_multiline_mixed( - r#"{ -"str": "value", -"num": 42, -"bool": true -}"#, - "{\n \"str\": \"value\",\n \"num\": 42,\n \"bool\": true\n}\n" - )] - #[case::equal_operator("let x = 1 == 2", "let x = 1 == 2")] - #[case::not_equal_operator("let y = 3 != 4", "let y = 3 != 4")] - #[case::string_with_newline(r#""line1\nline2""#, r#""line1\nline2""#)] - #[case::plus_operator("let x = 1 + 2", "let x = 1 + 2")] - #[case::let_newline_after_equal( - r#"let x = -"test""#, - r#"let x = - "test" -"# - )] - #[case::let_with_if_multiline( - r#"let x = if(test): -test -else: -test2"#, - r#"let x = if (test): - test -else: - test2 -"# - )] - #[case::let_with_while_multiline( - r#"let x = while(condition()): -process();"#, - r#"let x = while (condition()): - process(); -"# - )] - #[case::less_than_operator("let x = 1 < 2", "let x = 1 < 2")] - #[case::less_than_equal_operator("let x = 1 <= 2", "let x = 1 <= 2")] - #[case::greater_than_operator("let x = 2 > 1", "let x = 2 > 1")] - #[case::greater_than_equal_operator("let x = 2 >= 1", "let x = 2 >= 1")] - #[case::range_operator("1..1", "1..1")] - #[case::range_operator_with_spaces("1 .. 1", "1..1")] - #[case::range_operator_with_variables("x..y", "x..y")] - #[case::range_operator_with_string(r#""1" .. "2""#, r#""1".."2""#)] - #[case::selector_attr(".code.lang", ".code.lang")] - #[case::standalone_attr_selector(".lang", ".lang")] - #[case::standalone_attr_selector_value(".value", ".value")] - #[case::env("let ENV = $env", "let ENV = $env")] - #[case::mul("1 * 1", "1 * 1")] - #[case::mul("1 / 1", "1 / 1")] - #[case::and("true && false", "true && false")] - #[case::or("true || false", "true || false")] - #[case::binary_op_multiline_or( - r#"1 -|| 2 -|| 3"#, - "1 - || 2 - || 3 -" - )] - #[case::binary_op_multiline( - r#"let v = 1 -|| 2 -|| 3"#, - "let v = 1 - || 2 - || 3 -" - )] - #[case::def_contains( - r#"def contains(haystack, needle): -if (is_dict(haystack)): - not(is_none(get(haystack, needle))) -else: - index(haystack, needle) != -1; -"#, - r#"def contains(haystack, needle): - if (is_dict(haystack)): - not(is_none(get(haystack, needle))) - else: - index(haystack, needle) != -1; -"# - )] - #[case::escape_sequence_clear_screen(r#""\x1b[2J\x1b[H""#, r#""\x1b[2J\x1b[H""#)] - #[case::control_character_bell(r#""\x07""#, r#""\x07""#)] - #[case::control_character_backspace(r#""\x08""#, r#""\x08""#)] - #[case::control_character_vertical_tab(r#""\x0b""#, r#""\x0b""#)] - #[case::control_character_form_feed(r#""\x0c""#, r#""\x0c""#)] - #[case::control_character_escape(r#""\x1b""#, r#""\x1b""#)] - #[case::control_character_delete(r#""\x7f""#, r#""\x7f""#)] - #[case::not_operator("!true", "!true")] - #[case::let_with_if_multiline_in_while( - r#"while(condition()): - let x = 1 - | let y = if(test): -test -else: -test2 -end -"#, - r#"while (condition()): - let x = 1 - | let y = if (test): - test - else: - test2 -end -"# - )] - #[case::let_with_while_multiline( - r#"let x = while(condition()): -process();"#, - r#"let x = while (condition()): - process(); -"# - )] - #[case::let_with_while_multiline2( - r#""test" -| let x = while(condition()): -process();"#, - r#""test" -| let x = while (condition()): - process(); -"# - )] - #[case::pipe_let_block_in_nested_do( - "def f(t):\n if (t): 1\n else: do\n let a = 1\n | let b = foreach (r, t):\n r\n end\n | let c = if (a):\n b\n else:\n a\n | c\n end\nend", - "def f(t):\n if (t): 1\n else: do\n let a = 1\n | let b = foreach (r, t):\n r\n end\n | let c = if (a):\n b\n else:\n a\n | c\n end\nend\n" - )] - #[case::fn_arg_after_pipe( - "def f(xs):\n let t = 1\n | fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y;\n )\nend", - "def f(xs):\n let t = 1\n | fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y;\n )\nend\n" - )] - #[case::fn_arg_after_pipe_in_do( - "let t = 1\n| do\n let u = 2\n | filter(xs, fn(x):\n x > u;\n )\n end", - "let t = 1\n| do\n let u = 2\n | filter(xs, fn(x):\n x > u;\n )\n end\n" - )] - #[case::array_index_access("let arr = [1, 2, 3]\n|arr[1]", "let arr = [1, 2, 3]\n| arr[1]\n")] - #[case::array_index_access_inline("arr[0]", "arr[0]")] - #[case::index_on_dict_literal_after_def("def f(x): x;\n{\"a\":1}[\"a\"]", "def f(x): x;\n{\"a\": 1}[\"a\"]\n")] - #[case::slice_on_string_literal_after_def("def f(x): x;\n\"abc\"[1:2]", "def f(x): x;\n\"abc\"[1:2]\n")] - #[case::index_on_array_literal_in_body("def f(x):\n 1\n |[1][0]\nend", "def f(x):\n 1\n | [1][0]\nend\n")] - #[case::dict_index_access( - "let d = {\"key\": \"value\"}\n|d[\"key\"]", - "let d = {\"key\": \"value\"}\n| d[\"key\"]\n" - )] - #[case::dict_index_access_inline("d[\"key\"]", "d[\"key\"]")] - #[case::comment_first_line("# comment\nlet x = 1", "# comment\nlet x = 1\n")] - #[case::string_keeps_unicode_escape(r#"let x = "\u{D7FF}\u{E000}""#, r#"let x = "\u{D7FF}\u{E000}""#)] - #[case::string_keeps_hex_escape(r#"let x = "\x41""#, r#"let x = "\x41""#)] - #[case::string_keeps_regex_escapes(r#"let x = "\d+\s\/""#, r#"let x = "\d+\s\/""#)] - #[case::string_escapes_raw_tab("let x = \"a\tb\"", r#"let x = "a\tb""#)] - #[case::string_escapes_raw_newline("let x = \"a\nb\"", "let x = \"a\\nb\"")] - #[case::dict_key_keeps_unicode_escape(r#"{"\u{E000}": 1}"#, r#"{"\u{E000}": 1}"#)] - #[case::interpolated_string_keeps_unicode_escape(r#"s"\u{E000}${x}""#, r#"s"\u{E000}${x}""#)] - #[case::string_after_multibyte_text(r#"let s = "ๆ—ฅๆœฌ่ชž" | let x = "\x41""#, r#"let s = "ๆ—ฅๆœฌ่ชž" | let x = "\x41""#)] - #[case::string_after_leading_tab("\tlet x = \"\\u{41}\"", r#"let x = "\u{41}""#)] - #[case::string_with_crlf("let x = 1\r\n| let y = \"\\u{41}\"\r\n", "let x = 1\n| let y = \"\\u{41}\"\n")] - #[case::string_keeps_quote_and_backslash(r#"let x = "a\"b\\c""#, r#"let x = "a\"b\\c""#)] - #[case::string_empty(r#"let x = """#, r#"let x = """#)] - #[case::string_multiline_keeps_escape_on_second_line("let x = \"a\n \\u{41}\"", r#"let x = "a\n \u{41}""#)] - #[case::array_keeps_escapes(r#"["\u{41}", "\x42"]"#, r#"["\u{41}", "\x42"]"#)] - #[case::match_pattern_keeps_escape( - r#"match (x): | "\u{41}": 1 | _: 2 end"#, - r#"match (x): | "\u{41}": 1 | _: 2 end"# - )] - #[case::symbol_keeps_escape(r#":"\u{41}""#, r#":"\u{41}""#)] - #[case::interpolated_string_keeps_quote_and_backslash(r#"s"\"${a}\\""#, r#"s"\"${a}\\""#)] - #[case::interpolated_string_keeps_raw_newline("s\"a\n${x}\"", "s\"a\n${x}\"\n")] - #[case::array_arg_after_pipe( - "def f():\n let r = 1\n | g(r, [\n 1,\n 2,\n ])\nend", - "def f():\n let r = 1\n | g(r, [\n 1,\n 2,\n ])\nend\n" - )] - #[case::dict_arg_after_pipe( - "def f():\n let r = 1\n | g(r, {\n \"a\": 1,\n })\nend", - "def f():\n let r = 1\n | g(r, {\n \"a\": 1,\n })\nend\n" - )] - #[case::dict_after_pipe( - "def f():\n let r = 1\n | {\n a: r\n }\nend", - "def f():\n let r = 1\n | {\n a: r\n }\nend\n" - )] - #[case::array_arg_without_pipe("def f():\n g(r, [\n 1,\n ])\nend", "def f():\n g(r, [\n 1,\n ])\nend\n")] - #[case::comment_keeps_leading_spaces("# [1, 2],\nlet x = 1", "# [1, 2],\nlet x = 1\n")] - #[case::comment_keeps_leading_tab("#\tcomment\nlet x = 1", "#\tcomment\nlet x = 1\n")] - #[case::comment_adds_space_when_missing("#comment\nlet x = 1", "# comment\nlet x = 1\n")] - #[case::comment_trims_trailing_spaces("# comment \nlet x = 1", "# comment\nlet x = 1\n")] - #[case::comment_keeps_leading_spaces_before_close("[\n 1,\n # c\n]", "[\n 1,\n # c\n]\n")] - #[case::comment_inline("let x = 1 # inline comment", "let x = 1 # inline comment")] - #[case::comment_multiline( - "let x = 1\n# multiline comment\n| let y = 2", - "let x = 1\n# multiline comment\n| let y = 2\n" - )] - #[case::comment_after_expr_multiline( - "if(test):\n test # comment\nelse:\n test2 # comment2", - "if (test):\n test # comment\nelse:\n test2 # comment2\n" - )] - #[case::comment_after_expr_inline( - "if(test): test # comment else: test2 # comment2", - "if (test): test # comment else: test2 # comment2" - )] - #[case::fn_as_call_arg_single_line("map(fn(): program;)", "map(fn(): program;)")] - #[case::fn_as_call_arg_multi_line( - "map(\n fn(arg):\n process(arg);\n)", - "map(\n fn(arg):\n process(arg);\n)\n" - )] - #[case::fn_as_call_arg_with_other_args("map(fn(): program;, 1, \"test\")", "map(fn(): program;, 1, \"test\")")] - #[case::nested_fn_as_call_arg("outer(map(fn(): inner();))", "outer(map(fn(): inner();))")] - #[case::fn_as_call_arg_with_multiline_args( - "map(\n fn(x):\n process(x);\n ,\n fn(y):\n process(y);\n)", - "map(\n fn(x):\n process(x);\n ,\n fn(y):\n process(y);\n)\n" - )] - #[case::fn_arg_without_terminator("map(xs, fn(x): x + 1)", "map(xs, fn(x): x + 1)")] - #[case::fn_arg_without_terminator_before_comma("map(fn(x): x + 1, 1, \"test\")", "map(fn(x): x + 1, 1, \"test\")")] - #[case::fn_arg_without_terminator_with_pipe( - "filter(xs, fn(x): x | select(.h), 1)", - "filter(xs, fn(x): x | select(.h), 1)" - )] - #[case::nested_fn_arg_without_terminator("outer(map(xs, fn(x): inner(x)))", "outer(map(xs, fn(x): inner(x)))")] - #[case::fn_arg_without_terminator_multi_line( - "map(\n fn(arg):\n process(arg)\n)", - "map(\n fn(arg):\n process(arg)\n)\n" - )] - #[case::fn_arg_without_terminator_after_pipe( - "let t = 1\n| fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y\n )", - "let t = 1\n| fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y\n )\n" - )] - #[case::fn_without_params("map(xs, fn: self + 1)", "map(xs, fn: self + 1)")] - #[case::fn_without_params_with_pipe( - "filter(xs, fn: select(.h || .code) | !is_none, 1)", - "filter(xs, fn: select(.h || .code) | !is_none, 1)" - )] - #[case::fn_without_params_nested("map(xs, fn: map(fn: to_text))", "map(xs, fn: map(fn: to_text))")] - #[case::fn_without_params_terminated("let f = fn: self + 1;", "let f = fn: self + 1;")] - #[case::fn_without_params_spacing("map(xs, fn : self)", "map(xs, fn: self)")] - #[case::fn_without_params_multi_line("map(\n fn:\n process(self)\n)", "map(\n fn:\n process(self)\n)\n")] - #[case::group_simple("(1)", "(1)")] - #[case::group_with_expr("(1 + 2)", "(1 + 2)")] - #[case::group_with_nested_group("((1 + 2) * 3)", "((1 + 2) * 3)")] - #[case::group_with_multiple_ops("(1 + 2 * 3)", "(1 + 2 * 3)")] - #[case::group_with_array("(array(1, 2))", "(array(1, 2))")] - #[case::group_with_dict("({\"key\": \"value\"})", "({\"key\": \"value\"})")] - #[case::group_with_comment("(1 + 2) # group comment", "(1 + 2) # group comment")] - #[case::group_with_call("(test(1, 2))", "(test(1, 2))")] - #[case::group_with_if("(if(test): test else: test2)", "(if (test): test else: test2)")] - #[case::group_with_let("(let x = 1)", "(let x = 1)")] - #[case::fn_end("fn(): test end", "fn(): test end")] - #[case::negate_operator("-v", "-v")] - #[case::try_catch_multiline( - r#"try: - process() -catch: - handle_error()"#, - "try: - process() -catch: - handle_error() -" - )] - #[case::try_catch_oneline("try: process() catch: handle_error()", "try: process() catch: handle_error()")] - #[case::try_catch_with_binder( - "try: process() catch(e): handle_error(e)", - "try: process() catch(e): handle_error(e)" - )] - #[case::try_catch_with_binder_extra_spaces( - "try: process() catch( e ): handle_error(e)", - "try: process() catch(e): handle_error(e)" - )] - #[case::try_catch_with_binder_multiline( - r#"try: - process() -catch(e): - handle_error(e)"#, - "try: - process() -catch(e): - handle_error(e) -" - )] - #[case::try_catch_with_finally( - r#"try: - process() -catch: - handle_error() -"#, - "try: - process() -catch: - handle_error() -" - )] - #[case::coalesce_operator("let x = a?? b", "let x = a ?? b")] - #[case::coalesce_operator_with_call( - "let result = get_value() ?? default()", - "let result = get_value() ?? default()" - )] - #[case::coalesce_operator_with_literal("let x = value ?? 42", "let x = value ?? 42")] - #[case::coalesce_operator_with_string("let s = str ?? \"default\"", "let s = str ?? \"default\"")] - #[case::coalesce_operator_chain("let x = a ?? b ?? c", "let x = a ?? b ?? c")] - #[case::coalesce_operator_in_if( - "if(a ?? b): do_something() else: do_other()", - "if (a ?? b): do_something() else: do_other()" - )] - #[case::coalesce_operator_in_array("[a ?? b, c]", "[a ?? b, c]")] - #[case::coalesce_operator_in_dict("{\"key\": a ?? b}", "{\"key\": a ?? b}")] - #[case::coalesce_operator_with_comment("let x = a ?? b # fallback", "let x = a ?? b # fallback")] - #[case::call_dynamic("v[0](1,2,3)", "v[0](1, 2, 3)")] - #[case::dict_with_fn_value_multiline( - r#"{ -"key1": fn(): -process(); -,"key2": "value2" -}"#, - r#"{ - "key1": fn(): - process(); - ,"key2": "value2" -} -"# - )] - #[case::do_block_multiline( - r#"do - process1() - | process2() -end -"#, - "do - process1() - | process2() -end -" - )] - #[case::do_block_oneline("do process1() | process2();", "do process1() | process2();")] - #[case::let_with_do_block_multiline( - r#"let result = do - step1() - | step2(); -"#, - "let result = do - step1() - | step2(); -" - )] - #[case::let_with_do_block_oneline("let result = do step1() | step2();", "let result = do step1() | step2();")] - #[case::symbol_with_ident(":foo", ":foo")] - #[case::symbol_with_string(r#":"bar""#, r#":"bar""#)] - #[case::symbol_with_spaces(": foo", ":foo")] - #[case::symbol_in_array("[:foo, :bar]", "[:foo, :bar]")] - #[case::symbol_in_dict(r#"{:key: "value"}"#, r#"{:key: "value"}"#)] - #[case::symbol_comparison(":foo == :bar", ":foo == :bar")] - #[case::match_simple( - "match(x): | 1: \"one\" | _: \"other\" end", - "match (x): | 1: \"one\" | _: \"other\" end" - )] - #[case::match_multiline( - r#"match(x): -| 1: "one" -| 2: "two" -| _: "other" -end"#, - r#"match (x): - | 1: "one" - | 2: "two" - | _: "other" -end -"# - )] - #[case::match_with_guard( - "match(x): | n if(n > 0): \"positive\" | _: \"non-positive\" end", - "match (x): | n if (n > 0): \"positive\" | _: \"non-positive\" end" - )] - #[case::match_with_array_pattern( - "match(arr): | [a, b]: add(a, b) | _: 0 end", - "match (arr): | [a, b]: add(a, b) | _: 0 end" - )] - #[case::match_with_array_pattern_with_literal( - "match(arr): | [1, 2]: add(1, 2) | _: 0 end", - "match (arr): | [1, 2]: add(1, 2) | _: 0 end" - )] - #[case::match_with_array_pattern_with_symbol( - "match(arr): | [:string, :string]: add(1, 2) | _: 0 end", - "match (arr): | [:string, :string]: add(1, 2) | _: 0 end" - )] - #[case::match_with_dict_pattern( - "match(obj): | {name: n}: n | _: \"unknown\" end", - "match (obj): | {name: n}: n | _: \"unknown\" end" - )] - #[case::match_nested_in_let( - r#"let result = match(x): -| 1: "one" -| 2: "two" -| _: "other" -end"#, - r#"let result = match (x): - | 1: "one" - | 2: "two" - | _: "other" -end -"# - )] - #[case::match_with_type_pattern( - "match(val): | :string: \"is string\" | :number: \"is number\" | _: \"other\" end", - "match (val): | :string: \"is string\" | :number: \"is number\" | _: \"other\" end" - )] - #[case::match_multiline_in_pipe( - r#""test" -| match(x): - | 1: "one" - | 2: "two" - end"#, - r#""test" -| match (x): - | 1: "one" - | 2: "two" - end -"# - )] - #[case::dict_nested_multiline_level3( - r#"{ -"level1": { -"level2": { -"level3": "value" -} -} -}"#, - r#"{ - "level1": { - "level2": { - "level3": "value" - } - } -} -"# - )] - #[case::array_nested_multiline_level3( - r#"[ -[ -[ -"value" -] -] -]"#, - r#"[ - [ - [ - "value" - ] - ] -] -"# - )] - #[case::comment_with_newline("# comment\nlet x = 1", "# comment\nlet x = 1\n")] - #[case::comment_with_indent( - "if(test):\n test # comment\nelse:\n test2 # comment2", - "if (test):\n test # comment\nelse:\n test2 # comment2\n" - )] - #[case::comment_inline_with_indent("let x = 1 # inline comment", "let x = 1 # inline comment")] - #[case::comment_multiline_with_indent( - "let x = 1\n # multiline comment\n| let y = 2", - "let x = 1\n# multiline comment\n| let y = 2\n" - )] - #[case::comment_after_expr_multiline_with_indent( - "if(test):\n test # comment\nelse:\n test2 # comment2", - "if (test):\n test # comment\nelse:\n test2 # comment2\n" - )] - #[case::comment_after_expr_inline_with_indent( - "if(test): test # comment else: test2 # comment2", - "if (test): test # comment else: test2 # comment2" - )] - #[case::interpolated_string_with_escaped_brackets(r#"s"\\[${phrase}\\]\\(""#, r#"s"\\[${phrase}\\]\\(""#)] - #[case::interpolated_string_with_backslash(r#"s"\\test""#, r#"s"\\test""#)] - #[case::module_with_body( - r#"module test: -import "foo.mq" -| def main(): test(); -end"#, - r#"module test: - import "foo.mq" - | def main(): test(); -end -"# - )] - #[case::module_with_do( - r#"module test do -import "foo.mq" -| def main(): test(); -end"#, - r#"module test do - import "foo.mq" - | def main(): test(); -end -"# - )] - #[case::module_with_do_and_new_line( - r#"module test -do -import "foo.mq" -| def main(): test(); -end"#, - r#"module test - do - import "foo.mq" - | def main(): test(); -end -"# - )] - #[case::comment_preserves_indent_after_newline( - "let x = 1\n # indented comment after newline\n| let y = 2", - "let x = 1\n# indented comment after newline\n| let y = 2\n" - )] - #[case::comment_preserves_indent_after_newline_deep( - "if(test):\n test\n # deeper indented comment\nelse:\n test2", - "if (test):\n test\n# deeper indented comment\nelse:\n test2\n" - )] - #[case::comment_preserves_indent_after_newline_array( - "[1,\n # comment for 2\n 2]", - "[1,\n # comment for 2\n 2]\n" - )] - #[case::comment_preserves_indent_after_newline_dict( - "{\n \"a\": 1,\n # comment for b\n \"b\": 2\n}", - "{\n \"a\": 1,\n # comment for b\n \"b\": 2\n}\n" - )] - #[case::match_preserves_indent_after_newline( - "let v = \nmatch (x):\n| 1: \"one\"\n| 2: \"two\"\n end", - "let v =\n match (x):\n | 1: \"one\"\n | 2: \"two\"\n end\n" - )] - #[case::match_with_do_block( - "let result = match (x):\n| 1: do\n foo() |\n bar()\n end\n| 2: \"two\"\nend", - "let result = match (x):\n | 1: do\n foo() |\n bar()\n end\n | 2: \"two\"\nend\n" - )] - #[case::assign("var i=0 | i=i + 1", "var i = 0 | i = i + 1")] - #[case::assign_right_multiline( - r#"let x = -"test""#, - r#"let x = - "test" -"# - )] - #[case::assign_right_multiline_pipe( - r#"let x = -"test" -| upcase()"#, - r#"let x = - "test" -| upcase() -"# - )] - #[case::assign_right_multiline_if( - r#"let x = -if(test): -test -else: -test2"#, - r#"let x = - if (test): - test - else: - test2 -"# - )] - #[case::if_without_colon_oneline("if(test) 1 else 2", "if (test) 1 else 2")] - #[case::if_without_colon_multiline( - r#"if(test) -test -else -test2"#, - r#"if (test) - test -else - test2 -"# - )] - #[case::if_elif_else_without_colon_oneline("if(test) 1 elif(test2) 2 else 3", "if (test) 1 elif (test2) 2 else 3")] - #[case::if_elif_else_without_colon_multiline( - r#"if(test) -test -elif(test2) -test2 -else -test3"#, - r#"if (test) - test -elif (test2) - test2 -else - test3 -"# - )] - #[case::if_without_colon_with_do_block( - r#"if(test) do -test -end -else do -test2 -end"#, - r#"if (test) do - test - end -else do - test2 - end -"# - )] - #[case::while_do_end_oneline("while(x > 0) do x - 1 end", "while (x > 0) do x - 1 end")] - #[case::while_do_end_multiline( - r#"while(x > 0) do -let x = x - 1 | x -end"#, - r#"while (x > 0) do - let x = x - 1 | x -end -"# - )] - #[case::while_do_end_with_break( - r#"while(x < 10) do -let x = x + 1 -| if(x == 3): -break -else: -x -end"#, - r#"while (x < 10) do - let x = x + 1 - | if (x == 3): - break - else: - x -end -"# - )] - #[case::foreach_do_end_oneline( - "foreach(item, arr) do process(item) end", - "foreach (item, arr) do process(item) end" - )] - #[case::foreach_do_end_multiline( - r#"foreach(x, array(1, 2, 3)) do -add(x, 1) -end"#, - r#"foreach (x, array(1, 2, 3)) do - add(x, 1) -end -"# - )] - #[case::foreach_do_end_with_continue( - r#"foreach(x, array(1, 2, 3, 4, 5)) do -if(x == 3): -continue -else: -x + 10 -end"#, - r#"foreach (x, array(1, 2, 3, 4, 5)) do - if (x == 3): - continue - else: - x + 10 -end -"# - )] - #[case::foreach_do_end_nested( - r#"foreach(row, arr) do -foreach(x, row) do -x * 2 -end -end"#, - r#"foreach (row, arr) do - foreach (x, row) do - x * 2 - end -end -"# - )] - #[case::match_do_end_oneline( - "match(x) do | 1: \"one\" | 2: \"two\" | _: \"other\" end", - "match (x) do | 1: \"one\" | 2: \"two\" | _: \"other\" end" - )] - #[case::match_do_end_multiline( - r#"match(2) do -| 1: "one" -| 2: "two" -| _: "other" -end"#, - r#"match (2) do - | 1: "one" - | 2: "two" - | _: "other" -end -"# - )] - #[case::match_do_end_type_pattern( - r#"match(array(1, 2, 3)) do -| :array: "is_array" -| :number: "is_number" -| _: "other" -end"#, - r#"match (array(1, 2, 3)) do - | :array: "is_array" - | :number: "is_number" - | _: "other" -end -"# - )] - #[case::match_do_end_with_guard( - r#"match(x) do -| n if(n > 0): "positive" -| _: "non-positive" -end"#, - r#"match (x) do - | n if (n > 0): "positive" - | _: "non-positive" -end -"# - )] - #[case::def_with_default_single( - "def greet(name = \"World\"): - name;", - "def greet(name = \"World\"): - name; -" - )] - #[case::def_with_multiple_defaults( - "def foo(a, b = 2, c = 3): - a + b + c;", - "def foo(a, b = 2, c = 3): - a + b + c; -" - )] - #[case::def_with_mixed_params( - "def bar(x,y=10): - x+y;", - "def bar(x, y = 10): - x + y; -" - )] - #[case::def_with_array_default( - "def baz(x,y=[1,2,3]): - x+len(y);", - "def baz(x, y = [1, 2, 3]): - x + len(y); -" - )] - #[case::def_with_ident_default( - "def f(x = y): - x;", - "def f(x = y): - x; -" - )] - #[case::def_with_string_default( - "def test(a,b=\"test\"): - a+b;", - "def test(a, b = \"test\"): - a + b; -" - )] - #[case::def_with_number_default( - "def calc(x=42): - x * 2;", - "def calc(x = 42): - x * 2; -" - )] - #[case::def_with_boolean_default( - "def result(x=true): - x;", - "def result(x = true): - x; -" - )] - #[case::def_all_params_with_defaults( - "def calc(a=1,b=2,c=3): - a+b+c;", - "def calc(a = 1, b = 2, c = 3): - a + b + c; -" - )] - #[case::def_expr_with_default( - "def calc(a=1,b=2,c=3 + 1): - a+b+c;", - "def calc(a = 1, b = 2, c = 3 + 1): - a + b + c; -" - )] - #[case::def_expr_with_variadic( - "def calc(v,*args): - v + args;", - "def calc(v, *args): - v + args; -" - )] - #[case::assign_with_selector_attr("value.test|= value.attr", "value.test |= value.attr")] - #[case::index_assign("arr[0]=10", "arr[0] = 10")] - #[case::index_assign_string_key("dict[\"key\"]=\"value\"", "dict[\"key\"] = \"value\"")] - #[case::index_compound_assign("arr[0]+=1", "arr[0] += 1")] - #[case::selector_call_heading_single_arg(".h(1)", ".h(1)")] - #[case::selector_call_heading_multi_arg(".h(1,2)", ".h(1, 2)")] - #[case::selector_call_code_lang(".code(\"rust\")", ".code(\"rust\")")] - #[case::selector_call_pipe(".h(1)|.text()", ".h(1) | .text()")] - #[case::descendant_chain(".blockquote .code", ".blockquote .code")] - #[case::descendant_chain_no_space(".blockquote.code", ".blockquote .code")] - #[case::descendant_chain_extra_space(".blockquote .code", ".blockquote .code")] - #[case::descendant_chain_three_levels(".blockquote .list .code", ".blockquote .list .code")] - #[case::descendant_chain_with_selector_call(".blockquote .code(\"rust\")", ".blockquote .code(\"rust\")")] - #[case::descendant_chain_trailing_attr(".blockquote .code.lang", ".blockquote .code.lang")] - #[case::arrow_simple("->(): program;", "->(): program;")] - #[case::arrow_multiline( - "->(arg1,arg2): - program;", - "->(arg1, arg2): - program; -" - )] - #[case::arrow_as_call_arg("map( ->():program;)", "map(->(): program;)")] - #[case::arrow_as_call_arg_single_line("map(->(): program;)", "map(->(): program;)")] - #[case::arrow_as_call_arg_with_other_args("map(->(): program;, 1, \"test\")", "map(->(): program;, 1, \"test\")")] - #[case::nested_arrow_as_call_arg("outer(map(->(): inner();))", "outer(map(->(): inner();))")] - #[case::arrow_end("->(): test end", "->(): test end")] - #[case::bytes_literal_basic(r#"b"abc""#, r#"b"abc""#)] - #[case::bytes_literal_hex(r#"b"\xf0\x9f\x99\x82""#, r#"b"\xf0\x9f\x99\x82""#)] - #[case::bytes_literal_with_pipe(r#"b"abc" | len"#, r#"b"abc" | len"#)] - #[case::bytes_literal_in_call(r#"len(b"abc")"#, r#"len(b"abc")"#)] - #[case::as_binding_basic("42 as x | x", "42 as x | x")] - #[case::as_binding_spaces("42 as x | x", "42 as x | x")] - #[case::as_binding_selector(".text as title | title", ".text as title | title")] - #[case::def_with_do_block_body( - r#"def test(x): - do - step1(x) - | step2(x) - end;"#, - r#"def test(x): - do - step1(x) - | step2(x) - end; -"# - )] - #[case::foreach_colon_with_do_block_body( - r#"foreach(x, items): - do - process(x) - end;"#, - r#"foreach (x, items): - do - process(x) - end; -"# - )] - #[case::foreach_do_with_nested_do_block( - r#"foreach(row, arr) do - do - process(row) - end -end"#, - r#"foreach (row, arr) do - do - process(row) - end -end -"# - )] - #[case::fn_multiline_params_as_first_array_element( - "let fns = [fn( - a, - b -): a + b;, fn(c): c;]", - "let fns = [fn( - a, - b -): a + b;, fn(c): c;] -" - )] - #[case::fn_multiline_variadic_params_as_first_array_element( - "let fns = [fn( - a, - *rest -): rest;, fn(c): c;]", - "let fns = [fn( - a, - *rest -): rest;, fn(c): c;] -" - )] - #[case::fn_multiline_params_as_non_first_array_element( - "let fns = [first, fn( - a, - b -): a + b;]", - "let fns = [first, fn( - a, - b -): a + b;] -" - )] - #[case::fn_multiline_params_in_nested_array( - "let xs = [[fn( - a, - b -): a + b;]]", - "let xs = [[fn( - a, - b -): a + b;]] -" - )] - #[case::fn_multiline_params_as_call_arg( - "map(arr, fn( - a, - b -): a + b;)", - "map(arr, fn( - a, - b -): a + b;) -" - )] - #[case::qualified_call_multiline_args_nested_in_call( - "let result = to_string(md::doc( - md::h(\"Items\", 2), - map([\"a\", \"b\"], fn(x): md::list(x);), - ))", - "let result = to_string(md::doc( - md::h(\"Items\", 2), - map([\"a\", \"b\"], fn(x): md::list(x);), -)) -" - )] - #[case::yield_oneline("def g(): yield: 1;", "def g(): yield: 1;")] - #[case::yield_bare("def g(): yield;", "def g(): yield;")] - #[case::break_with_value("while (true): break: 1;", "while (true): break: 1;")] - #[case::yield_multiline( - "def g(): - yield: 1 - | yield: 2;", - "def g(): - yield: 1 - | yield: 2; -" - )] - #[case::def_with_comment_before_yield( - "def g(): - # a comment - yield: 1;", - "def g(): - # a comment - yield: 1; -" - )] - fn test_format(#[case] code: &str, #[case] expected: &str) { - let result = Formatter::new(None).format(code); - assert_eq!(result.unwrap(), expected); - } - - /// Builds a `def` whose block expression sits `depth` `do`s deep after `prefix` - /// (`"let b = "`, `"| let b = "` or `"| "`), indented as expected: the block body goes - /// one level below where the expression's line content starts (after any `| `). - fn block_program(block: &[(usize, &str)], prefix: &str, depth: usize, indent_width: usize) -> String { - let pad = |level: usize| " ".repeat(level * indent_width); - let line_indent = depth + 1; - let piped = prefix.starts_with('|'); - let base = line_indent + usize::from(piped); - let mut lines = vec!["def f(t):".to_string()]; - for d in 0..depth { - lines.push(format!("{}do", pad(d + 1))); - } - if piped { - lines.push(format!("{}let a = 1", pad(line_indent))); - } - lines.push(format!("{}{prefix}{}", pad(line_indent), block[0].1)); - for (level, text) in &block[1..] { - lines.push(format!("{}{text}", pad(base + level))); - } - if prefix.contains("let") { - lines.push(format!("{}| b", pad(line_indent))); - } - for d in (0..depth).rev() { - lines.push(format!("{}end", pad(d + 1))); - } - lines.push("end".to_string()); - lines.join("\n") + "\n" - } - - #[rstest] - #[case::foreach(&[(0, "foreach (r, t):"), (1, "r"), (0, "end")])] - #[case::while_(&[(0, "while (t):"), (1, "r"), (0, "end")])] - #[case::until(&[(0, "until (t):"), (1, "r"), (0, "end")])] - #[case::loop_(&[(0, "loop:"), (1, "break"), (0, "end")])] - #[case::do_(&[(0, "do"), (1, "r"), (0, "end")])] - #[case::fn_(&[(0, "fn(x):"), (1, "x"), (0, "end")])] - #[case::if_else(&[(0, "if (t):"), (1, "1"), (0, "else:"), (1, "2")])] - #[case::if_elif(&[(0, "if (t):"), (1, "1"), (0, "elif (t):"), (1, "2"), (0, "else:"), (1, "3")])] - #[case::match_(&[(0, "match (t):"), (1, "| 1: \"one\""), (1, "| _: \"other\""), (0, "end")])] - #[case::call_with_fn(&[(0, "map(t, fn(x):"), (1, "let y = x"), (1, "| y;"), (0, ")")])] - #[case::array(&[(0, "["), (1, "1,"), (1, "2"), (0, "]")])] - #[case::dict(&[(0, "{"), (1, "\"a\": 1,"), (1, "\"b\": 2"), (0, "}")])] - #[case::call_with_array(&[(0, "g(t, ["), (1, "1,"), (1, "2"), (0, "])")])] - #[case::call_with_dict(&[(0, "g(t, {"), (1, "\"a\": 1"), (0, "})")])] - #[case::array_of_dicts(&[(0, "g(t, ["), (1, "{\"a\": 1},"), (1, "{\"b\": 2},"), (0, "])")])] - #[case::nested_array_in_dict(&[(0, "{"), (1, "\"a\": ["), (2, "1"), (1, "]"), (0, "}")])] - #[case::array_comment_before_close(&[(0, "g(t, ["), (1, "1,"), (1, "# c"), (0, "])")])] - fn test_format_block_indent( - #[case] block: &[(usize, &str)], - #[values("let b = ", "| let b = ", "| ")] prefix: &str, - #[values(0, 1, 2)] depth: usize, - #[values(2, 4)] indent_width: usize, - ) { - let expected = block_program(block, prefix, depth, indent_width); - let flattened = expected.lines().map(str::trim_start).collect::>().join("\n"); - let format = |code: &str| { - Formatter::new(Some(FormatterConfig { - indent_width, - ..FormatterConfig::default() - })) - .format(code) - .unwrap() - }; - - assert_eq!(format(&expected), expected, "not idempotent"); - assert_eq!(format(&flattened), expected, "from flattened input"); - } - - #[rstest] - #[case::sort_imports( - r#"import "c.mq" -| import "a.mq" -| import "b.mq""#, - r#"import "a.mq" -| import "b.mq" -| import "c.mq" -"# - )] - #[case::sort_imports_with_alias( - r#"import "c.mq" -| import "a.mq" as a -| import "b.mq""#, - r#"import "a.mq" as a -| import "b.mq" -| import "c.mq" -"# - )] - #[case::sort_functions( - r#"def z(): test; -def a(): test; -def m(): test; -"#, - "def a(): test;\ndef m(): test;\ndef z(): test;\n" - )] - #[case::sort_fields( - r#"let z = 1 -| let a = 2 -| let m = 3"#, - r#"let a = 2 -| let m = 3 -| let z = 1 -"# - )] - #[case::sort_mixed( - r#"let z = 1 -| import "b.mq" -def y(): test; -| let a = 2 -| import "a.mq" -def b(): test;"#, - "import \"a.mq\"\n| import \"b.mq\"\n| let a = 2\n| let z = 1\n|\ndef b(): test;\ndef y(): test;\n" - )] - #[case::sort_with_other( - r#"def z(): test; -| let x = 1 -| nodes -def a(): test;"#, - "let x = 1\n|\ndef a(): test;\ndef z(): test;\n| nodes\n" - )] - #[case::sort_with_piped_calls( - r#"def z(): test; -| let x = 1 -| add() | mul() | sub() -def a(): test; -| let y = 2"#, - "let x = 1\n| let y = 2\n|\ndef a(): test;\ndef z(): test;\n| add()\n| mul()\n| sub()\n" - )] - #[case::sort_all_types( - r#"add() | sub() -def func_b(): test; -| let var_z = 1 -| import "z.mq" -| let var_a = 2 -def func_a(): test; -| mul() | div() -| import "a.mq""#, - "import \"a.mq\"\n| import \"z.mq\"\n| let var_a = 2\n| let var_z = 1\n|\ndef func_a(): test;\ndef func_b(): test;\n| add()\n| sub()\n| mul()\n| div()\n" - )] - fn test_format_with_sort(#[case] code: &str, #[case] expected: &str) { - let config = FormatterConfig { - indent_width: 2, - sort_imports: true, - sort_functions: true, - sort_fields: true, - max_width: None, - }; - let result = Formatter::new(Some(config)).format(code); - assert_eq!(result.unwrap(), expected); - } - - #[rstest] - #[case::wraps_top_level_pipeline( - "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\")", - 40, - "select(\"h1\") | upcase() | add_class(\"title\")\n| trim() | to_text() | replace(\"a\", \"b\")\n| join(\",\")\n" - )] - #[case::wraps_pipeline_inside_def( - "def foo(x):\n select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\");", - 40, - "def foo(x):\n select(\"h1\") | upcase() | add_class(\"title\")\n | trim() | to_text() | replace(\"a\", \"b\")\n | join(\",\");\n" - )] - #[case::keeps_short_pipeline_inline("select(\"h1\") | upcase()", 80, "select(\"h1\") | upcase()")] - fn test_format_with_max_width(#[case] code: &str, #[case] max_width: usize, #[case] expected: &str) { - let config = FormatterConfig { - max_width: Some(max_width), - ..Default::default() - }; - let result = Formatter::new(Some(config)).format(code); - assert_eq!(result.unwrap(), expected); - } - - #[test] - fn test_format_with_max_width_is_idempotent() { - let code = "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\")"; - let config = || FormatterConfig { - max_width: Some(40), - ..Default::default() - }; - let once = Formatter::new(Some(config())).format(code).unwrap(); - let twice = Formatter::new(Some(config())).format(&once).unwrap(); - assert_eq!(once, twice); - } - - #[test] - fn test_format_without_max_width_does_not_wrap() { - let code = "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\", \"b\") | join(\",\")"; - let result = Formatter::default().format(code); - assert_eq!(result.unwrap(), code); - } - - #[test] - fn test_format_yield_is_idempotent() { - for code in ["def g(): yield: 1;", "def g(): yield: 1 | yield: 2;"] { - let once = Formatter::new(None).format(code).unwrap(); - let twice = Formatter::new(None).format(&once).unwrap(); - assert_eq!(once, twice, "not idempotent for {code:?}"); - } - } - - const FORMAT_STATEMENTS: &[&str] = &[ - "def f(x):\n x + 1;", - "let a = [1,2]", - "if(a):1 elif(b):2 else:3", - "foreach(v,a):v;", - "match(x): | 1: :a | _: :b end", - "{\"k\": 1}", - ".h1", - "try: 1 catch(e): e", - "fn(x): x;", - "while(a): break;", - "a[0](1)", - "1 as n", - ]; - - /// The previous allocating `is_let_line`, kept as an oracle. - fn reference_is_let_line(output: &str) -> bool { - let start = output.rfind('\n').map_or(0, |pos| pos + 1); - if start < output.len() { - let last_line = &output[start..]; - (!last_line.starts_with("let ") && last_line.trim().starts_with("let ")) - || last_line.trim().replace(" ", "").starts_with("|let") - } else { - false - } - } - - fn is_let_line(output: &str) -> bool { - let mut formatter = Formatter::new(None); - formatter.output = output.to_string(); - formatter.is_let_line() - } - - #[rstest] - #[case::top_level_let("let x = 1", false)] - #[case::indented_let(" let x = 1", true)] - #[case::piped_let("| let x", true)] - #[case::piped_let_no_space("|let x", true)] - #[case::piped_let_spaced("| let x", true)] - #[case::last_line_only("let a = 1\n | let b", true)] - #[case::previous_line_ignored(" let a\nfoo", false)] - #[case::trailing_newline(" let a\n", false)] - #[case::empty("", false)] - #[case::not_let(" letter", false)] - #[case::pipe_other("| foo", false)] - fn test_is_let_line(#[case] output: &str, #[case] expected: bool) { - assert_eq!(is_let_line(output), expected); - assert_eq!(reference_is_let_line(output), expected); - } - - #[test] - fn test_format_with_cst_normalizes_escapes_without_source() { - let (mut nodes, _) = mq_lang::parse_recovery(r#""\u{41}\t""#); - let result = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); - assert_eq!(result, r#""A\t""#); - } - - /// Comments before a terminating `;` are kept, and formatting them again changes nothing. - /// Comments between an expression and its postfix `[`/`(` on the next line are kept. - #[rstest::rstest] - #[case::index("x # note\n[0]", "x # note\n[0]\n")] - #[case::call("x # note\n(1)", "x # note\n(1)\n")] - #[case::index_after_call("f(x) # note\n[0]", "f(x) # note\n[0]\n")] - #[case::qualified_call("mod::f # note\n(1)", "mod::f # note\n(1)\n")] - #[case::in_def("def g():\n x # note\n [0]\n;", "def g():\n x # note\n [0];\n")] - fn test_format_keeps_comment_before_postfix(#[case] code: &str, #[case] expected: &str) { - let once = Formatter::new(None).format(code).unwrap(); - assert_eq!(once, expected); - assert_eq!(Formatter::new(None).format(&once).unwrap(), once); - } - - #[rstest::rstest] - #[case::own_line_in_def("def f():\n 1\n # note\n;", "def f():\n 1\n # note\n;\n")] - #[case::trailing_in_def("def f():\n 1 # trail\n;", "def f():\n 1 # trail\n;\n")] - #[case::after_let("let x = 1\n# c\n;", "let x = 1\n# c\n;\n")] - #[case::in_text("# a ; b\nlet x = 1", "# a ; b\nlet x = 1\n")] - fn test_format_keeps_comment_before_semicolon(#[case] code: &str, #[case] expected: &str) { - let once = Formatter::new(None).format(code).unwrap(); - assert_eq!(once, expected); - assert_eq!(Formatter::new(None).format(&once).unwrap(), once); - } - - /// Unparsable source in an `Error` node is emitted as written, strings included. - #[rstest::rstest] - #[case::junk_string("foo(1 \"a b\", 2)", "foo(1 \"a b\", 2)")] - #[case::unknown_token("foo(1, ยง, 2)", "foo(1, ยง, 2)")] - #[case::missing_paren("foo(1, 2", "foo(1, 2")] - #[case::array_final_newline("[1\n", "[1\n")] - #[case::array_items_final_newline("[1, 2\n", "[1, 2\n")] - #[case::dict_final_newline("{\"a\": 1\n", "{\"a\": 1\n")] - #[case::call_final_newline("foo(1\n", "foo(1\n")] - fn test_format_with_cst_keeps_broken_source(#[case] code: &str, #[case] expected: &str) { - let (mut nodes, errors) = mq_lang::parse_recovery(code); - assert!(errors.has_errors()); - - let result = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); - - assert_eq!(result, expected); - } - - /// A comment before an absent closer at EOF is kept exactly once. - #[rstest::rstest] - #[case::call("foo(1 # note\n")] - #[case::array("[1 # note\n")] - #[case::dict("{\"a\": 1 # note\n")] - #[case::call_own_line("foo(1,\n # note\n")] - #[case::array_own_line("[1,\n # note\n")] - #[case::dict_own_line("{\"a\": 1,\n # note\n")] - fn test_format_with_cst_keeps_comment_before_missing_closer(#[case] code: &str) { - let (mut nodes, errors) = mq_lang::parse_recovery(code); - assert!(errors.has_errors()); - - let result = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); - - assert_eq!(result.matches("# note").count(), 1, "{code:?} -> {result:?}"); - } - - #[test] - fn test_format_sort_fields_keeps_escapes() { - let config = FormatterConfig { - sort_fields: true, - ..FormatterConfig::default() - }; - let result = Formatter::new(Some(config)) - .format("let b = \"\\u{41}\"\n| let a = {\"\\u{E000}\": \"\\x42\"}") - .unwrap(); - assert_eq!(result, "let a = {\"\\u{E000}\": \"\\x42\"}\n| let b = \"\\u{41}\"\n"); - } - - /// Escape sequences and raw characters a string literal can contain. - const STRING_PIECES: &[&str] = &[ - "a", - "Z", - "0", - " ", - "ๆ—ฅ", - "รฉ", - "๐Ÿ˜€", - "\\n", - "\\t", - "\\r", - "\\\"", - "\\\\", - "\\/", - "\\d", - "\\s", - "\\(", - "\\{", - "\\x41", - "\\x1b", - "\\u{41}", - "\\u{D7FF}", - "\\u{E000}", - "\\u{10FFFF}", - ]; - - proptest::proptest! { - /// A string literal is written back exactly as in the source. - #[test] - fn prop_string_literal_is_preserved( - pieces in proptest::collection::vec(proptest::sample::select(STRING_PIECES), 0..12), - prefix in proptest::sample::select(&["let x = ", "| ", "f(", "[", "{\"k\": ", "s"][..]), - ) { - let body = pieces.concat(); - let suffix = match prefix { - "f(" => ")", - "[" => "]", - "{\"k\": " => "}", - _ => "", - }; - let code = format!("{prefix}\"{body}\"{suffix}"); - let code = code.strip_prefix("| ").unwrap_or(&code).to_string(); - // Interpolated strings accept fewer escapes than plain strings. - proptest::prop_assume!(!mq_lang::parse_recovery(&code).1.has_errors()); - let once = Formatter::new(None).format(&code).unwrap(); - proptest::prop_assert!(once.contains(&format!("\"{body}\"")), "{code:?} -> {once:?}"); - let twice = Formatter::new(None).format(&once).unwrap(); - proptest::prop_assert_eq!(twice, once); - } - - /// Whitespace after `#` is kept; only a missing space is added. - #[test] - fn prop_comment_keeps_leading_whitespace(text in "[ \t]{0,3}[a-z\\[\\],\" ]{0,12}") { - let formatted = Formatter::new(None).format(&format!("#{text}\nlet x = 1")).unwrap(); - let text = text.trim_end(); - let expected = if text.is_empty() || text.starts_with([' ', '\t']) { - format!("#{text}") - } else { - format!("# {text}") - }; - proptest::prop_assert_eq!(formatted.lines().next().unwrap(), expected.trim_end()); - } - - #[test] - fn prop_is_let_line_matches_reference(output in "[ |let\\na-z\\t]{0,24}") { - proptest::prop_assert_eq!(is_let_line(&output), reference_is_let_line(&output)); - } - - /// Formatting through a pre-parsed CST matches formatting from source. - #[test] - fn prop_format_with_cst_matches_format( - stmts in proptest::collection::vec(proptest::sample::select(FORMAT_STATEMENTS), 1..6) - ) { - let code = stmts.join(" | "); - let expected = Formatter::new(None).format(&code).unwrap(); - let (mut nodes, _) = mq_lang::parse_recovery(&code); - let actual = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); - proptest::prop_assert_eq!(actual, expected); - } - - #[test] - fn prop_format_is_idempotent( - stmts in proptest::collection::vec(proptest::sample::select(FORMAT_STATEMENTS), 1..6) - ) { - let once = Formatter::new(None).format(&stmts.join(" | ")).unwrap(); - let twice = Formatter::new(None).format(&once).unwrap(); - proptest::prop_assert_eq!(twice, once); - } - } -} +mod tests; diff --git a/crates/mq-formatter/src/formatter/tests.rs b/crates/mq-formatter/src/formatter/tests.rs new file mode 100644 index 000000000..8b34cf584 --- /dev/null +++ b/crates/mq-formatter/src/formatter/tests.rs @@ -0,0 +1,1727 @@ +use super::*; +use rstest::rstest; + +#[rstest] +#[case::empty("", "")] +#[case::if_( + "if(test): + test + else: + test2", + "if (test): + test +else: + test2 +" +)] +#[case::def_( + "def name(test): + test | test2;", + "def name(test): + test | test2; +" +)] +#[case::foreach_( + "foreach(x,array(1, 2, 3)): + add(x, 1);", + "foreach (x, array(1, 2, 3)): + add(x, 1); +" +)] +#[case::def_( + "def test(): + test1 + |test2 + |test3;", + "def test(): + test1 + | test2 + | test3; +" +)] +#[case("test()|test2()", "test() | test2()")] +#[case( + "if(test): + test + elif(test2): + test2 + else: + test3", + "if (test): + test +elif (test2): + test2 +else: + test3 +" +)] +#[case::if_( + "if(test): + test + elif(test2): + test2 + else: + test3", + "if (test): + test +elif (test2): + test2 +else: + test3 +" +)] +#[case::if_( + "if(test): + test + else: + test2", + "if (test): + test +else: + test2 +" +)] +#[case::if_else( + "if(test): + test + else: do + test2 + end", + "if (test): + test +else: do + test2 + end +" +)] +#[case::one_line("if(test): test else: test2", "if (test): test else: test2")] +#[case::one_line( + "if(test): test elif(test2): test2 else: test3", + "if (test): test elif (test2): test2 else: test3" +)] +#[case::foreach_one_line("foreach(x,array(1,2,3)):add(x,1);", "foreach (x, array(1, 2, 3)): add(x, 1);")] +#[case::foreach_one_line( + "foreach(x,array(1,2,3)):add(x,1);|add(1,2);", + "foreach (x, array(1, 2, 3)): add(x, 1); | add(1, 2);" +)] +#[case::foreach_one_line(".[]|upcase()", ".[] | upcase()")] +#[case::while_multiline( + "while(condition()): + process();", + "while (condition()): + process(); +" +)] +#[case::while_oneline("while(condition()): process();", "while (condition()): process();")] +#[case::while_with_pipe( + "while(check_condition()): + data + | process() + | output();", + "while (check_condition()): + data + | process() + | output(); +" +)] +#[case::loop_multiline( + "loop: + process();", + "loop: + process(); +" +)] +#[case::loop_oneline("loop: process();", "loop: process();")] +#[case::loop_with_pipe( + "loop: + data + | process() + | output();", + "loop: + data + | process() + | output(); +" +)] +#[case::loop_with_break( + "loop: + add(self,1) + |if(gt(self,5)):break;;", + "loop: + add(self, 1) + | if (gt(self, 5)): break;; +" +)] +#[case::until_multiline( + "until(condition()): + process();", + "until (condition()): + process(); +" +)] +#[case::until_oneline("until(condition()): process();", "until (condition()): process();")] +#[case::until_with_pipe( + "until(check_condition()): + data + | process() + | output();", + "until (check_condition()): + data + | process() + | output(); +" +)] +#[case::until_with_break( + "until(false): + add(self,1) + |if(gt(self,5)):break;;", + "until (false): + add(self, 1) + | if (gt(self, 5)): break;; +" +)] +#[case::unless_multiline( + "unless(test): + test", + "unless (test): + test +" +)] +#[case::unless_oneline("unless(test): test", "unless (test): test")] +#[case::def( + r##".h +| let link = to_link(add("#", to_text(self)), to_text(self), ""); +| if (eq(to_md_name(), "h1")): +to_md_list(link, 1) +elif (eq(to_md_name(),"h2")): +to_md_list(link, 2) +elif (eq(to_md_name(), "h3")): +to_md_list(link, 3) +elif (eq(to_md_name(), "h4")): +to_md_list(link, 4) +elif (eq(to_md_name(), "h5")): +to_md_list(link, 5) +else: +None"##, + r##".h +| let link = to_link(add("#", to_text(self)), to_text(self), ""); +| if (eq(to_md_name(), "h1")): + to_md_list(link, 1) + elif (eq(to_md_name(), "h2")): + to_md_list(link, 2) + elif (eq(to_md_name(), "h3")): + to_md_list(link, 3) + elif (eq(to_md_name(), "h4")): + to_md_list(link, 4) + elif (eq(to_md_name(), "h5")): + to_md_list(link, 5) + else: + None +"## +)] +#[case::def( + r#"def snake_to_camel(x): + let words = split(x, "_") + | foreach (word, words): + let first_char = upcase(first(word)) + | let rest_str = downcase(slice(word, 1, len(word))) + | s"${first_char}${rest_str}"; + | join(""); +| snake_to_camel()"#, + r#"def snake_to_camel(x): + let words = split(x, "_") + | foreach (word, words): + let first_char = upcase(first(word)) + | let rest_str = downcase(slice(word, 1, len(word))) + | s"${first_char}${rest_str}"; + | join(""); +| snake_to_camel() +"# +)] +#[case::def( + r#"def snake_to_camel(x): let words = split(x, "_") | foreach (word, words): let first_char = upcase(first(word)) | let rest_str = downcase(slice(word, 1, len(word))) | add(first_char, rest_str); | join("");| snake_to_camel()"#, + r#"def snake_to_camel(x): let words = split(x, "_") | foreach (word, words): let first_char = upcase(first(word)) | let rest_str = downcase(slice(word, 1, len(word))) | add(first_char, rest_str); | join(""); | snake_to_camel()"# + )] +#[case::let_(r#"let test = "test""#, r#"let test = "test""#)] +#[case::call( + r#"test( +"test")"#, + r#"test( + "test") +"# +)] +#[case::call( + r#"test( +"test" + )"#, + r#"test( + "test" +) +"# +)] +#[case::call( + r#"test( +"test" +,"test" +,true + )"#, + r#"test( + "test" + ,"test" + ,true +) +"# +)] +#[case::interpolated_string( + r#"test( +s"test${val1}" + )"#, + r#"test( + s"test${val1}" +) +"# +)] +#[case::include("include \"test.mq\"", "include \"test.mq\"")] +#[case::import("import \"test.mq\"", "import \"test.mq\"")] +#[case::import_as("import \"test.mq\" as m", "import \"test.mq\" as m")] +#[case::nodes("nodes|nodes", "nodes | nodes")] +#[case::fn_("fn(): program;", "fn(): program;")] +#[case::fn_multiline( + "fn(arg1,arg2): + program;", + "fn(arg1, arg2): + program; +" +)] +#[case::fn_args("map( fn():program;)", "map(fn(): program;)")] +#[case::array_trailing_comma("[1, 2,]", "[1, 2,]")] +#[case::dict_trailing_comma("{\"a\": 1,}", "{\"a\": 1,}")] +#[case::dict_trailing_comma_multiline("{\n \"a\": 1,\n \"b\": 2,\n}", "{\n \"a\": 1,\n \"b\": 2,\n}\n")] +#[case::array_comment_before_close("[\n 1,\n # c\n]", "[\n 1,\n # c\n]\n")] +#[case::dict_comment_before_close("{\n \"a\": 1\n # c\n}", "{\n \"a\": 1\n # c\n}\n")] +#[case::array_same_line_comment_before_close("[1, 2, # c\n]", "[1, 2, # c\n]\n")] +#[case::array_comments_before_close("[\n 1,\n\n # a\n # b\n]", "[\n 1,\n # a\n # b\n]\n")] +#[case::number_small_fraction("1e-9", "0.000000001")] +#[case::number_long_fraction("3.14159265358979", "3.14159265358979")] +#[case::number_integral_float("2.0", "2")] +#[case::array_empty("[]", "[]")] +#[case::array_single_element("[1]", "[1]")] +#[case::array_multiple_elements("[1,2,3]", "[1, 2, 3]")] +#[case::array_mixed_types("[1,\"test\",true]", "[1, \"test\", true]")] +#[case::array_nested("[[1,2],[3,4]]", "[[1, 2], [3, 4]]")] +#[case::array_with_spaces("[ 1 , 2 , 3 ]", "[1, 2, 3]")] +#[case::array_multiline( + r#"[ + 1, + 2, + 3 + ]"#, + "[\n 1,\n 2,\n 3\n]\n" +)] +#[case::let_with_array(r#"let arr = [1, 2, 3]"#, r#"let arr = [1, 2, 3]"#)] +#[case::let_with_array_multiline( + r#"let arr = [ +1, +2, +3 +]"#, + "let arr = [\n 1,\n 2,\n 3\n]\n" +)] +#[case::dict_empty("{}", "{}")] +#[case::def_with_let_and_array( + r#"def foo(): + let arr = [ + 1, + 2, + 3]; +"#, + r#"def foo(): + let arr = [ + 1, + 2, + 3 + ]; +"# +)] +#[case::dict_single_pair("{\"key\": \"value\"}", "{\"key\": \"value\"}")] +#[case::dict_multiple_pairs( + "{\"key1\": \"value1\", \"key2\": \"value2\"}", + "{\"key1\": \"value1\", \"key2\": \"value2\"}" +)] +#[case::dict_with_spaces("{ \"key\" : \"value\" }", "{\"key\": \"value\"}")] +#[case::dict_mixed_types( + "{\"str\": \"value\", \"num\": 42, \"bool\": true}", + "{\"str\": \"value\", \"num\": 42, \"bool\": true}" +)] +#[case::dict_nested("{\"outer\": {\"inner\": \"value\"}}", "{\"outer\": {\"inner\": \"value\"}}")] +#[case::dict_multiline( + r#"{ +"key1": "value1", +"key2": "value2" +}"#, + "{\n \"key1\": \"value1\",\n \"key2\": \"value2\"\n}\n" +)] +#[case::dict_multiline_mixed( + r#"{ +"str": "value", +"num": 42, +"bool": true +}"#, + "{\n \"str\": \"value\",\n \"num\": 42,\n \"bool\": true\n}\n" +)] +#[case::equal_operator("let x = 1 == 2", "let x = 1 == 2")] +#[case::not_equal_operator("let y = 3 != 4", "let y = 3 != 4")] +#[case::string_with_newline(r#""line1\nline2""#, r#""line1\nline2""#)] +#[case::plus_operator("let x = 1 + 2", "let x = 1 + 2")] +#[case::let_newline_after_equal( + r#"let x = +"test""#, + r#"let x = + "test" +"# +)] +#[case::let_with_if_multiline( + r#"let x = if(test): +test +else: +test2"#, + r#"let x = if (test): + test +else: + test2 +"# +)] +#[case::let_with_while_multiline( + r#"let x = while(condition()): +process();"#, + r#"let x = while (condition()): + process(); +"# +)] +#[case::less_than_operator("let x = 1 < 2", "let x = 1 < 2")] +#[case::less_than_equal_operator("let x = 1 <= 2", "let x = 1 <= 2")] +#[case::greater_than_operator("let x = 2 > 1", "let x = 2 > 1")] +#[case::greater_than_equal_operator("let x = 2 >= 1", "let x = 2 >= 1")] +#[case::range_operator("1..1", "1..1")] +#[case::range_operator_with_spaces("1 .. 1", "1..1")] +#[case::range_operator_with_variables("x..y", "x..y")] +#[case::range_operator_with_string(r#""1" .. "2""#, r#""1".."2""#)] +#[case::selector_attr(".code.lang", ".code.lang")] +#[case::standalone_attr_selector(".lang", ".lang")] +#[case::standalone_attr_selector_value(".value", ".value")] +#[case::env("let ENV = $env", "let ENV = $env")] +#[case::mul("1 * 1", "1 * 1")] +#[case::mul("1 / 1", "1 / 1")] +#[case::and("true && false", "true && false")] +#[case::or("true || false", "true || false")] +#[case::binary_op_multiline_or( + r#"1 +|| 2 +|| 3"#, + "1 + || 2 + || 3 +" +)] +#[case::binary_op_multiline( + r#"let v = 1 +|| 2 +|| 3"#, + "let v = 1 + || 2 + || 3 +" +)] +#[case::def_contains( + r#"def contains(haystack, needle): +if (is_dict(haystack)): + not(is_none(get(haystack, needle))) +else: + index(haystack, needle) != -1; +"#, + r#"def contains(haystack, needle): + if (is_dict(haystack)): + not(is_none(get(haystack, needle))) + else: + index(haystack, needle) != -1; +"# +)] +#[case::escape_sequence_clear_screen(r#""\x1b[2J\x1b[H""#, r#""\x1b[2J\x1b[H""#)] +#[case::control_character_bell(r#""\x07""#, r#""\x07""#)] +#[case::control_character_backspace(r#""\x08""#, r#""\x08""#)] +#[case::control_character_vertical_tab(r#""\x0b""#, r#""\x0b""#)] +#[case::control_character_form_feed(r#""\x0c""#, r#""\x0c""#)] +#[case::control_character_escape(r#""\x1b""#, r#""\x1b""#)] +#[case::control_character_delete(r#""\x7f""#, r#""\x7f""#)] +#[case::not_operator("!true", "!true")] +#[case::let_with_if_multiline_in_while( + r#"while(condition()): + let x = 1 + | let y = if(test): +test +else: +test2 +end +"#, + r#"while (condition()): + let x = 1 + | let y = if (test): + test + else: + test2 +end +"# +)] +#[case::let_with_while_multiline( + r#"let x = while(condition()): +process();"#, + r#"let x = while (condition()): + process(); +"# +)] +#[case::let_with_while_multiline2( + r#""test" +| let x = while(condition()): +process();"#, + r#""test" +| let x = while (condition()): + process(); +"# +)] +#[case::pipe_let_block_in_nested_do( + "def f(t):\n if (t): 1\n else: do\n let a = 1\n | let b = foreach (r, t):\n r\n end\n | let c = if (a):\n b\n else:\n a\n | c\n end\nend", + "def f(t):\n if (t): 1\n else: do\n let a = 1\n | let b = foreach (r, t):\n r\n end\n | let c = if (a):\n b\n else:\n a\n | c\n end\nend\n" +)] +#[case::fn_arg_after_pipe( + "def f(xs):\n let t = 1\n | fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y;\n )\nend", + "def f(xs):\n let t = 1\n | fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y;\n )\nend\n" +)] +#[case::fn_arg_after_pipe_in_do( + "let t = 1\n| do\n let u = 2\n | filter(xs, fn(x):\n x > u;\n )\n end", + "let t = 1\n| do\n let u = 2\n | filter(xs, fn(x):\n x > u;\n )\n end\n" +)] +#[case::array_index_access("let arr = [1, 2, 3]\n|arr[1]", "let arr = [1, 2, 3]\n| arr[1]\n")] +#[case::array_index_access_inline("arr[0]", "arr[0]")] +#[case::index_on_dict_literal_after_def("def f(x): x;\n{\"a\":1}[\"a\"]", "def f(x): x;\n{\"a\": 1}[\"a\"]\n")] +#[case::slice_on_string_literal_after_def("def f(x): x;\n\"abc\"[1:2]", "def f(x): x;\n\"abc\"[1:2]\n")] +#[case::index_on_array_literal_in_body("def f(x):\n 1\n |[1][0]\nend", "def f(x):\n 1\n | [1][0]\nend\n")] +#[case::dict_index_access( + "let d = {\"key\": \"value\"}\n|d[\"key\"]", + "let d = {\"key\": \"value\"}\n| d[\"key\"]\n" +)] +#[case::dict_index_access_inline("d[\"key\"]", "d[\"key\"]")] +#[case::comment_first_line("# comment\nlet x = 1", "# comment\nlet x = 1\n")] +#[case::string_keeps_unicode_escape(r#"let x = "\u{D7FF}\u{E000}""#, r#"let x = "\u{D7FF}\u{E000}""#)] +#[case::string_keeps_hex_escape(r#"let x = "\x41""#, r#"let x = "\x41""#)] +#[case::string_keeps_regex_escapes(r#"let x = "\d+\s\/""#, r#"let x = "\d+\s\/""#)] +#[case::string_escapes_raw_tab("let x = \"a\tb\"", r#"let x = "a\tb""#)] +#[case::string_escapes_raw_newline("let x = \"a\nb\"", "let x = \"a\\nb\"")] +#[case::dict_key_keeps_unicode_escape(r#"{"\u{E000}": 1}"#, r#"{"\u{E000}": 1}"#)] +#[case::interpolated_string_keeps_unicode_escape(r#"s"\u{E000}${x}""#, r#"s"\u{E000}${x}""#)] +#[case::string_after_multibyte_text(r#"let s = "ๆ—ฅๆœฌ่ชž" | let x = "\x41""#, r#"let s = "ๆ—ฅๆœฌ่ชž" | let x = "\x41""#)] +#[case::string_after_leading_tab("\tlet x = \"\\u{41}\"", r#"let x = "\u{41}""#)] +#[case::string_with_crlf("let x = 1\r\n| let y = \"\\u{41}\"\r\n", "let x = 1\n| let y = \"\\u{41}\"\n")] +#[case::string_keeps_quote_and_backslash(r#"let x = "a\"b\\c""#, r#"let x = "a\"b\\c""#)] +#[case::string_empty(r#"let x = """#, r#"let x = """#)] +#[case::string_multiline_keeps_escape_on_second_line("let x = \"a\n \\u{41}\"", r#"let x = "a\n \u{41}""#)] +#[case::array_keeps_escapes(r#"["\u{41}", "\x42"]"#, r#"["\u{41}", "\x42"]"#)] +#[case::match_pattern_keeps_escape(r#"match (x): | "\u{41}": 1 | _: 2 end"#, r#"match (x): | "\u{41}": 1 | _: 2 end"#)] +#[case::symbol_keeps_escape(r#":"\u{41}""#, r#":"\u{41}""#)] +#[case::interpolated_string_keeps_quote_and_backslash(r#"s"\"${a}\\""#, r#"s"\"${a}\\""#)] +#[case::interpolated_string_keeps_raw_newline("s\"a\n${x}\"", "s\"a\n${x}\"\n")] +#[case::array_arg_after_pipe( + "def f():\n let r = 1\n | g(r, [\n 1,\n 2,\n ])\nend", + "def f():\n let r = 1\n | g(r, [\n 1,\n 2,\n ])\nend\n" +)] +#[case::dict_arg_after_pipe( + "def f():\n let r = 1\n | g(r, {\n \"a\": 1,\n })\nend", + "def f():\n let r = 1\n | g(r, {\n \"a\": 1,\n })\nend\n" +)] +#[case::dict_after_pipe( + "def f():\n let r = 1\n | {\n a: r\n }\nend", + "def f():\n let r = 1\n | {\n a: r\n }\nend\n" +)] +#[case::array_arg_without_pipe("def f():\n g(r, [\n 1,\n ])\nend", "def f():\n g(r, [\n 1,\n ])\nend\n")] +#[case::comment_keeps_leading_spaces("# [1, 2],\nlet x = 1", "# [1, 2],\nlet x = 1\n")] +#[case::comment_keeps_leading_tab("#\tcomment\nlet x = 1", "#\tcomment\nlet x = 1\n")] +#[case::comment_adds_space_when_missing("#comment\nlet x = 1", "# comment\nlet x = 1\n")] +#[case::comment_trims_trailing_spaces("# comment \nlet x = 1", "# comment\nlet x = 1\n")] +#[case::comment_keeps_leading_spaces_before_close("[\n 1,\n # c\n]", "[\n 1,\n # c\n]\n")] +#[case::comment_inline("let x = 1 # inline comment", "let x = 1 # inline comment")] +#[case::comment_multiline( + "let x = 1\n# multiline comment\n| let y = 2", + "let x = 1\n# multiline comment\n| let y = 2\n" +)] +#[case::comment_after_expr_multiline( + "if(test):\n test # comment\nelse:\n test2 # comment2", + "if (test):\n test # comment\nelse:\n test2 # comment2\n" +)] +#[case::comment_after_expr_inline( + "if(test): test # comment else: test2 # comment2", + "if (test): test # comment else: test2 # comment2" +)] +#[case::fn_as_call_arg_single_line("map(fn(): program;)", "map(fn(): program;)")] +#[case::fn_as_call_arg_multi_line("map(\n fn(arg):\n process(arg);\n)", "map(\n fn(arg):\n process(arg);\n)\n")] +#[case::fn_as_call_arg_with_other_args("map(fn(): program;, 1, \"test\")", "map(fn(): program;, 1, \"test\")")] +#[case::nested_fn_as_call_arg("outer(map(fn(): inner();))", "outer(map(fn(): inner();))")] +#[case::fn_as_call_arg_with_multiline_args( + "map(\n fn(x):\n process(x);\n ,\n fn(y):\n process(y);\n)", + "map(\n fn(x):\n process(x);\n ,\n fn(y):\n process(y);\n)\n" +)] +#[case::fn_arg_without_terminator("map(xs, fn(x): x + 1)", "map(xs, fn(x): x + 1)")] +#[case::fn_arg_without_terminator_before_comma("map(fn(x): x + 1, 1, \"test\")", "map(fn(x): x + 1, 1, \"test\")")] +#[case::fn_arg_without_terminator_with_pipe( + "filter(xs, fn(x): x | select(.h), 1)", + "filter(xs, fn(x): x | select(.h), 1)" +)] +#[case::nested_fn_arg_without_terminator("outer(map(xs, fn(x): inner(x)))", "outer(map(xs, fn(x): inner(x)))")] +#[case::fn_arg_without_terminator_multi_line( + "map(\n fn(arg):\n process(arg)\n)", + "map(\n fn(arg):\n process(arg)\n)\n" +)] +#[case::fn_arg_without_terminator_after_pipe( + "let t = 1\n| fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y\n )", + "let t = 1\n| fold(xs, 0, fn(acc, x):\n let y = x + 1\n | acc + y\n )\n" +)] +#[case::fn_without_params("map(xs, fn: self + 1)", "map(xs, fn: self + 1)")] +#[case::fn_without_params_with_pipe( + "filter(xs, fn: select(.h || .code) | !is_none, 1)", + "filter(xs, fn: select(.h || .code) | !is_none, 1)" +)] +#[case::fn_without_params_nested("map(xs, fn: map(fn: to_text))", "map(xs, fn: map(fn: to_text))")] +#[case::fn_without_params_terminated("let f = fn: self + 1;", "let f = fn: self + 1;")] +#[case::fn_without_params_spacing("map(xs, fn : self)", "map(xs, fn: self)")] +#[case::fn_without_params_multi_line("map(\n fn:\n process(self)\n)", "map(\n fn:\n process(self)\n)\n")] +#[case::group_simple("(1)", "(1)")] +#[case::group_with_expr("(1 + 2)", "(1 + 2)")] +#[case::group_with_nested_group("((1 + 2) * 3)", "((1 + 2) * 3)")] +#[case::group_with_multiple_ops("(1 + 2 * 3)", "(1 + 2 * 3)")] +#[case::group_with_array("(array(1, 2))", "(array(1, 2))")] +#[case::group_with_dict("({\"key\": \"value\"})", "({\"key\": \"value\"})")] +#[case::group_with_comment("(1 + 2) # group comment", "(1 + 2) # group comment")] +#[case::group_with_call("(test(1, 2))", "(test(1, 2))")] +#[case::group_with_if("(if(test): test else: test2)", "(if (test): test else: test2)")] +#[case::group_with_let("(let x = 1)", "(let x = 1)")] +#[case::fn_end("fn(): test end", "fn(): test end")] +#[case::negate_operator("-v", "-v")] +#[case::try_catch_multiline( + r#"try: + process() +catch: + handle_error()"#, + "try: + process() +catch: + handle_error() +" +)] +#[case::try_catch_oneline("try: process() catch: handle_error()", "try: process() catch: handle_error()")] +#[case::try_catch_with_binder( + "try: process() catch(e): handle_error(e)", + "try: process() catch(e): handle_error(e)" +)] +#[case::try_catch_with_binder_extra_spaces( + "try: process() catch( e ): handle_error(e)", + "try: process() catch(e): handle_error(e)" +)] +#[case::try_catch_with_binder_multiline( + r#"try: + process() +catch(e): + handle_error(e)"#, + "try: + process() +catch(e): + handle_error(e) +" +)] +#[case::try_catch_with_finally( + r#"try: + process() +catch: + handle_error() +"#, + "try: + process() +catch: + handle_error() +" +)] +#[case::coalesce_operator("let x = a?? b", "let x = a ?? b")] +#[case::coalesce_operator_with_call("let result = get_value() ?? default()", "let result = get_value() ?? default()")] +#[case::coalesce_operator_with_literal("let x = value ?? 42", "let x = value ?? 42")] +#[case::coalesce_operator_with_string("let s = str ?? \"default\"", "let s = str ?? \"default\"")] +#[case::coalesce_operator_chain("let x = a ?? b ?? c", "let x = a ?? b ?? c")] +#[case::coalesce_operator_in_if( + "if(a ?? b): do_something() else: do_other()", + "if (a ?? b): do_something() else: do_other()" +)] +#[case::coalesce_operator_in_array("[a ?? b, c]", "[a ?? b, c]")] +#[case::coalesce_operator_in_dict("{\"key\": a ?? b}", "{\"key\": a ?? b}")] +#[case::coalesce_operator_with_comment("let x = a ?? b # fallback", "let x = a ?? b # fallback")] +#[case::call_dynamic("v[0](1,2,3)", "v[0](1, 2, 3)")] +#[case::dict_with_fn_value_multiline( + r#"{ +"key1": fn(): +process(); +,"key2": "value2" +}"#, + r#"{ + "key1": fn(): + process(); + ,"key2": "value2" +} +"# +)] +#[case::do_block_multiline( + r#"do + process1() + | process2() +end +"#, + "do + process1() + | process2() +end +" +)] +#[case::do_block_oneline("do process1() | process2();", "do process1() | process2();")] +#[case::let_with_do_block_multiline( + r#"let result = do + step1() + | step2(); +"#, + "let result = do + step1() + | step2(); +" +)] +#[case::let_with_do_block_oneline("let result = do step1() | step2();", "let result = do step1() | step2();")] +#[case::symbol_with_ident(":foo", ":foo")] +#[case::symbol_with_string(r#":"bar""#, r#":"bar""#)] +#[case::symbol_with_spaces(": foo", ":foo")] +#[case::symbol_in_array("[:foo, :bar]", "[:foo, :bar]")] +#[case::symbol_in_dict(r#"{:key: "value"}"#, r#"{:key: "value"}"#)] +#[case::symbol_comparison(":foo == :bar", ":foo == :bar")] +#[case::match_simple( + "match(x): | 1: \"one\" | _: \"other\" end", + "match (x): | 1: \"one\" | _: \"other\" end" +)] +#[case::match_multiline( + r#"match(x): +| 1: "one" +| 2: "two" +| _: "other" +end"#, + r#"match (x): + | 1: "one" + | 2: "two" + | _: "other" +end +"# +)] +#[case::match_with_guard( + "match(x): | n if(n > 0): \"positive\" | _: \"non-positive\" end", + "match (x): | n if (n > 0): \"positive\" | _: \"non-positive\" end" +)] +#[case::match_with_array_pattern( + "match(arr): | [a, b]: add(a, b) | _: 0 end", + "match (arr): | [a, b]: add(a, b) | _: 0 end" +)] +#[case::match_with_array_pattern_with_literal( + "match(arr): | [1, 2]: add(1, 2) | _: 0 end", + "match (arr): | [1, 2]: add(1, 2) | _: 0 end" +)] +#[case::match_with_array_pattern_with_symbol( + "match(arr): | [:string, :string]: add(1, 2) | _: 0 end", + "match (arr): | [:string, :string]: add(1, 2) | _: 0 end" +)] +#[case::match_with_dict_pattern( + "match(obj): | {name: n}: n | _: \"unknown\" end", + "match (obj): | {name: n}: n | _: \"unknown\" end" +)] +#[case::match_nested_in_let( + r#"let result = match(x): +| 1: "one" +| 2: "two" +| _: "other" +end"#, + r#"let result = match (x): + | 1: "one" + | 2: "two" + | _: "other" +end +"# +)] +#[case::match_with_type_pattern( + "match(val): | :string: \"is string\" | :number: \"is number\" | _: \"other\" end", + "match (val): | :string: \"is string\" | :number: \"is number\" | _: \"other\" end" +)] +#[case::match_multiline_in_pipe( + r#""test" +| match(x): + | 1: "one" + | 2: "two" + end"#, + r#""test" +| match (x): + | 1: "one" + | 2: "two" + end +"# +)] +#[case::dict_nested_multiline_level3( + r#"{ +"level1": { +"level2": { +"level3": "value" +} +} +}"#, + r#"{ + "level1": { + "level2": { + "level3": "value" + } + } +} +"# +)] +#[case::array_nested_multiline_level3( + r#"[ +[ +[ +"value" +] +] +]"#, + r#"[ + [ + [ + "value" + ] + ] +] +"# +)] +#[case::comment_with_newline("# comment\nlet x = 1", "# comment\nlet x = 1\n")] +#[case::comment_with_indent( + "if(test):\n test # comment\nelse:\n test2 # comment2", + "if (test):\n test # comment\nelse:\n test2 # comment2\n" +)] +#[case::comment_inline_with_indent("let x = 1 # inline comment", "let x = 1 # inline comment")] +#[case::comment_multiline_with_indent( + "let x = 1\n # multiline comment\n| let y = 2", + "let x = 1\n# multiline comment\n| let y = 2\n" +)] +#[case::comment_after_expr_multiline_with_indent( + "if(test):\n test # comment\nelse:\n test2 # comment2", + "if (test):\n test # comment\nelse:\n test2 # comment2\n" +)] +#[case::comment_after_expr_inline_with_indent( + "if(test): test # comment else: test2 # comment2", + "if (test): test # comment else: test2 # comment2" +)] +#[case::interpolated_string_with_escaped_brackets(r#"s"\\[${phrase}\\]\\(""#, r#"s"\\[${phrase}\\]\\(""#)] +#[case::interpolated_string_with_backslash(r#"s"\\test""#, r#"s"\\test""#)] +#[case::module_with_body( + r#"module test: +import "foo.mq" +| def main(): test(); +end"#, + r#"module test: + import "foo.mq" + | def main(): test(); +end +"# +)] +#[case::module_with_do( + r#"module test do +import "foo.mq" +| def main(): test(); +end"#, + r#"module test do + import "foo.mq" + | def main(): test(); +end +"# +)] +#[case::module_with_do_and_new_line( + r#"module test +do +import "foo.mq" +| def main(): test(); +end"#, + r#"module test + do + import "foo.mq" + | def main(): test(); +end +"# +)] +#[case::comment_preserves_indent_after_newline( + "let x = 1\n # indented comment after newline\n| let y = 2", + "let x = 1\n# indented comment after newline\n| let y = 2\n" +)] +#[case::comment_preserves_indent_after_newline_deep( + "if(test):\n test\n # deeper indented comment\nelse:\n test2", + "if (test):\n test\n# deeper indented comment\nelse:\n test2\n" +)] +#[case::comment_preserves_indent_after_newline_array( + "[1,\n # comment for 2\n 2]", + "[1,\n # comment for 2\n 2]\n" +)] +#[case::comment_preserves_indent_after_newline_dict( + "{\n \"a\": 1,\n # comment for b\n \"b\": 2\n}", + "{\n \"a\": 1,\n # comment for b\n \"b\": 2\n}\n" +)] +#[case::match_preserves_indent_after_newline( + "let v = \nmatch (x):\n| 1: \"one\"\n| 2: \"two\"\n end", + "let v =\n match (x):\n | 1: \"one\"\n | 2: \"two\"\n end\n" +)] +#[case::match_with_do_block( + "let result = match (x):\n| 1: do\n foo() |\n bar()\n end\n| 2: \"two\"\nend", + "let result = match (x):\n | 1: do\n foo() |\n bar()\n end\n | 2: \"two\"\nend\n" +)] +#[case::assign("var i=0 | i=i + 1", "var i = 0 | i = i + 1")] +#[case::assign_right_multiline( + r#"let x = +"test""#, + r#"let x = + "test" +"# +)] +#[case::assign_right_multiline_pipe( + r#"let x = +"test" +| upcase()"#, + r#"let x = + "test" +| upcase() +"# +)] +#[case::assign_right_multiline_if( + r#"let x = +if(test): +test +else: +test2"#, + r#"let x = + if (test): + test + else: + test2 +"# +)] +#[case::if_without_colon_oneline("if(test) 1 else 2", "if (test) 1 else 2")] +#[case::if_without_colon_multiline( + r#"if(test) +test +else +test2"#, + r#"if (test) + test +else + test2 +"# +)] +#[case::if_elif_else_without_colon_oneline("if(test) 1 elif(test2) 2 else 3", "if (test) 1 elif (test2) 2 else 3")] +#[case::if_elif_else_without_colon_multiline( + r#"if(test) +test +elif(test2) +test2 +else +test3"#, + r#"if (test) + test +elif (test2) + test2 +else + test3 +"# +)] +#[case::if_without_colon_with_do_block( + r#"if(test) do +test +end +else do +test2 +end"#, + r#"if (test) do + test + end +else do + test2 + end +"# +)] +#[case::while_do_end_oneline("while(x > 0) do x - 1 end", "while (x > 0) do x - 1 end")] +#[case::while_do_end_multiline( + r#"while(x > 0) do +let x = x - 1 | x +end"#, + r#"while (x > 0) do + let x = x - 1 | x +end +"# +)] +#[case::while_do_end_with_break( + r#"while(x < 10) do +let x = x + 1 +| if(x == 3): +break +else: +x +end"#, + r#"while (x < 10) do + let x = x + 1 + | if (x == 3): + break + else: + x +end +"# +)] +#[case::foreach_do_end_oneline( + "foreach(item, arr) do process(item) end", + "foreach (item, arr) do process(item) end" +)] +#[case::foreach_do_end_multiline( + r#"foreach(x, array(1, 2, 3)) do +add(x, 1) +end"#, + r#"foreach (x, array(1, 2, 3)) do + add(x, 1) +end +"# +)] +#[case::foreach_do_end_with_continue( + r#"foreach(x, array(1, 2, 3, 4, 5)) do +if(x == 3): +continue +else: +x + 10 +end"#, + r#"foreach (x, array(1, 2, 3, 4, 5)) do + if (x == 3): + continue + else: + x + 10 +end +"# +)] +#[case::foreach_do_end_nested( + r#"foreach(row, arr) do +foreach(x, row) do +x * 2 +end +end"#, + r#"foreach (row, arr) do + foreach (x, row) do + x * 2 + end +end +"# +)] +#[case::match_do_end_oneline( + "match(x) do | 1: \"one\" | 2: \"two\" | _: \"other\" end", + "match (x) do | 1: \"one\" | 2: \"two\" | _: \"other\" end" +)] +#[case::match_do_end_multiline( + r#"match(2) do +| 1: "one" +| 2: "two" +| _: "other" +end"#, + r#"match (2) do + | 1: "one" + | 2: "two" + | _: "other" +end +"# +)] +#[case::match_do_end_type_pattern( + r#"match(array(1, 2, 3)) do +| :array: "is_array" +| :number: "is_number" +| _: "other" +end"#, + r#"match (array(1, 2, 3)) do + | :array: "is_array" + | :number: "is_number" + | _: "other" +end +"# +)] +#[case::match_do_end_with_guard( + r#"match(x) do +| n if(n > 0): "positive" +| _: "non-positive" +end"#, + r#"match (x) do + | n if (n > 0): "positive" + | _: "non-positive" +end +"# +)] +#[case::def_with_default_single( + "def greet(name = \"World\"): + name;", + "def greet(name = \"World\"): + name; +" +)] +#[case::def_with_multiple_defaults( + "def foo(a, b = 2, c = 3): + a + b + c;", + "def foo(a, b = 2, c = 3): + a + b + c; +" +)] +#[case::def_with_mixed_params( + "def bar(x,y=10): + x+y;", + "def bar(x, y = 10): + x + y; +" +)] +#[case::def_with_array_default( + "def baz(x,y=[1,2,3]): + x+len(y);", + "def baz(x, y = [1, 2, 3]): + x + len(y); +" +)] +#[case::def_with_ident_default( + "def f(x = y): + x;", + "def f(x = y): + x; +" +)] +#[case::def_with_string_default( + "def test(a,b=\"test\"): + a+b;", + "def test(a, b = \"test\"): + a + b; +" +)] +#[case::def_with_number_default( + "def calc(x=42): + x * 2;", + "def calc(x = 42): + x * 2; +" +)] +#[case::def_with_boolean_default( + "def result(x=true): + x;", + "def result(x = true): + x; +" +)] +#[case::def_all_params_with_defaults( + "def calc(a=1,b=2,c=3): + a+b+c;", + "def calc(a = 1, b = 2, c = 3): + a + b + c; +" +)] +#[case::def_expr_with_default( + "def calc(a=1,b=2,c=3 + 1): + a+b+c;", + "def calc(a = 1, b = 2, c = 3 + 1): + a + b + c; +" +)] +#[case::def_expr_with_variadic( + "def calc(v,*args): + v + args;", + "def calc(v, *args): + v + args; +" +)] +#[case::assign_with_selector_attr("value.test|= value.attr", "value.test |= value.attr")] +#[case::index_assign("arr[0]=10", "arr[0] = 10")] +#[case::index_assign_string_key("dict[\"key\"]=\"value\"", "dict[\"key\"] = \"value\"")] +#[case::index_compound_assign("arr[0]+=1", "arr[0] += 1")] +#[case::selector_call_heading_single_arg(".h(1)", ".h(1)")] +#[case::selector_call_heading_multi_arg(".h(1,2)", ".h(1, 2)")] +#[case::selector_call_code_lang(".code(\"rust\")", ".code(\"rust\")")] +#[case::selector_call_pipe(".h(1)|.text()", ".h(1) | .text()")] +#[case::descendant_chain(".blockquote .code", ".blockquote .code")] +#[case::descendant_chain_no_space(".blockquote.code", ".blockquote .code")] +#[case::descendant_chain_extra_space(".blockquote .code", ".blockquote .code")] +#[case::descendant_chain_three_levels(".blockquote .list .code", ".blockquote .list .code")] +#[case::descendant_chain_with_selector_call(".blockquote .code(\"rust\")", ".blockquote .code(\"rust\")")] +#[case::descendant_chain_trailing_attr(".blockquote .code.lang", ".blockquote .code.lang")] +#[case::arrow_simple("->(): program;", "->(): program;")] +#[case::arrow_multiline( + "->(arg1,arg2): + program;", + "->(arg1, arg2): + program; +" +)] +#[case::arrow_as_call_arg("map( ->():program;)", "map(->(): program;)")] +#[case::arrow_as_call_arg_single_line("map(->(): program;)", "map(->(): program;)")] +#[case::arrow_as_call_arg_with_other_args("map(->(): program;, 1, \"test\")", "map(->(): program;, 1, \"test\")")] +#[case::nested_arrow_as_call_arg("outer(map(->(): inner();))", "outer(map(->(): inner();))")] +#[case::arrow_end("->(): test end", "->(): test end")] +#[case::bytes_literal_basic(r#"b"abc""#, r#"b"abc""#)] +#[case::bytes_literal_hex(r#"b"\xf0\x9f\x99\x82""#, r#"b"\xf0\x9f\x99\x82""#)] +#[case::bytes_literal_with_pipe(r#"b"abc" | len"#, r#"b"abc" | len"#)] +#[case::bytes_literal_in_call(r#"len(b"abc")"#, r#"len(b"abc")"#)] +#[case::as_binding_basic("42 as x | x", "42 as x | x")] +#[case::as_binding_spaces("42 as x | x", "42 as x | x")] +#[case::as_binding_selector(".text as title | title", ".text as title | title")] +#[case::def_with_do_block_body( + r#"def test(x): + do + step1(x) + | step2(x) + end;"#, + r#"def test(x): + do + step1(x) + | step2(x) + end; +"# +)] +#[case::foreach_colon_with_do_block_body( + r#"foreach(x, items): + do + process(x) + end;"#, + r#"foreach (x, items): + do + process(x) + end; +"# +)] +#[case::foreach_do_with_nested_do_block( + r#"foreach(row, arr) do + do + process(row) + end +end"#, + r#"foreach (row, arr) do + do + process(row) + end +end +"# +)] +#[case::fn_multiline_params_as_first_array_element( + "let fns = [fn( + a, + b +): a + b;, fn(c): c;]", + "let fns = [fn( + a, + b +): a + b;, fn(c): c;] +" +)] +#[case::fn_multiline_variadic_params_as_first_array_element( + "let fns = [fn( + a, + *rest +): rest;, fn(c): c;]", + "let fns = [fn( + a, + *rest +): rest;, fn(c): c;] +" +)] +#[case::fn_multiline_params_as_non_first_array_element( + "let fns = [first, fn( + a, + b +): a + b;]", + "let fns = [first, fn( + a, + b +): a + b;] +" +)] +#[case::fn_multiline_params_in_nested_array( + "let xs = [[fn( + a, + b +): a + b;]]", + "let xs = [[fn( + a, + b +): a + b;]] +" +)] +#[case::fn_multiline_params_as_call_arg( + "map(arr, fn( + a, + b +): a + b;)", + "map(arr, fn( + a, + b +): a + b;) +" +)] +#[case::qualified_call_multiline_args_nested_in_call( + "let result = to_string(md::doc( + md::h(\"Items\", 2), + map([\"a\", \"b\"], fn(x): md::list(x);), + ))", + "let result = to_string(md::doc( + md::h(\"Items\", 2), + map([\"a\", \"b\"], fn(x): md::list(x);), +)) +" +)] +#[case::yield_oneline("def g(): yield: 1;", "def g(): yield: 1;")] +#[case::yield_bare("def g(): yield;", "def g(): yield;")] +#[case::break_with_value("while (true): break: 1;", "while (true): break: 1;")] +#[case::yield_multiline( + "def g(): + yield: 1 + | yield: 2;", + "def g(): + yield: 1 + | yield: 2; +" +)] +#[case::def_with_comment_before_yield( + "def g(): + # a comment + yield: 1;", + "def g(): + # a comment + yield: 1; +" +)] +fn test_format(#[case] code: &str, #[case] expected: &str) { + let result = Formatter::new(None).format(code); + assert_eq!(result.unwrap(), expected); +} + +/// Builds a `def` whose block expression sits `depth` `do`s deep after `prefix` +/// (`"let b = "`, `"| let b = "` or `"| "`), indented as expected: the block body goes +/// one level below where the expression's line content starts (after any `| `). +fn block_program(block: &[(usize, &str)], prefix: &str, depth: usize, indent_width: usize) -> String { + let pad = |level: usize| " ".repeat(level * indent_width); + let line_indent = depth + 1; + let piped = prefix.starts_with('|'); + let base = line_indent + usize::from(piped); + let mut lines = vec!["def f(t):".to_string()]; + for d in 0..depth { + lines.push(format!("{}do", pad(d + 1))); + } + if piped { + lines.push(format!("{}let a = 1", pad(line_indent))); + } + lines.push(format!("{}{prefix}{}", pad(line_indent), block[0].1)); + for (level, text) in &block[1..] { + lines.push(format!("{}{text}", pad(base + level))); + } + if prefix.contains("let") { + lines.push(format!("{}| b", pad(line_indent))); + } + for d in (0..depth).rev() { + lines.push(format!("{}end", pad(d + 1))); + } + lines.push("end".to_string()); + lines.join("\n") + "\n" +} + +#[rstest] +#[case::foreach(&[(0, "foreach (r, t):"), (1, "r"), (0, "end")])] +#[case::while_(&[(0, "while (t):"), (1, "r"), (0, "end")])] +#[case::until(&[(0, "until (t):"), (1, "r"), (0, "end")])] +#[case::loop_(&[(0, "loop:"), (1, "break"), (0, "end")])] +#[case::do_(&[(0, "do"), (1, "r"), (0, "end")])] +#[case::fn_(&[(0, "fn(x):"), (1, "x"), (0, "end")])] +#[case::if_else(&[(0, "if (t):"), (1, "1"), (0, "else:"), (1, "2")])] +#[case::if_elif(&[(0, "if (t):"), (1, "1"), (0, "elif (t):"), (1, "2"), (0, "else:"), (1, "3")])] +#[case::match_(&[(0, "match (t):"), (1, "| 1: \"one\""), (1, "| _: \"other\""), (0, "end")])] +#[case::call_with_fn(&[(0, "map(t, fn(x):"), (1, "let y = x"), (1, "| y;"), (0, ")")])] +#[case::array(&[(0, "["), (1, "1,"), (1, "2"), (0, "]")])] +#[case::dict(&[(0, "{"), (1, "\"a\": 1,"), (1, "\"b\": 2"), (0, "}")])] +#[case::call_with_array(&[(0, "g(t, ["), (1, "1,"), (1, "2"), (0, "])")])] +#[case::call_with_dict(&[(0, "g(t, {"), (1, "\"a\": 1"), (0, "})")])] +#[case::array_of_dicts(&[(0, "g(t, ["), (1, "{\"a\": 1},"), (1, "{\"b\": 2},"), (0, "])")])] +#[case::nested_array_in_dict(&[(0, "{"), (1, "\"a\": ["), (2, "1"), (1, "]"), (0, "}")])] +#[case::array_comment_before_close(&[(0, "g(t, ["), (1, "1,"), (1, "# c"), (0, "])")])] +fn test_format_block_indent( + #[case] block: &[(usize, &str)], + #[values("let b = ", "| let b = ", "| ")] prefix: &str, + #[values(0, 1, 2)] depth: usize, + #[values(2, 4)] indent_width: usize, +) { + let expected = block_program(block, prefix, depth, indent_width); + let flattened = expected.lines().map(str::trim_start).collect::>().join("\n"); + let format = |code: &str| { + Formatter::new(Some(FormatterConfig { + indent_width, + ..FormatterConfig::default() + })) + .format(code) + .unwrap() + }; + + assert_eq!(format(&expected), expected, "not idempotent"); + assert_eq!(format(&flattened), expected, "from flattened input"); +} + +#[rstest] +#[case::sort_imports( + r#"import "c.mq" +| import "a.mq" +| import "b.mq""#, + r#"import "a.mq" +| import "b.mq" +| import "c.mq" +"# +)] +#[case::sort_imports_with_alias( + r#"import "c.mq" +| import "a.mq" as a +| import "b.mq""#, + r#"import "a.mq" as a +| import "b.mq" +| import "c.mq" +"# +)] +#[case::sort_functions( + r#"def z(): test; +def a(): test; +def m(): test; +"#, + "def a(): test;\ndef m(): test;\ndef z(): test;\n" +)] +#[case::sort_fields( + r#"let z = 1 +| let a = 2 +| let m = 3"#, + r#"let a = 2 +| let m = 3 +| let z = 1 +"# +)] +#[case::sort_mixed( + r#"let z = 1 +| import "b.mq" +def y(): test; +| let a = 2 +| import "a.mq" +def b(): test;"#, + "import \"a.mq\"\n| import \"b.mq\"\n| let a = 2\n| let z = 1\n|\ndef b(): test;\ndef y(): test;\n" +)] +#[case::sort_with_other( + r#"def z(): test; +| let x = 1 +| nodes +def a(): test;"#, + "let x = 1\n|\ndef a(): test;\ndef z(): test;\n| nodes\n" +)] +#[case::sort_with_piped_calls( + r#"def z(): test; +| let x = 1 +| add() | mul() | sub() +def a(): test; +| let y = 2"#, + "let x = 1\n| let y = 2\n|\ndef a(): test;\ndef z(): test;\n| add()\n| mul()\n| sub()\n" +)] +#[case::sort_all_types( + r#"add() | sub() +def func_b(): test; +| let var_z = 1 +| import "z.mq" +| let var_a = 2 +def func_a(): test; +| mul() | div() +| import "a.mq""#, + "import \"a.mq\"\n| import \"z.mq\"\n| let var_a = 2\n| let var_z = 1\n|\ndef func_a(): test;\ndef func_b(): test;\n| add()\n| sub()\n| mul()\n| div()\n" +)] +fn test_format_with_sort(#[case] code: &str, #[case] expected: &str) { + let config = FormatterConfig { + indent_width: 2, + sort_imports: true, + sort_functions: true, + sort_fields: true, + max_width: None, + }; + let result = Formatter::new(Some(config)).format(code); + assert_eq!(result.unwrap(), expected); +} + +#[rstest] +#[case::wraps_top_level_pipeline( + "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\")", + 40, + "select(\"h1\") | upcase() | add_class(\"title\")\n| trim() | to_text() | replace(\"a\", \"b\")\n| join(\",\")\n" +)] +#[case::wraps_pipeline_inside_def( + "def foo(x):\n select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\");", + 40, + "def foo(x):\n select(\"h1\") | upcase() | add_class(\"title\")\n | trim() | to_text() | replace(\"a\", \"b\")\n | join(\",\");\n" +)] +#[case::keeps_short_pipeline_inline("select(\"h1\") | upcase()", 80, "select(\"h1\") | upcase()")] +fn test_format_with_max_width(#[case] code: &str, #[case] max_width: usize, #[case] expected: &str) { + let config = FormatterConfig { + max_width: Some(max_width), + ..Default::default() + }; + let result = Formatter::new(Some(config)).format(code); + assert_eq!(result.unwrap(), expected); +} + +#[test] +fn test_format_with_max_width_is_idempotent() { + let code = + "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\",\"b\") | join(\",\")"; + let config = || FormatterConfig { + max_width: Some(40), + ..Default::default() + }; + let once = Formatter::new(Some(config())).format(code).unwrap(); + let twice = Formatter::new(Some(config())).format(&once).unwrap(); + assert_eq!(once, twice); +} + +#[test] +fn test_format_without_max_width_does_not_wrap() { + let code = + "select(\"h1\") | upcase() | add_class(\"title\") | trim() | to_text() | replace(\"a\", \"b\") | join(\",\")"; + let result = Formatter::default().format(code); + assert_eq!(result.unwrap(), code); +} + +#[test] +fn test_format_yield_is_idempotent() { + for code in ["def g(): yield: 1;", "def g(): yield: 1 | yield: 2;"] { + let once = Formatter::new(None).format(code).unwrap(); + let twice = Formatter::new(None).format(&once).unwrap(); + assert_eq!(once, twice, "not idempotent for {code:?}"); + } +} + +const FORMAT_STATEMENTS: &[&str] = &[ + "def f(x):\n x + 1;", + "let a = [1,2]", + "if(a):1 elif(b):2 else:3", + "foreach(v,a):v;", + "match(x): | 1: :a | _: :b end", + "{\"k\": 1}", + ".h1", + "try: 1 catch(e): e", + "fn(x): x;", + "while(a): break;", + "a[0](1)", + "1 as n", +]; + +/// The previous allocating `is_let_line`, kept as an oracle. +fn reference_is_let_line(output: &str) -> bool { + let start = output.rfind('\n').map_or(0, |pos| pos + 1); + if start < output.len() { + let last_line = &output[start..]; + (!last_line.starts_with("let ") && last_line.trim().starts_with("let ")) + || last_line.trim().replace(" ", "").starts_with("|let") + } else { + false + } +} + +fn is_let_line(output: &str) -> bool { + let mut formatter = Formatter::new(None); + formatter.output = output.to_string(); + formatter.is_let_line() +} + +#[rstest] +#[case::top_level_let("let x = 1", false)] +#[case::indented_let(" let x = 1", true)] +#[case::piped_let("| let x", true)] +#[case::piped_let_no_space("|let x", true)] +#[case::piped_let_spaced("| let x", true)] +#[case::last_line_only("let a = 1\n | let b", true)] +#[case::previous_line_ignored(" let a\nfoo", false)] +#[case::trailing_newline(" let a\n", false)] +#[case::empty("", false)] +#[case::not_let(" letter", false)] +#[case::pipe_other("| foo", false)] +fn test_is_let_line(#[case] output: &str, #[case] expected: bool) { + assert_eq!(is_let_line(output), expected); + assert_eq!(reference_is_let_line(output), expected); +} + +#[test] +fn test_format_with_cst_normalizes_escapes_without_source() { + let (mut nodes, _) = mq_lang::parse_recovery(r#""\u{41}\t""#); + let result = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); + assert_eq!(result, r#""A\t""#); +} + +/// Comments before a terminating `;` are kept, and formatting them again changes nothing. +/// Comments between an expression and its postfix `[`/`(` on the next line are kept. +#[rstest::rstest] +#[case::index("x # note\n[0]", "x # note\n[0]\n")] +#[case::call("x # note\n(1)", "x # note\n(1)\n")] +#[case::index_after_call("f(x) # note\n[0]", "f(x) # note\n[0]\n")] +#[case::qualified_call("mod::f # note\n(1)", "mod::f # note\n(1)\n")] +#[case::in_def("def g():\n x # note\n [0]\n;", "def g():\n x # note\n [0];\n")] +fn test_format_keeps_comment_before_postfix(#[case] code: &str, #[case] expected: &str) { + let once = Formatter::new(None).format(code).unwrap(); + assert_eq!(once, expected); + assert_eq!(Formatter::new(None).format(&once).unwrap(), once); +} + +#[rstest::rstest] +#[case::own_line_in_def("def f():\n 1\n # note\n;", "def f():\n 1\n # note\n;\n")] +#[case::trailing_in_def("def f():\n 1 # trail\n;", "def f():\n 1 # trail\n;\n")] +#[case::after_let("let x = 1\n# c\n;", "let x = 1\n# c\n;\n")] +#[case::in_text("# a ; b\nlet x = 1", "# a ; b\nlet x = 1\n")] +fn test_format_keeps_comment_before_semicolon(#[case] code: &str, #[case] expected: &str) { + let once = Formatter::new(None).format(code).unwrap(); + assert_eq!(once, expected); + assert_eq!(Formatter::new(None).format(&once).unwrap(), once); +} + +/// Unparsable source in an `Error` node is emitted as written, strings included. +#[rstest::rstest] +#[case::junk_string("foo(1 \"a b\", 2)", "foo(1 \"a b\", 2)")] +#[case::unknown_token("foo(1, ยง, 2)", "foo(1, ยง, 2)")] +#[case::missing_paren("foo(1, 2", "foo(1, 2")] +#[case::array_final_newline("[1\n", "[1\n")] +#[case::array_items_final_newline("[1, 2\n", "[1, 2\n")] +#[case::dict_final_newline("{\"a\": 1\n", "{\"a\": 1\n")] +#[case::call_final_newline("foo(1\n", "foo(1\n")] +fn test_format_with_cst_keeps_broken_source(#[case] code: &str, #[case] expected: &str) { + let (mut nodes, errors) = mq_lang::parse_recovery(code); + assert!(errors.has_errors()); + + let result = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); + + assert_eq!(result, expected); +} + +/// A comment before an absent closer at EOF is kept exactly once. +#[rstest::rstest] +#[case::call("foo(1 # note\n")] +#[case::array("[1 # note\n")] +#[case::dict("{\"a\": 1 # note\n")] +#[case::call_own_line("foo(1,\n # note\n")] +#[case::array_own_line("[1,\n # note\n")] +#[case::dict_own_line("{\"a\": 1,\n # note\n")] +fn test_format_with_cst_keeps_comment_before_missing_closer(#[case] code: &str) { + let (mut nodes, errors) = mq_lang::parse_recovery(code); + assert!(errors.has_errors()); + + let result = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); + + assert_eq!(result.matches("# note").count(), 1, "{code:?} -> {result:?}"); +} + +#[test] +fn test_format_sort_fields_keeps_escapes() { + let config = FormatterConfig { + sort_fields: true, + ..FormatterConfig::default() + }; + let result = Formatter::new(Some(config)) + .format("let b = \"\\u{41}\"\n| let a = {\"\\u{E000}\": \"\\x42\"}") + .unwrap(); + assert_eq!(result, "let a = {\"\\u{E000}\": \"\\x42\"}\n| let b = \"\\u{41}\"\n"); +} + +/// Escape sequences and raw characters a string literal can contain. +const STRING_PIECES: &[&str] = &[ + "a", + "Z", + "0", + " ", + "ๆ—ฅ", + "รฉ", + "๐Ÿ˜€", + "\\n", + "\\t", + "\\r", + "\\\"", + "\\\\", + "\\/", + "\\d", + "\\s", + "\\(", + "\\{", + "\\x41", + "\\x1b", + "\\u{41}", + "\\u{D7FF}", + "\\u{E000}", + "\\u{10FFFF}", +]; + +proptest::proptest! { + /// A string literal is written back exactly as in the source. + #[test] + fn prop_string_literal_is_preserved( + pieces in proptest::collection::vec(proptest::sample::select(STRING_PIECES), 0..12), + prefix in proptest::sample::select(&["let x = ", "| ", "f(", "[", "{\"k\": ", "s"][..]), + ) { + let body = pieces.concat(); + let suffix = match prefix { + "f(" => ")", + "[" => "]", + "{\"k\": " => "}", + _ => "", + }; + let code = format!("{prefix}\"{body}\"{suffix}"); + let code = code.strip_prefix("| ").unwrap_or(&code).to_string(); + // Interpolated strings accept fewer escapes than plain strings. + proptest::prop_assume!(!mq_lang::parse_recovery(&code).1.has_errors()); + let once = Formatter::new(None).format(&code).unwrap(); + proptest::prop_assert!(once.contains(&format!("\"{body}\"")), "{code:?} -> {once:?}"); + let twice = Formatter::new(None).format(&once).unwrap(); + proptest::prop_assert_eq!(twice, once); + } + + /// Whitespace after `#` is kept; only a missing space is added. + #[test] + fn prop_comment_keeps_leading_whitespace(text in "[ \t]{0,3}[a-z\\[\\],\" ]{0,12}") { + let formatted = Formatter::new(None).format(&format!("#{text}\nlet x = 1")).unwrap(); + let text = text.trim_end(); + let expected = if text.is_empty() || text.starts_with([' ', '\t']) { + format!("#{text}") + } else { + format!("# {text}") + }; + proptest::prop_assert_eq!(formatted.lines().next().unwrap(), expected.trim_end()); + } + + #[test] + fn prop_is_let_line_matches_reference(output in "[ |let\\na-z\\t]{0,24}") { + proptest::prop_assert_eq!(is_let_line(&output), reference_is_let_line(&output)); + } + + /// Formatting through a pre-parsed CST matches formatting from source. + #[test] + fn prop_format_with_cst_matches_format( + stmts in proptest::collection::vec(proptest::sample::select(FORMAT_STATEMENTS), 1..6) + ) { + let code = stmts.join(" | "); + let expected = Formatter::new(None).format(&code).unwrap(); + let (mut nodes, _) = mq_lang::parse_recovery(&code); + let actual = Formatter::new(None).format_with_cst(&mut nodes).unwrap(); + proptest::prop_assert_eq!(actual, expected); + } + + #[test] + fn prop_format_is_idempotent( + stmts in proptest::collection::vec(proptest::sample::select(FORMAT_STATEMENTS), 1..6) + ) { + let once = Formatter::new(None).format(&stmts.join(" | ")).unwrap(); + let twice = Formatter::new(None).format(&once).unwrap(); + proptest::prop_assert_eq!(twice, once); + } +} diff --git a/crates/mq-help/README.md b/crates/mq-help/README.md index a1d4d66be..19cc2c599 100644 --- a/crates/mq-help/README.md +++ b/crates/mq-help/README.md @@ -1,6 +1,6 @@

mq-help

-Documentation catalog for the mq language: builds the single, unified catalog of every native builtin, selector, `builtin.mq` function, and standard-module function โ€” the shared source for the `mq help` CLI command and `mq-web-api`'s documentation endpoints. +Documentation catalog for the mq language: builds the single, unified catalog of every native builtin, selector, `builtin.mq` function, and standard-module function, the shared source for the `mq help` CLI command and `mq-web-api`'s documentation endpoints. ## Features @@ -29,7 +29,7 @@ let suggestion = suggest("mpa"); // Some("map") for entry in &matches { println!("{}", render_human(entry)); - // Or `render_markdown`/`render_module_markdown` โ€” Markdown output that mq can query + // Or `render_markdown`/`render_module_markdown`: Markdown output that mq can query // right back, e.g. `mq help section --markdown | mq 'select(.code.lang == "mq")'`. } ``` diff --git a/crates/mq-help/src/catalog.rs b/crates/mq-help/src/catalog.rs index 74a8f884a..0486e5692 100644 --- a/crates/mq-help/src/catalog.rs +++ b/crates/mq-help/src/catalog.rs @@ -27,7 +27,7 @@ pub struct HelpExample { } /// Full documentation for one function or selector, unifying native builtins, selectors, -/// `builtin.mq` functions, and standard-module functions behind a single shape โ€” the same +/// `builtin.mq` functions, and standard-module functions behind a single shape, the same /// shape can be served by `mq help --json` and by `mq-web-api`'s `/functions/{name}` and /// `/selectors/{name}` endpoints, so CLI and Web API can't drift apart. #[derive(Debug, Clone, Serialize)] @@ -53,7 +53,7 @@ pub struct HelpEntry { pub aliases: Vec, } -/// Looks up every entry matching `name` โ€” usually one, but a name may be defined in more +/// Looks up every entry matching `name`: usually one, but a name may be defined in more /// than one standard module, in which case every match is returned. `name` may be qualified /// as `module::function` to disambiguate a function whose name collides with its own module /// (e.g. `section::section`, distinct from the `section` module itself). @@ -111,7 +111,7 @@ fn native_entry(kind: &'static str, doc: &BuiltinDoc) -> HelpEntry { } /// Native builtin functions, selectors, `builtin.mq` functions, and language keywords with -/// their own help topic (currently just `nodes`) โ€” everything in [`all_entries`] except +/// their own help topic (currently just `nodes`); everything in [`all_entries`] except /// standard-module functions. Cheap: unlike `all_entries`/`all_modules`, it never parses a /// standard module's source. pub fn top_level_entries() -> Vec { @@ -383,7 +383,7 @@ pub fn render_module_human(module: &HelpModule) -> String { } /// Renders a single entry as Markdown, headed at `level` (e.g. `2` for `##`) so it can be -/// nested โ€” [`render_module_markdown`] renders each of its functions one level deeper. +/// nested: [`render_module_markdown`] renders each of its functions one level deeper. fn entry_markdown(entry: &HelpEntry, level: usize) -> String { let h = "#".repeat(level); let sub_h = "#".repeat(level + 1); @@ -440,7 +440,7 @@ fn entry_markdown(entry: &HelpEntry, level: usize) -> String { out } -/// Renders a single entry as Markdown โ€” e.g. queryable with mq itself via +/// Renders a single entry as Markdown, e.g. queryable with mq itself via /// `mq help --markdown | mq 'select(.code.lang == "mq")'`. pub fn render_markdown(entry: &HelpEntry) -> String { entry_markdown(entry, 2) @@ -654,7 +654,7 @@ mod tests { parts } - /// Every example shown by `mq help` is a real, runnable snippet โ€” this evaluates each + /// Every example shown by `mq help` is a real, runnable snippet; this evaluates each /// one through the actual engine and checks it against its recorded `expected` output, /// so a stale/wrong example fails CI instead of silently misleading a reader. #[test] diff --git a/crates/mq-help/src/lib.rs b/crates/mq-help/src/lib.rs index 714c7ee13..c6caf4871 100644 --- a/crates/mq-help/src/lib.rs +++ b/crates/mq-help/src/lib.rs @@ -6,7 +6,7 @@ //! the `catalog` feature (on by default); without it only the doc tables are built. The //! catalog combines two things: //! -//! - [`reference`]: extracts `MqFnDoc`/`MqExample` from the CST of any mq source, by parsing +//! - [`mod@reference`]: extracts `MqFnDoc`/`MqExample` from the CST of any mq source, by parsing //! the Markdown-ish doc-comment convention above each `def` (used for `builtin.mq` //! and every standard module). //! - [`catalog`]: unifies that CST-extracted documentation with the native builtin diff --git a/crates/mq-help/src/reference.rs b/crates/mq-help/src/reference.rs index 5529df8fd..853b9e64d 100644 --- a/crates/mq-help/src/reference.rs +++ b/crates/mq-help/src/reference.rs @@ -74,7 +74,7 @@ pub fn extract_module_doc(source: &str) -> Option { } /// Extracts a module's header doc and function docs in a single CST parse. `extract_module_doc` -/// and `extract_functions_from_cst` each call this and discard half the result โ€” fine for one- +/// and `extract_functions_from_cst` each call this and discard half the result, fine for one- /// off lookups, but callers needing both (e.g. `mq help`'s catalog build, over every standard /// module) should call this directly to parse each module's source only once. pub fn extract_module(source: &str, skip_native: bool) -> (Option, Vec) { @@ -130,7 +130,7 @@ fn collect_module_functions(module_node: &Shared, prefix: &str, out: &m } // If node 0 is a documented def, its first paragraph is only a module header when a second -// paragraph is left over for its own doc โ€” otherwise it's just that def's doc comment. +// paragraph is left over for its own doc; otherwise it's just that def's doc comment. fn module_header_paragraph(source: &str, nodes: &[Shared]) -> Option> { let mut paragraphs = leading_comment_paragraphs(source); if paragraphs.is_empty() { @@ -218,9 +218,9 @@ fn def_name(node: &Shared) -> Option { /// One line of a doc comment outside any fenced code block, classified by its role in the /// Markdown-ish convention documented on [`extract_functions_from_cst`]. Adding a new /// recognized line kind (e.g. a future `Capability:` line) means adding one variant here and -/// one match arm in [`parse_doc_comment`] โ€” the fence-toggling logic never has to change. +/// one match arm in [`parse_doc_comment`]: the fence-toggling logic never has to change. enum DocLine<'a> { - /// A blank line, or the literal `Example:` label โ€” pure formatting, carries no data. + /// A blank line, or the literal `Example:` label: pure formatting, carries no data. Skip, /// `Returns: TYPE` Returns(&'a str), @@ -278,7 +278,7 @@ fn parse_doc_comment(lines: impl IntoIterator) -> (String, Option /// Parses the (already de-indented) lines inside one ` ``` ` fence into a single example. /// /// Everything before the first `#=>` line is the code; the arrow's own text plus every line -/// after it (up to the fence close) is the expected value, joined with `\n` โ€” so output that's +/// after it (up to the fence close) is the expected value, joined with `\n`, so output that's /// naturally multi-line (a CSV/table/frontmatter render, ...) doesn't need to be contorted onto /// one line. A fence with no `#=>` line, or no code before it, yields no example. A function /// with multiple examples uses multiple fences (see `test_extract_functions_multiple_examples`), @@ -392,7 +392,7 @@ mod tests { #[test] fn test_module_doc_absent_when_only_first_fn_doc() { // A single paragraph directly above the first (public) def is that def's own doc, - // not a module header โ€” even though it happens to precede a blank-separated Example. + // not a module header, even though it happens to precede a blank-separated Example. let src = "# Checks if input is an array.\n#\n# Example:\n# ```\n# is_array([1, 2])\n# #=> true\n# ```\ndef is_array(a): a;"; assert_eq!(extract_module_doc(src), None); let fns = extract_functions_from_cst(src, false); diff --git a/crates/mq-hir/src/hir/query.rs b/crates/mq-hir/src/hir/query.rs index a531e5e21..f7f778c09 100644 --- a/crates/mq-hir/src/hir/query.rs +++ b/crates/mq-hir/src/hir/query.rs @@ -1,4 +1,4 @@ -//! Query methods for the HIR โ€” read-only accessors over symbols, scopes, and sources. +//! Query methods for the HIR: read-only accessors over symbols, scopes, and sources. use rustc_hash::FxHashMap; use smol_str::SmolStr; diff --git a/crates/mq-hir/src/lib.rs b/crates/mq-hir/src/lib.rs index 0cf13408d..8b4f800e0 100644 --- a/crates/mq-hir/src/lib.rs +++ b/crates/mq-hir/src/lib.rs @@ -41,4 +41,4 @@ pub use error::{HirError, HirWarning}; pub use hir::Hir; pub use scope::{Scope, ScopeId, ScopeKind}; pub use source::{Source, SourceId, SourceInfo}; -pub use symbol::{Doc, ParamInfo, Symbol, SymbolId, SymbolKind}; +pub use symbol::{Doc, ParamInfo, Symbol, SymbolId, SymbolKind, deprecation_message, is_deprecation_marker}; diff --git a/crates/mq-hir/src/symbol.rs b/crates/mq-hir/src/symbol.rs index 0c26586c8..86f49ae66 100644 --- a/crates/mq-hir/src/symbol.rs +++ b/crates/mq-hir/src/symbol.rs @@ -45,6 +45,19 @@ type Params = Vec; /// Doc comment text with its source range. pub type Doc = (mq_lang::Range, String); +/// Whether a doc line marks deprecation: `deprecated` in any case, alone or followed by `:` or a space. +/// Lines that only mention the word do not count. +pub fn is_deprecation_marker(text: &str) -> bool { + let lower = text.trim().to_lowercase(); + lower == "deprecated" || lower.starts_with("deprecated:") || lower.starts_with("deprecated ") +} + +/// The message after the colon of a deprecation marker line, if any. +pub fn deprecation_message(text: &str) -> Option { + let message = text.trim().split_once(':')?.1.trim(); + (!message.is_empty()).then(|| message.to_string()) +} + /// A named or structural element of the program, such as a function, variable or call. #[derive(Debug, Clone, PartialEq)] pub struct Symbol { @@ -234,12 +247,10 @@ impl Symbol { } } - /// Check if documentation contains deprecated marker + /// Whether a doc line marks the symbol as deprecated, see [`is_deprecation_marker`]. #[inline(always)] pub fn is_deprecated(&self) -> bool { - self.doc - .iter() - .any(|(_, text)| text.contains("deprecated") || text.contains("Deprecated") || text.contains("DEPRECATED")) + self.doc.iter().any(|(_, text)| is_deprecation_marker(text)) } } @@ -270,6 +281,38 @@ mod tests { } } + #[rstest] + #[case("deprecated", true)] + #[case("deprecated:", true)] + #[case("Deprecated: use foo instead", true)] + #[case("DEPRECATED: old", true)] + #[case(" deprecated: ", true)] + #[case("use this instead of the deprecated foo API", false)] + #[case("see the deprecated section below", false)] + fn test_is_deprecation_marker(#[case] text: &str, #[case] expected: bool) { + assert_eq!(is_deprecation_marker(text), expected); + } + + #[rstest] + #[case("deprecated: use foo instead", Some("use foo instead"))] + #[case(" Deprecated: trimmed message ", Some("trimmed message"))] + #[case("deprecated:", None)] + #[case("deprecated", None)] + fn test_deprecation_message(#[case] text: &str, #[case] expected: Option<&str>) { + assert_eq!(deprecation_message(text).as_deref(), expected); + } + + #[test] + fn test_is_deprecated_ignores_lines_that_only_mention_the_word() { + let mut symbol = create_test_symbol(SymbolKind::Function(Vec::new()), Some("f")); + symbol.doc = vec![(mq_lang::Range::default(), "Replaces the deprecated g.".to_string())]; + assert!(!symbol.is_deprecated()); + symbol + .doc + .push((mq_lang::Range::default(), "Deprecated: use h.".to_string())); + assert!(symbol.is_deprecated()); + } + #[rstest] #[case(SymbolKind::Function(Vec::new()), true)] #[case(SymbolKind::Function(vec![ParamInfo { name: SmolStr::from("param"), has_default: false, is_variadic: false }]), true)] diff --git a/crates/mq-lang/builtin.mq b/crates/mq-lang/builtin.mq index 924e6f7da..0f24e1a32 100644 --- a/crates/mq-lang/builtin.mq +++ b/crates/mq-lang/builtin.mq @@ -2595,7 +2595,7 @@ def stats_by(arr, group_f, value_f, aggs = ["sum", "mean", "min", "max", "stddev end # Buckets a numeric array into `buckets` equal-width bins between its min and -# max, returning an array of `{range, count, bar}` โ€” `bar` is a block-character +# max, returning an array of `{range, count, bar}`; `bar` is a block-character # bar scaled so the tallest bucket is 40 characters wide, handy for a quick # terminal or Markdown look at a distribution. # diff --git a/crates/mq-lang/builtin_tests.mq b/crates/mq-lang/builtin_tests.mq index 473230750..74167710a 100644 --- a/crates/mq-lang/builtin_tests.mq +++ b/crates/mq-lang/builtin_tests.mq @@ -114,6 +114,58 @@ def test_is_callout_non_node(): | assert_eq(result, false) end +# @parametrize([[1], [2], [3], [4], [5], [6]]) +def test_is_h_level(level): + let h = to_h("t", level) + | let matches = [is_h1(h), is_h2(h), is_h3(h), is_h4(h), is_h5(h), is_h6(h)] + | assert_eq(matches, map(range(1, 6), fn(i): i == level;)) + | assert_eq(is_h_level(h, level), true) + | assert_eq(is_h_level(h, level % 6 + 1), false) + | assert_eq(is_h(h), true) +end + +def test_is_node_kind(): + let first_node = fn(source): do source | to_markdown() | first();; + | let text = first_node("plain") + | let list = first_node("- item") + | let yaml = first_node("---\na: 1\n---\n") + | let toml = first_node("+++\nb = 1\n+++\n") + | let html = first_node("
x
\n") + | let code = first_node("```\nx\n```\n") + | assert_eq([is_text(text), is_text(list), is_text(code)], [true, false, false]) + | assert_eq([is_list(list), is_list(text)], [true, false]) + | assert_eq([is_yaml(yaml), is_yaml(toml)], [true, false]) + | assert_eq([is_toml(toml), is_toml(yaml)], [true, false]) + | assert_eq([is_html(html), is_html(text)], [true, false]) + | assert_eq([is_code(code), is_code(html)], [true, false]) +end + +def test_is_table_cell_and_align(): + let cells = to_markdown("| a | b |\n|:-:|--|\n| 1 | 2 |\n") + | assert_eq(is_table_cell(cells[0]), true) + | assert_eq(is_table_align(cells[0]), false) + | assert_eq(is_table_align(cells[2]), true) + | assert_eq(is_table_cell(cells[2]), false) +end + +def test_is_em(): + assert_eq(is_em(to_em("a")), true) + | assert_eq(is_em(to_h("a", 1)), false) +end + +def test_is_markdown_and_markdowns(): + let h = to_h("t", 1) + | assert_eq(is_markdown(h), true) + | assert_eq(is_markdown("t"), false) + | assert_eq(markdowns(h), h) + | assert_eq(markdowns("t"), None) +end + +def test_is_bytes(): + assert_eq(is_bytes(b"ab"), true) + | assert_eq(is_bytes("ab"), false) +end + # String manipulation tests # @parametrize([ # ["hello world", "world", true], @@ -465,6 +517,17 @@ def test_sort_natural(value, expected): | assert_eq(result, expected) end +# @parametrize([ +# [[3, 1, 2], fn(x): x;, [1, 2, 3]], +# [["bb", "a", "ccc"], len, ["a", "bb", "ccc"]], +# [[{"k": 2}, {"k": 1}], fn(x): x["k"];, [{"k": 1}, {"k": 2}]], +# [[], fn(x): x;, []], +# ]) +def test_sort_by(value, f, expected): + let result = sort_by(value, f) + | assert_eq(result, expected) +end + # @parametrize([[[1, 2, 3, 4, 5], 2, [3, 4, 5]], [[1, 2, 3], 0, [1, 2, 3]], [[1, 2, 3], 5, []]]) def test_skip(value, n, expected): let result = skip(value, n) @@ -697,6 +760,16 @@ def test_unique_by(value, f, expected): | assert_eq(result, expected) end +# @parametrize([[[1, 1, 2, 1], [1, 2]], [["a", "b", "a"], ["a", "b"]], [[], []]]) +def test_uniq(value, expected): + let result = uniq(value) + | assert_eq(result, expected) +end + +def test_uniq_non_array(): + assert_eq(try: uniq(1) catch: "error", "error") +end + # @parametrize([ # [[1, 2, 3], [2, 3, 4], [1, 2, 3, 4]], # [[1, 1, 2], [2, 2, 3], [1, 2, 3]], @@ -1711,15 +1784,15 @@ def test_walk(value, f, expected): end def test_walk_markdown(): - # markdown: text leaf โ€” identity preserves text content + # markdown text leaf: identity preserves text content let result1 = do "plain text" | to_markdown() | first() | walk(fn(n): n;) | to_text(); | assert_eq(result1, "plain text") - # markdown: heading โ€” identity preserves HTML output + # markdown heading: identity preserves HTML output | let result2 = do "# heading" | to_markdown() | first() | walk(fn(n): n;) | to_html(); | assert_eq(result2, "

heading

") - # markdown: h2 โ€” identity preserves depth + # markdown h2: identity preserves depth | let result3 = do "## section" | to_markdown() | first() | walk(fn(n): n;) | to_html(); | assert_eq(result3, "

section

") @@ -2158,3 +2231,20 @@ def test_lambda_without_params_fills_missing_argument_from_pipeline(): let one = fn: self; | assert_eq(42 | one(), 42) end + +def test_to_date_iso8601(): + assert_eq(to_date_iso8601(0), "1970-01-01T00:00:00Z") +end + +def test_stream_lines_chunks_and_bytes(): + write_file("stream.txt", "ab\ncd\nef") + | assert_eq(stream_lines("stream.txt") | collect(), ["ab", "cd", "ef"]) + | assert_eq(stream_chunks("stream.txt", 4) | collect() | map(len), [4, 4, 1]) + | assert_eq(stream_bytes("stream.txt") | collect() | len(), 8) +end + +def test_load_markdown(): + write_file("doc.md", "# Title\n\ntext") + | let doc = load_markdown("doc.md") + | assert_eq(map(doc, to_md_name), ["h1", "text"]) +end diff --git a/crates/mq-lang/modules/gen.mq b/crates/mq-lang/modules/gen.mq index 4d0de7c5a..70fd89489 100644 --- a/crates/mq-lang/modules/gen.mq +++ b/crates/mq-lang/modules/gen.mq @@ -1,6 +1,6 @@ # Generator combinators for property-based testing. # -# A generator is a plain function `fn(seed): value;` โ€” a deterministic mapping from an integer +# A generator is a plain function `fn(seed): value;`, a deterministic mapping from an integer # seed to a generated value. Combinators derive a distinct sub-seed per part so nested generators # don't correlate. # diff --git a/crates/mq-lang/modules/json.mq b/crates/mq-lang/modules/json.mq index 729225a97..7138e0ed2 100644 --- a/crates/mq-lang/modules/json.mq +++ b/crates/mq-lang/modules/json.mq @@ -536,7 +536,7 @@ end # `;;;` (the convention used by e.g. Hexo). Unlike YAML (`---`) and TOML # (`+++`) front matter, mq's Markdown parser does not recognize `;;;` blocks # as a distinct front matter node, so this output won't round-trip through -# `frontmatter`/`is_yaml`/`is_toml`-style selectors โ€” it's meant for +# `frontmatter`/`is_yaml`/`is_toml`-style selectors: it's meant for # generating files for tools that do support this convention. # # Example: diff --git a/crates/mq-lang/modules/md.mq b/crates/mq-lang/modules/md.mq index 08e4ecdb2..c02fef689 100644 --- a/crates/mq-lang/modules/md.mq +++ b/crates/mq-lang/modules/md.mq @@ -3,7 +3,7 @@ # This module is under development. APIs and behavior may change without notice. # # `md::reference_index` and `md::resolve_references` need every document node at -# once โ€” pass `-A` on the CLI, or pipe through `nodes` in an inline query/script; +# once; pass `-A` on the CLI, or pipe through `nodes` in an inline query/script; # a single node instead prints a stderr warning and is treated as a one-element array. # Wraps `value` in a heading node at the given `depth` (1-6). @@ -415,7 +415,7 @@ end # footnote definitions (`[^ident]: ...`), and indexes them by identifier. Each # identifier maps to an array of every definition found for it, in document # order, so a duplicate definition (multiple `[ident]:` lines for the same -# `ident`) shows up as an array with more than one entry โ€” CommonMark resolves +# `ident`) shows up as an array with more than one entry; CommonMark resolves # such a duplicate to the first one, which `md::resolve_references` picks. # Each entry carries its `location` (from `get_location`) for building # diagnostics like linkcheck output. @@ -471,8 +471,8 @@ end # `index` defaults to `md::reference_index(md_nodes)`, resolving references # against their own document. Pass an explicit index (e.g. built from a # different set of nodes) to resolve references that got separated from their -# definitions โ€” for example when a document is split or a section is moved -# into another file โ€” which is exactly the case where a reference legitimately +# definitions, for example when a document is split or a section is moved +# into another file, which is exactly the case where a reference legitimately # comes back unresolved. # # Example: diff --git a/crates/mq-lang/modules/section.mq b/crates/mq-lang/modules/section.mq index e50296a8d..53e47e9c5 100644 --- a/crates/mq-lang/modules/section.mq +++ b/crates/mq-lang/modules/section.mq @@ -2,7 +2,7 @@ # # Call it via `import "section"` then `section::fn()` (recommended, namespaced), # `include "section"` then `fn()` (no namespace prefix), or `mq -A 'section::fn()'` -# on the command line. Section functions need every document node at once โ€” pass +# on the command line. Section functions need every document node at once; pass # `-A` on the CLI, or pipe through `nodes` in an inline query/script; a single # node instead prints a stderr warning and is treated as a one-element array. # diff --git a/crates/mq-lang/modules/semver.mq b/crates/mq-lang/modules/semver.mq index 09df93a6c..85a815da3 100644 --- a/crates/mq-lang/modules/semver.mq +++ b/crates/mq-lang/modules/semver.mq @@ -395,7 +395,7 @@ end # either the hyphen range or ">=2.0.0"). # # Pre-release versions (e.g. "1.0.0-alpha") are ordered using the same precedence -# rules as `semver_compare` and are matched like any other version โ€” there is no +# rules as `semver_compare` and are matched like any other version; there is no # npm-style restriction that hides pre-releases from ranges that don't mention one. # # Example: semver_satisfies("1.5.0", ">=1.0.0,<2.0.0") == true diff --git a/crates/mq-lang/modules/table.mq b/crates/mq-lang/modules/table.mq index 6bf7dd855..bb3ade09a 100644 --- a/crates/mq-lang/modules/table.mq +++ b/crates/mq-lang/modules/table.mq @@ -1,7 +1,7 @@ # The table module extracts and transforms Markdown tables. This module is # under development; APIs and behavior may change without notice. # -# Call it via `import "table"` then `table::fn()` โ€” from an inline query, a +# Call it via `import "table"` then `table::fn()`, from an inline query, a # script, or `mq -A 'import "table" | table::fn()'` on the command line. # Unlike `section`, `import` must always be written explicitly (no `include` # shortcut). Table functions need every document node at once: pass `-A` on diff --git a/crates/mq-lang/src/arena.rs b/crates/mq-lang/src/arena.rs index 5ad026b24..f4f134695 100644 --- a/crates/mq-lang/src/arena.rs +++ b/crates/mq-lang/src/arena.rs @@ -68,7 +68,7 @@ impl ArenaId { /// that can be used to retrieve elements later. This pattern provides fast allocation /// and cache-friendly access. /// -/// A layered arena (see [`Arena::layered`]) sits on a parent arena: it allocates its own +/// A layered arena (see `Arena::layered`) sits on a parent arena: it allocates its own /// elements and still resolves the parent's ids, so it can be dropped once its ids are unused. #[derive(Debug, Clone, Default)] pub struct Arena { diff --git a/crates/mq-lang/src/ast/parser.rs b/crates/mq-lang/src/ast/parser.rs index 1ac13e6a4..50d7175b2 100644 --- a/crates/mq-lang/src/ast/parser.rs +++ b/crates/mq-lang/src/ast/parser.rs @@ -3072,7416 +3072,4 @@ impl<'a, 'alloc> Parser<'a, 'alloc> { } #[cfg(test)] -mod tests { - use crate::ast::node::StringSegment; - use crate::{Module, ast::node::MatchArm, range::Range, selector}; - - use super::*; - use rstest::rstest; - use smallvec::smallvec; - - fn token(token_kind: TokenKind) -> Token { - Token { - range: Range::default(), - kind: token_kind, - module_id: 1.into(), - } - } - - #[rstest] - #[case::ident1( - vec![ - token(TokenKind::Ident(SmolStr::new("and"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("contains"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("test".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("startswith"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("test2".to_owned())), - token(TokenKind::RParen), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("and", Some(4.into())), - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("contains", Some(1.into())), - smallvec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("test".to_owned())) - })], - ) - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("startswith", Some(3.into())), - smallvec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("test2".to_owned())) - })], - ) - }) - ], - ) - }) - ]))] - #[case::ident2( - vec![ - token(TokenKind::Ident(SmolStr::new("and"))), - token(TokenKind::LParen), - token(TokenKind::Selector(SmolStr::new(".h1"))), - token(TokenKind::Comma), - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 8.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("and", Some(6.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Selector(Selector::Heading(Some(1))), - }), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Selector(Selector::Table(Some(2), None)), - }), - ], - ) - }) - ]))] - #[case::ident3( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("filter"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("arg1"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("arg2"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("contains"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg1".to_owned())), - token(TokenKind::Comma), - token(TokenKind::StringLiteral("arg2".to_owned())), - token(TokenKind::RParen), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("filter", Some(7.into())), - vec![ - Param::new(IdentWithToken::new_with_token("arg1", Some(1.into()))), - Param::new(IdentWithToken::new_with_token("arg2", Some(2.into()))), - ], - vec![Shared::new(Node { - token_id: 6.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("contains", Some(6.into())), - smallvec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::String("arg1".to_owned())), - }), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("arg2".to_owned())), - }), - ], - ), - })], - ), - }), - ]))] - #[case::ident4( - vec![ - token(TokenKind::Ident(SmolStr::new("and"))), - token(TokenKind::LParen), - token(TokenKind::None), - token(TokenKind::Comma), - token(TokenKind::Self_), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("and", Some(2.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::None), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Self_, - }), - ], - ) - }) - ]))] - #[case::ident5( - vec![ - token(TokenKind::Ident(SmolStr::new("and"))), - token(TokenKind::LParen), - token(TokenKind::None), - token(TokenKind::Comma), - token(TokenKind::Self_), - token(TokenKind::RParen), - token(TokenKind::Ident(SmolStr::new("and"))), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident(SmolStr::new("and"))))))] - #[case::ident5( - vec![ - token(TokenKind::Ident(SmolStr::new("and"))), - token(TokenKind::LParen), - token(TokenKind::None), - token(TokenKind::Comma), - token(TokenKind::Self_), - token(TokenKind::RParen), - token(TokenKind::Def), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Def))))] - #[case::ident6( - vec![ - token(TokenKind::Ident(SmolStr::new("and"))), - token(TokenKind::Def), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident(SmolStr::new("and"))))))] - #[case::ident_attribute_access( - vec![ - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Selector(SmolStr::new(".lang"))), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("lang".to_owned())), - }), - ], - ), - }) - ]))] - #[case::error( - vec![ - token(TokenKind::Ident(SmolStr::new("contains"))), - token(TokenKind::LParen), - token(TokenKind::Selector(SmolStr::new("inline_code"))), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnknownSelector(selector::UnknownSelector::new(token(TokenKind::Selector(SmolStr::new("inline_code")))))))] - #[case::def1( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("name", Some(3.into())), - Vec::new(), - vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - })], - ), - }), - ]))] - #[case::def_with_end( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::End) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("name", Some(3.into())), - Vec::new(), - vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - })], - ), - }), - ]))] - #[case::def2( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::Comma), - token(TokenKind::RParen), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::Comma, module_id: 1.into()})))] - #[case::def3( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::Comma), - token(TokenKind::RParen), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] - #[case::def4( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] - #[case::def5( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] - #[case::def6( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::SemiColon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] - #[case::def7( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] - #[case::def7( - vec![ - token(TokenKind::Def), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::LParen, module_id: 1.into()})))] - #[case::def_without_colon1( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("name", Some(2.into())), - Vec::new(), - vec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - })], - ), - }), - ]))] - #[case::def_without_colon2( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::End) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("name", Some(2.into())), - Vec::new(), - vec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - })], - ), - }), - ]))] - #[case::def_without_colon_with_args( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("name", Some(3.into())), - vec![ - Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), - ], - vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident( - IdentWithToken::new_with_token("x", Some(2.into())), - ), - })], - ), - }), - ]))] - #[case::unmatched_end_at_root( - // `end` at root level followed by non-EOF: def foo: if (.): . end end | foo - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::Colon), - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::End), // closes def body - token(TokenKind::End), // unmatched โ€“ no open block - token(TokenKind::Pipe), // something after (like `| foo`) - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnmatchedEnd(token(TokenKind::End))))] - #[case::unmatched_end_standalone_if( - // `end` after a standalone single-line if (no open block): if (.): . end end - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::End), // unmatched - token(TokenKind::End), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnmatchedEnd(token(TokenKind::End))))] - #[case::unmatched_end_at_start( - // `end` as the very first token at root level - vec![ - token(TokenKind::End), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnmatchedEnd(token(TokenKind::End))))] - #[case::let_1( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Let( - Pattern::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - ), - }) - ]))] - #[case::let_2( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Equal), - token(TokenKind::StringLiteral("hello".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Let( - Pattern::Ident(IdentWithToken::new_with_token("y", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("hello".to_owned())), - }), - ), - }) - ]))] - #[case::let_3( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("flag"))), - token(TokenKind::Equal), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Let( - Pattern::Ident(IdentWithToken::new_with_token("flag", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - ), - }) - ]))] - #[case::let_4( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("z"))), - token(TokenKind::Equal), - token(TokenKind::Ident(SmolStr::new("some_var"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Let( - Pattern::Ident(IdentWithToken::new_with_token("z", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: - Expr::Ident(IdentWithToken::new_with_token("some_var", - Some(3.into()))) - }), - ), - }) - ]))] - #[case::let_5( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("z"))), - token(TokenKind::Equal), - token(TokenKind::Ident(SmolStr::new("some_var"))), - token(TokenKind::Pipe), - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Pipe))))] - #[case::let_6( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("z"))), - token(TokenKind::Equal), - token(TokenKind::Ident(SmolStr::new("some_var"))), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Let( - Pattern::Ident(IdentWithToken::new_with_token("z", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: - Expr::Ident(IdentWithToken::new_with_token("some_var", Some(3.into()))), - }), - ), - }) - ]))] - #[case::var_1( - vec![ - token(TokenKind::Var), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Var( - Pattern::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - ), - }) - ]))] - #[case::var_2( - vec![ - token(TokenKind::Var), - token(TokenKind::Ident(SmolStr::new("count"))), - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Var( - Pattern::Ident(IdentWithToken::new_with_token("count", Some(1.into()))), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ), - }) - ]))] - #[case::assign_1( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(100.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Assign( - IdentWithToken::new_with_token("x", Some(0.into())), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(100.into())), - }), - ), - }) - ]))] - #[case::assign_2( - vec![ - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::Equal), - token(TokenKind::StringLiteral("Alice".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Assign( - IdentWithToken::new_with_token("name", Some(0.into())), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("Alice".to_owned())), - }), - ), - }) - ]))] - #[case::index_assign( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(10.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::IndexAssign( -IdentWithToken::new_with_token("arr", Some(0.into())), -Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), -Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(10.into())), - }), -), - }) - ]))] - #[case::index_compound_assign( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::PlusEqual), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::IndexAssign( -IdentWithToken::new_with_token("arr", Some(0.into())), -Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), -Shared::new(Node { - token_id: 3.into(), - expr: Expr::BinaryOp( - BinaryOp::Add, - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ), - }), -), - }) - ]))] - #[case::index_double_slash_equal( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::DoubleSlashEqual), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - // arr[0] //= 2 โ†’ arr[0] = floor(div(get(arr, 0), 2)) - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::IndexAssign( -IdentWithToken::new_with_token("arr", Some(0.into())), -Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), -Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::FLOOR, Some(3.into())), - smallvec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::BinaryOp( - BinaryOp::Div, - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - })], - ), - }), -), - }) - ]))] - #[case::root_semicolon_error( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::SemiColon), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident(SmolStr::new("y"))))))] - #[case::if_1( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Else), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("false branch".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 7.into(), - expr: Expr::If(vec![ - ( - Some(Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - })), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("true branch".to_owned())), - }) - ), - ( - None, - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Literal(Literal::String("false branch".to_owned())), - }) - ) - ]), - }) - ]))] - #[case::if_elif_else( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Elif), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("elif branch".to_owned())), - token(TokenKind::Else), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("else branch".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 11.into(), - expr: Expr::If(vec![ - ( - Some(Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - })), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("true branch".to_owned())), - }) - ), - ( - Some(Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::Bool(false)), - })), - Shared::new(Node { - token_id: 7.into(), - expr: Expr::Literal(Literal::String("elif branch".to_owned())), - }) - ), - ( - None, - Shared::new(Node { - token_id: 10.into(), - expr: Expr::Literal(Literal::String("else branch".to_owned())), - }) - ) - ]), - }) - ]))] - #[case::if_only( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::If(vec![ - ( - Some(Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - })), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("true branch".to_owned())), - }) - ), - ]), - }) - ]))] - #[case::if_elif( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Elif), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 8.into(), - expr: Expr::If(vec![ - ( - Some(Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - })), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("true branch".to_owned())), - }) - ), - ( - Some(Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::Bool(true)), - })), - Shared::new(Node { - token_id: 7.into(), - expr: Expr::Literal(Literal::String("true branch".to_owned())), - }) - ), - ]), - }) - ]))] - #[case::if_error( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Elif), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("elif branch".to_owned())), - token(TokenKind::Else), - token(TokenKind::Colon), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFDetected(0.into())))] - #[case::if_error( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Elif), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("elif branch".to_owned())), - token(TokenKind::Else), - token(TokenKind::Colon), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::If, module_id: 1.into()})))] - #[case::elif_error( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("true branch".to_owned())), - token(TokenKind::Elif), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("elif branch".to_owned())), - token(TokenKind::Else), - token(TokenKind::Colon), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Elif, module_id: 1.into()})))] - #[case::h_selector( - vec![ - token(TokenKind::Selector(SmolStr::new(".h"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Selector(Selector::Heading(None)), - }) - ]))] - #[case::h_selector_without_number( - vec![ - token(TokenKind::Selector(SmolStr::new(".h"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Selector(Selector::Heading(None)), - }) - ]))] - #[case::while_( - vec![ - token(TokenKind::While), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("loop body".to_owned())), - token(TokenKind::SemiColon), - ], - Ok(vec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::While( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - vec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("loop body".to_owned())), - })], - ), - })]))] - #[case::while_error( - vec![ - token(TokenKind::While), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("loop body".to_owned())), - token(TokenKind::SemiColon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::While, module_id: 1.into()})))] - #[case::while_error( - vec![ - token(TokenKind::While), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::While, module_id: 1.into()})))] - #[case::while_do_end( - vec![ - token(TokenKind::While), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Do), - token(TokenKind::StringLiteral("loop body".to_owned())), - token(TokenKind::End), - ], - Ok(vec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::While( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - vec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("loop body".to_owned())), - })], - ), - })]))] - #[case::loop_( - vec![ - token(TokenKind::Loop), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("loop body".to_owned())), - token(TokenKind::SemiColon), - ], - Ok(vec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Loop( - vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("loop body".to_owned())), - })], - ), - })]))] - #[case::loop_error_no_body( - vec![ - token(TokenKind::Loop), - token(TokenKind::Colon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Loop, module_id: 1.into()})))] - #[case::until_( - vec![ - token(TokenKind::Until), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("loop body".to_owned())), - token(TokenKind::SemiColon), - ], - Ok(vec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Until( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - vec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("loop body".to_owned())), - })], - ), - })]))] - #[case::until_error( - vec![ - token(TokenKind::Until), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("loop body".to_owned())), - token(TokenKind::SemiColon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Until, module_id: 1.into()})))] - #[case::unless_( - vec![ - token(TokenKind::Unless), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("branch".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Unless(vec![ - ( - Some(Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(false)), - })), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("branch".to_owned())), - }) - ), - ]), - }) - ]))] - #[case::unless_error( - vec![ - token(TokenKind::Unless), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("branch".to_owned())), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Unless, module_id: 1.into()})))] - #[case::try_catch( - vec![ - token(TokenKind::Try), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("error_expr"))), - token(TokenKind::Catch), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("fallback".to_owned())), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Try( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(2.into()))), - }), - None, - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("fallback".to_owned())), - }), - ), - })]))] - #[case::try_catch_with_binder( - vec![ - token(TokenKind::Try), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("error_expr"))), - token(TokenKind::Catch), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("e"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("e"))), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Try( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(2.into()))), - }), - Some(IdentWithToken::new_with_token("e", Some(4.into()))), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("e", Some(6.into()))), - }), - ), - })]))] - #[case::foreach( - vec![ - token(TokenKind::Foreach), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("item"))), - token(TokenKind::Comma), - token(TokenKind::StringLiteral("array".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("print"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("item"))), - token(TokenKind::RParen), - token(TokenKind::SemiColon), - ], - Ok(vec![Shared::new(Node { - token_id: 6.into(), - expr: Expr::Foreach( - IdentWithToken::new_with_token( - "item", - Some(0.into()), - ), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("array".to_owned())), - }), - vec![Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token( - "print", - Some(4.into()), - ), - smallvec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::Ident(IdentWithToken::new_with_token( - "item", - Some(3.into()), - )), - })], - ), - })], - ), - })]))] - #[case::foreach( - vec![ - token(TokenKind::Foreach), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("print"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("item"))), - token(TokenKind::RParen), - token(TokenKind::SemiColon), - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Foreach, module_id: 1.into()})))] - #[case::foreach_do_end( - vec![ - token(TokenKind::Foreach), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("item"))), - token(TokenKind::Comma), - token(TokenKind::StringLiteral("array".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Do), - token(TokenKind::Ident(SmolStr::new("print"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("item"))), - token(TokenKind::RParen), - token(TokenKind::End), - ], - Ok(vec![Shared::new(Node { - token_id: 6.into(), - expr: Expr::Foreach( - IdentWithToken::new_with_token( - "item", - Some(0.into()), - ), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("array".to_owned())), - }), - vec![Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token( - "print", - Some(4.into()), - ), - smallvec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::Ident(IdentWithToken::new_with_token( - "item", - Some(3.into()), - )), - })], - ), - })], - ), - })]))] - #[case::self_( - vec![token(TokenKind::Self_), token(TokenKind::Eof)], - Ok(vec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Self_, - })]))] - #[case::include( - vec![ - token(TokenKind::Include), - token(TokenKind::StringLiteral("module_name".to_owned())), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Include(Literal::String("module_name".to_owned())), - })]))] - #[case::code_selector_with_language( - vec![ - token(TokenKind::Selector(SmolStr::new(".code"))), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Selector(Selector::Code), - })]))] - #[case::selector_call_heading_single_arg( - vec![ - token(TokenKind::Selector(SmolStr::new(".h"))), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::SelectorCall( - Selector::Heading(None), - // arg literal is allocated first (id=0), then the selector token (id=1) - Box::new(smallvec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - })]), - ), - })]))] - #[case::selector_call_heading_multi_arg( - vec![ - token(TokenKind::Selector(SmolStr::new(".h"))), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Comma), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::SelectorCall( - Selector::Heading(None), - // args are allocated first (id=0, id=1), then the selector token (id=2) - Box::new(smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ]), - ), - })]))] - #[case::selector_call_code_lang( - vec![ - token(TokenKind::Selector(SmolStr::new(".code"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("rust".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::SelectorCall( - Selector::Code, - // arg literal is allocated first (id=0), then the selector token (id=1) - Box::new(smallvec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("rust".to_owned())), - })]), - ), - })]))] - #[case::selector_call_with_attribute( - vec![ - token(TokenKind::Selector(SmolStr::new(".h"))), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RParen), - token(TokenKind::Selector(SmolStr::new(".level"))), - token(TokenKind::Eof), - ], - 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: Expr::Call( - IdentWithToken::new_with_token( - constants::builtins::ATTR, - Some(1.into()), - ), - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::SelectorCall( - Selector::Heading(None), - Box::new(smallvec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - })]), - ), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("level".to_owned())), - }), - ], - ), - })]))] - #[case::selector_call_code_with_lang_attribute( - vec![ - token(TokenKind::Selector(SmolStr::new(".code"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("rust".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Selector(SmolStr::new(".lang"))), - token(TokenKind::Eof), - ], - 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: Expr::Call( - IdentWithToken::new_with_token( - constants::builtins::ATTR, - Some(1.into()), - ), - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::SelectorCall( - Selector::Code, - Box::new(smallvec![Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("rust".to_owned())), - })]), - ), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("lang".to_owned())), - }), - ], - ), - })]))] - #[case::table_selector( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 8.into(), - expr: Expr::Selector(Selector::Table(Some(1), Some(2))), - })]))] - #[case::selector_call_list_bracket_single_arg( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::SelectorCall( - Selector::List(None, None), - Box::new(smallvec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(2.into())), - })]), - ), - })]))] - #[case::selector_call_table_bracket_single_arg( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::SelectorCall( - Selector::Table(None, None), - Box::new(smallvec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(1.into())), - })]), - ), - })]))] - #[case::selector_call_list_bracket_variable( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("v"))), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::SelectorCall( - Selector::List(None, None), - Box::new(smallvec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token( - "v", - Some(1.into()), - )), - })]), - ), - })]))] - #[case::selector_call_table_bracket_column_variable( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("v"))), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 5.into(), - expr: Expr::SelectorCall( - Selector::Table(None, None), - Box::new(smallvec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::None), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token( - "v", - Some(2.into()), - )), - }), - ]), - ), - })]))] - #[case::selector_call_table_bracket_variable( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("v"))), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 4.into(), - expr: Expr::SelectorCall( - Selector::Table(None, None), - Box::new(smallvec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token( - "v", - Some(1.into()), - )), - })]), - ), - })]))] - #[case::selector_call_table_bracket_row_col_args( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Comma), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 4.into(), - expr: Expr::SelectorCall( - Selector::Table(None, None), - Box::new(smallvec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ]), - ), - })]))] - #[case::foreach_error( - vec![ - token(TokenKind::Foreach), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("item"))), - token(TokenKind::Comma), - token(TokenKind::StringLiteral("array".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnexpectedEOFDetected(Module::TOP_LEVEL_MODULE_ID)))] - #[case::while_error( - vec![ - token(TokenKind::While), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnexpectedEOFDetected(Module::TOP_LEVEL_MODULE_ID)))] - #[case::using_reserved_keyword_let( - vec![ - token(TokenKind::Let), - token(TokenKind::If), // Using "if" as a variable name (should error) - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::If, module_id: 1.into()})))] - #[case::using_reserved_keyword_while( - vec![ - token(TokenKind::Let), - token(TokenKind::While), // Using "while" as a variable name (should error) - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::While, module_id: 1.into()})))] - #[case::using_reserved_keyword_def( - vec![ - token(TokenKind::Let), - token(TokenKind::Def), // Using "def" as a variable name (should error) - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::Def, module_id: 1.into()})))] - #[case::using_reserved_keyword_include( - vec![ - token(TokenKind::Let), - token(TokenKind::Include), // Using "include" as a variable name (should error) - token(TokenKind::Equal), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::Include, module_id: 1.into()})))] - #[case::nodes( - vec![ - token(TokenKind::Nodes), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Nodes, - }) - ]))] - #[case::nodes_error_in_subprogram( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("test"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Nodes), - token(TokenKind::SemiColon) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Nodes))))] - #[case::nodes_then_selector( - vec![ - token(TokenKind::Nodes), - token(TokenKind::Pipe), - token(TokenKind::Selector(SmolStr::new(".h1"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Nodes, - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Selector(Selector::Heading(Some(1))), - }) - ]))] - #[case::root_level_with_multiple_pipes( - vec![ - token(TokenKind::Nodes), - token(TokenKind::Pipe), - token(TokenKind::Nodes), - token(TokenKind::Pipe), - token(TokenKind::Selector(SmolStr::new(".h1"))), - token(TokenKind::Pipe), - token(TokenKind::Selector(SmolStr::new(".text"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Nodes, - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Nodes, - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Selector(Selector::Heading(Some(1))), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Selector(Selector::Text), - }) - ]))] - #[case::fn_simple( - vec![ - token(TokenKind::Fn), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("result".to_owned())), - token(TokenKind::SemiColon), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - Vec::new(), - vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("result".to_owned())), - }) - ], - ), - }) - ]))] - #[case::fn_with_args( - vec![ - token(TokenKind::Fn), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("contains"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::RParen), - token(TokenKind::SemiColon), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - vec![ - Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), - Param::new(IdentWithToken::new_with_token("y", Some(2.into()))), - ], - vec![ - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("contains", Some(6.into())), - smallvec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(4.into()))), - }), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(5.into()))), - }), - ], - ), - }) - ], - ), - }) - ]))] - #[case::fn_with_multiple_statements( - vec![ - token(TokenKind::Fn), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("first".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::StringLiteral("second".to_owned())), - token(TokenKind::SemiColon), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - vec![ - Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), - ], - vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("first".to_owned())), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::String("second".to_owned())), - }) - ], - ), - }) - ]))] - #[case::fn_with_invalid_args( - vec![ - token(TokenKind::Fn), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("invalid".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("result".to_owned())), - token(TokenKind::SemiColon), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::StringLiteral("invalid".to_owned())))))] - #[case::fn_without_body( - vec![ - token(TokenKind::Fn), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::SemiColon), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::SemiColon))))] - #[case::fn_nested_in_call( - vec![ - token(TokenKind::Ident(SmolStr::new("apply"))), - token(TokenKind::LParen), - token(TokenKind::Fn), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("processed".to_owned())), - token(TokenKind::SemiColon), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("apply", Some(4.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - vec![ - Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), - ], - vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("processed".to_owned())), - }) - ], - ), - }) - ], - ), - }) - ]))] - #[case::empty_array( - vec![ - token(TokenKind::LBracket), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array(SmallVec::new()), - }) - ]))] - #[case::array_with_elements( - vec![ - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("first".to_owned())), - token(TokenKind::Comma), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("first".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - ], - ), - }) - ]))] - #[case::array_with_mixed_elements( - vec![ - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("text".to_owned())), - token(TokenKind::Comma), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::Comma), - token(TokenKind::None), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("text".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::None), - }), - ], - ), - }) - ]))] - #[case::array_with_nested_array( - vec![ - token(TokenKind::LBracket), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RBracket), - token(TokenKind::Comma), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RBracket), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ], - ), - }), - ], - ), - }) - ]))] - #[case::array_with_trailing_comma( - vec![ - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::Comma), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - ], - ), - }) - ]))] - #[case::array_unclosed( - vec![ - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::Eof) - ], - Err(SyntaxError::ExpectedClosingBracket(token(TokenKind::Eof), Some(Box::new(token(TokenKind::LBracket))))))] - #[case::array_invalid_token( - vec![ - token(TokenKind::LBracket), - token(TokenKind::Pipe), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Pipe))))] - #[case::array_nested_unclosed( - vec![ - token(TokenKind::LBracket), - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("inner".to_owned())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Err(SyntaxError::ExpectedClosingBracket(token(TokenKind::Eof), Some(Box::new(token(TokenKind::LBracket))))))] - #[case::array_with_ident( - vec![ - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("bar"))), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("foo", Some(1.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("bar", Some(2.into()))), - }), - ], - ), - }) - ]))] - #[case::equality_simple( - vec![ - token(TokenKind::StringLiteral("hello".to_owned())), - token(TokenKind::EqEq), - token(TokenKind::StringLiteral("world".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("hello".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("world".to_owned())), - }), - ), - }) - ]))] - #[case::equality_numbers( - vec![ - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::EqEq), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - ), - }) - ]))] - #[case::equality_booleans( - vec![ - token(TokenKind::BoolLiteral(true)), - token(TokenKind::EqEq), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Bool(false)), - }), - ), - }) - ]))] - #[case::equality_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::EqEq), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), - }), - ), - }) - ]))] - #[case::equality_with_function_call( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg".to_owned())), - token(TokenKind::RParen), - token(TokenKind::EqEq), - token(TokenKind::StringLiteral("result".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("foo", Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("arg".to_owned())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("result".to_owned())), - }), - ), - }) - ]))] - #[case::equality_with_selectors( - vec![ - token(TokenKind::Selector(SmolStr::new(".h1"))), - token(TokenKind::EqEq), - token(TokenKind::Selector(SmolStr::new(".text"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Selector(Selector::Heading(Some(1))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Selector(Selector::Text), - }), - ), - }) - ]))] - #[case::equality_with_none( - vec![ - token(TokenKind::None), - token(TokenKind::EqEq), - token(TokenKind::None), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::None), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::None), - }), - ), - }) - ]))] - #[case::equality_error_missing_rhs( - vec![ - token(TokenKind::StringLiteral("hello".to_owned())), - token(TokenKind::EqEq), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::EqEq))))] - #[case::equality_in_if_condition( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::EqEq), - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("equal".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 6.into(), - expr: Expr::If(vec![ - ( - Some(Shared::new(Node { - token_id: 2.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(5.into())), - }), - ), - })), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("equal".to_owned())), - }) - ), - ]), - }) - ]))] - #[case::not_equality_simple( - vec![ - token(TokenKind::StringLiteral("hello".to_owned())), - token(TokenKind::NeEq), - token(TokenKind::StringLiteral("world".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("hello".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("world".to_owned())), - }), - ), - }) - ]))] - #[case::not_equality_numbers( - vec![ - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::NeEq), - token(TokenKind::NumberLiteral(24.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(24.into())), - }), - ), - }) - ]))] - #[case::not_equality_booleans( - vec![ - token(TokenKind::BoolLiteral(true)), - token(TokenKind::NeEq), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Bool(false)), - }), - ), - }) - ]))] - #[case::not_equality_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::NeEq), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), - }), - ), - }) - ]))] - #[case::not_equality_with_function_call( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg".to_owned())), - token(TokenKind::RParen), - token(TokenKind::NeEq), - token(TokenKind::StringLiteral("result".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("foo", Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("arg".to_owned())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("result".to_owned())), - }), - ), - }) - ]))] - #[case::not_equality_with_selectors( - vec![ - token(TokenKind::Selector(SmolStr::new(".h1"))), - token(TokenKind::NeEq), - token(TokenKind::Selector(SmolStr::new(".text"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Selector(Selector::Heading(Some(1))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Selector(Selector::Text), - }), - ), - }) - ]))] - #[case::not_equality_with_none( - vec![ - token(TokenKind::None), - token(TokenKind::NeEq), - token(TokenKind::StringLiteral("something".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::None), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("something".to_owned())), - }), - ), - }) - ]))] - #[case::not_equality_error_missing_rhs( - vec![ - token(TokenKind::StringLiteral("hello".to_owned())), - token(TokenKind::NeEq), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::NeEq))))] - #[case::not_equality_in_if_condition( - vec![ - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::NeEq), - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("not equal".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 6.into(), - expr: Expr::If(vec![ - ( - Some(Shared::new(Node { - token_id: 2.into(), - expr: Expr::BinaryOp( - BinaryOp::Ne, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(5.into())), - }), - ), - })), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("not equal".to_owned())), - }) - ), - ]), - }) - ]))] - #[case::plus_simple( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Plus), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Add, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ]))] - #[case::plus_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Plus), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Add, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), - }), - ), - }) - ]))] - #[case::plus_error_missing_rhs( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Plus), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Plus))))] - #[case::lt_simple( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Lt), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Lt, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ]))] - #[case::lte_simple( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Lte), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Le, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ]))] - #[case::gt_simple( - vec![ - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Gt), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Gt, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ]))] - #[case::gte_simple( - vec![ - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Gte), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Ge, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ]))] - #[case::dict_empty( - vec![ - token(TokenKind::LBrace), - token(TokenKind::RBrace), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Dict(SmallVec::new()), - }) - ]))] - #[case::dict_single_pair( - vec![ - token(TokenKind::LBrace), - token(TokenKind::Ident(SmolStr::new("key"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("value".to_owned())), - token(TokenKind::RBrace), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Dict( - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Symbol(Ident::new("key"))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - ], - ), - }), - ], - ), - }) - ]))] - #[case::dict_multiple_pairs( - vec![ - token(TokenKind::LBrace), - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Comma), - token(TokenKind::StringLiteral("b".to_owned())), - token(TokenKind::Colon), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::RBrace), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Dict( - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Symbol(Ident::new("a"))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("b".to_owned())), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Bool(true)), - }), - ], - ), - }), - ], - ), - }) - ]))] - #[case::dict_trailing_comma( - vec![ - token(TokenKind::LBrace), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(10.into())), - token(TokenKind::Comma), - token(TokenKind::RBrace), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Dict( - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Symbol(Ident::new("x"))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(10.into())), - }), - ], - ), - }), - ], - ), - }) - ]))] - #[case::dict_unclosed( - vec![ - token(TokenKind::LBrace), - token(TokenKind::Ident(SmolStr::new("k"))), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Eof) - ], - Err(SyntaxError::ExpectedClosingBrace(token(TokenKind::Eof), Some(Box::new(token(TokenKind::LBrace))))))] - #[case::dict_missing_colon( - vec![ - token(TokenKind::LBrace), - token(TokenKind::Ident(SmolStr::new("k"))), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RBrace), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::NumberLiteral(1.into())))))] - #[case::dict_invalid_key( - vec![ - token(TokenKind::LBrace), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("v".to_owned())), - token(TokenKind::RBrace), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::NumberLiteral(1.into())))))] - #[case::attr_h_value( - vec![ - token(TokenKind::Selector(".h".into())), - token(TokenKind::Selector(".value".into())), - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Selector(Selector::Heading(None)), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - - ], - )})]))] - #[case::attr( - vec![ - token(TokenKind::Selector(".list".into())), - token(TokenKind::Selector(".checked".into())), - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Selector(Selector::List(None, None)), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("checked".to_owned())), - }), - - ], - )})]))] - #[case::paren( - vec![ - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Plus), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RParen), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Paren( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::BinaryOp( - BinaryOp::Add, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ), - }) - ]))] - #[case::minus_simple( - vec![ - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::Minus), - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Sub, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(5.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - ), - }) - ]))] - #[case::minus_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Minus), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Sub, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - ), - }) - ]))] - #[case::slash_simple( - vec![ - token(TokenKind::NumberLiteral(6.into())), - token(TokenKind::Slash), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Div, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(6.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }) - ]))] - #[case::percent_simple( - vec![ - token(TokenKind::NumberLiteral(10.into())), - token(TokenKind::Percent), - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Mod, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(10.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - ), - }) - ]))] - #[case::percent_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Percent), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Mod, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - ), - }) - ]))] - #[case::percent_error_missing_rhs( - vec![ - token(TokenKind::NumberLiteral(10.into())), - token(TokenKind::Percent), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Percent))))] - #[case::mul_simple( - vec![ - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Asterisk), - token(TokenKind::NumberLiteral(4.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Mul, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(4.into())), - }), - ), - }) - ]))] - #[case::mul_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Asterisk), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Mul, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - ), - }) - ]))] - #[case::mul_error_missing_rhs( - vec![ - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::Asterisk), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Asterisk))))] - #[case::convert_simple( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Convert), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::CONVERT, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - ], - ), - }) - ]))] - #[case::convert_error_missing_rhs( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Convert), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Convert))))] - #[case::multiple_binary_operators( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Asterisk), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Asterisk), - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::BinaryOp( - BinaryOp::Mul, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Mul, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - ), - }) - ]))] - #[case::multiple_binary_operators_eq( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Plus), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::EqEq), - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::BinaryOp( - BinaryOp::Eq, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Add, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - ), - }) - ]))] - #[case::multiple_and_operators( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - ]), - }) - ]))] - #[case::multiple_or_operators( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("z"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("z", Some(4.into()))), - }), - ]), - }) - ]))] - #[case::and_or_mixed( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - ]), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - ]), - }) - ]))] - #[case::four_and_operators( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("d"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), - }), - ]), - }) - ]))] - #[case::four_or_operators( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("d"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), - }), - ]), - }) - ]))] - #[case::or_with_and_in_middle( - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("d"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - ]), - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), - }), - ]), - }) - ]))] - #[case::and_or_and_mixed( - // a && b || c && d => Or([And([a, b]), And([c, d])]) - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("d"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - ]), - }), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), - }), - ]), - }), - ]), - }) - ]))] - #[case::or_and_or_mixed( - // a || b && c || d => Or([a, And([b, c]), d]) - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("d"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - ]), - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), - }), - ]), - }) - ]))] - #[case::and_and_or_and_and_mixed( - // a && b && c || d && e && f => Or([And([a,b,c]), And([d,e,f])]) - vec![ - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("c"))), - token(TokenKind::Or), - token(TokenKind::Ident(SmolStr::new("d"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("e"))), - token(TokenKind::And), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), - }), - ]), - }), - Shared::new(Node { - token_id: 9.into(), - expr: Expr::And(vec![ - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), - }), - Shared::new(Node { - token_id: 8.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("e", Some(8.into()))), - }), - Shared::new(Node { - token_id: 10.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("f", Some(10.into()))), - }), - ]), - }), - ]), - }) - ]))] - #[case::range_simple( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::DoubleDot), - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::RANGE, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(5.into())), - }), - ], - ), - }) - ]))] - #[case::range_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("start"))), - token(TokenKind::DoubleDot), - token(TokenKind::Ident(SmolStr::new("end"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::RANGE, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("start", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("end", Some(2.into()))), - }), - ], - ), - }) - ]))] - #[case::range_error_missing_rhs( - vec![ - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::DoubleDot), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::DoubleDot))))] - #[case::args_missing_rparen( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("bar".to_owned())), - // Missing RParen - token(TokenKind::Eof) - ], - Err(SyntaxError::ExpectedClosingParen( - token(TokenKind::Eof), - Some(Box::new(token(TokenKind::LParen))), - )) - )] - #[case::args_unexpected_token( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Colon), // Invalid token in args - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Err(SyntaxError::ExpectedClosingParen( - token(TokenKind::Colon), - Some(Box::new(token(TokenKind::LParen))), - )) - )] - #[case::args_leading_comma( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("bar"))), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Comma))) - )] - #[case::args_double_comma( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("bar"))), - token(TokenKind::Comma), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("baz"))), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Comma))) - )] - #[case::binary_operator_chaining( - vec![ - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Gt), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Or), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Gt), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Or(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::BinaryOp( - BinaryOp::Gt, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ), - }), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::BinaryOp( - BinaryOp::Gt, - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ), - }), - ]), - }) - ]))] - #[case::not_simple( - vec![ - token(TokenKind::Not), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp(UnaryOp::Not, Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Bool(false)), - })), - }) - ]))] - #[case::not_with_expr( - vec![ - token(TokenKind::Not), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp(UnaryOp::Not, Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - })), - }) - ]))] - #[case::bracket_access_with_number( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(5.into())), - }), - ], - ), - }) - ]))] - #[case::bracket_access_with_string( - vec![ - token(TokenKind::Ident(SmolStr::new("dict"))), - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("key".to_owned())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token(constants::builtins::DICT, Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("key".to_owned())), - }), - ], - ), - }) - ]))] - #[case::bracket_access_error_missing_rbracket( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::Eof) - ], - Err(SyntaxError::ExpectedClosingBracket( - token(TokenKind::Eof), - Some(Box::new(token(TokenKind::LBracket))), - )))] - #[case::slice_access_with_numbers( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(3.into())), - }), - ], - ), - }) - ]))] - #[case::slice_access_with_variables( - vec![ - token(TokenKind::Ident(SmolStr::new("items"))), - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("start"))), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("end"))), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("items", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("start", Some(1.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("end", Some(2.into()))), - }), - ], - ), - }) - ]))] - #[case::not_with_paren_expr( - vec![ - token(TokenKind::Not), - token(TokenKind::LParen), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp(UnaryOp::Not, Shared::new(Node { - token_id: 1.into(), - expr: Expr::Paren( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Bool(false)), - }) - ), - })), - }) - ]))] - #[case::not_none( - vec![ - token(TokenKind::Not), - token(TokenKind::None), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp( - UnaryOp::Not, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::None), - }) - ), - }) - ]))] - #[case::not_error_missing_rhs( - vec![ - token(TokenKind::Not), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Not))))] - #[case::break_( - vec![ - token(TokenKind::Break), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Break(None), - }) - ]))] - #[case::continue_( - vec![ - token(TokenKind::Continue), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Continue, - }) - ]))] - #[case::self_bracket_access_with_number( - vec![ - token(TokenKind::Self_), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(5.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Self_, - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(5.into())), - }), - ], - ), - }) - ]))] - #[case::self_bracket_access_with_string( - vec![ - token(TokenKind::Self_), - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("key".to_owned())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Self_, - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("key".to_owned())), - }), - ], - ), - }) - ]))] - // Test function call followed by index access (e.g., foo()[0]) - #[case::function_call_with_index_access( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("foo", Some(0.into())), - SmallVec::new(), - ), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }) - ]))] - // Test function call with arguments followed by index access - #[case::function_call_with_args_and_index_access( - vec![ - token(TokenKind::Ident(SmolStr::new("bar"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg".to_owned())), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::StringLiteral("key".to_owned())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("bar", Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("arg".to_owned())), - }) - ], - ), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("key".to_owned())), - }), - ], - ), - }) - ]))] - // Test chained index access on function call result - #[case::function_call_with_chained_index_access( - vec![ - token(TokenKind::Ident(SmolStr::new("baz"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("baz", Some(0.into())), - SmallVec::new(), - ), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ], - ), - }) - ]))] - #[case::try_without_catch( - vec![ - token(TokenKind::Try), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("error_expr"))), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Try( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(2.into()))), - }), - None, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::None), - }), - ), - })]) - )] - // Test index access followed by function call (e.g., arr[0]()) - #[case::index_access_with_function_call( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::CallDynamic( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }), - SmallVec::new(), - ), - }) - ]))] - // Test index access with args followed by function call (e.g., arr[0](arg)) - #[case::index_access_with_function_call_and_args( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("test".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::CallDynamic( - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }), - smallvec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("test".to_owned())), - }) - ], - ), - }) - ]))] - // Test group expr with index access: (x)[0] โ†’ get(Paren(x), 0) - #[case::group_expr_with_index_access( - vec![ - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Paren( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - }), - ), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }) - ]))] - // Test group expr with dynamic call: (x)("test") โ†’ CallDynamic(Paren(x), ["test"]) - #[case::group_expr_with_dynamic_call( - vec![ - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("test".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::CallDynamic( - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Paren( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - }), - ), - }), - smallvec![ - Shared::new(Node { - token_id: 2.into(), - 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) - #[case::group_expr_with_chained_call_and_index( - vec![ - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::RParen), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("a".to_owned())), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::CallDynamic( - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Paren( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("f", Some(1.into()))), - }), - ), - }), - smallvec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("a".to_owned())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }) - ]))] - // Test array literal with index access: [1,2][0] โ†’ get(array([1,2]), 0) - #[case::array_literal_with_index_access( - vec![ - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Comma), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Array( - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }) - ]))] - // Test chained index access followed by function call (e.g., arr[0][1]()) - #[case::chained_index_access_with_function_call( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RBracket), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RBracket), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::CallDynamic( - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - ], - ), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ], - ), - }), - SmallVec::new(), - ), - }) - ]))] - #[case::function_call_with_question_mark( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Question), - token(TokenKind::Eof), - ], - Ok(vec![Shared::new(Node { - token_id: 1.into(), - expr: Expr::Try( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("foo", Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("arg".to_owned())), - }), - ], - ), - }), - None, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::None), - }), - ), - })]) - )] - #[case::question_mark_after_call( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Question), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Try( - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("foo", Some(0.into())), - SmallVec::new(), - ), - }), - None, - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::None), - }), - ), - }) - ]))] - #[case::question_mark_after_call_with_args( - vec![ - token(TokenKind::Ident(SmolStr::new("bar"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Question), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Try( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("bar", Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("arg".to_owned())), - }), - ], - ), - }), - None, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::None), - }), - ), - }) - ]))] - #[case::question_mark_after_call_error( - vec![ - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::Question), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident("foo".into())))))] - #[case::coalesce_simple( - vec![ - token(TokenKind::StringLiteral("foo".to_owned())), - token(TokenKind::Coalesce), - token(TokenKind::StringLiteral("bar".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::String("foo".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("bar".to_owned())), - }), - ], - ), - }) - ]))] - #[case::coalesce_with_none( - vec![ - token(TokenKind::None), - token(TokenKind::Coalesce), - token(TokenKind::StringLiteral("default".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Literal(Literal::None), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("default".to_owned())), - }), - ], - ), - }) - ]))] - #[case::coalesce_with_identifiers( - vec![ - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Coalesce), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), - }), - ], - ), - }) - ]))] - #[case::coalesce_error_missing_rhs( - vec![ - token(TokenKind::StringLiteral("foo".to_owned())), - token(TokenKind::Coalesce), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Coalesce))))] - #[case::negate_simple( - vec![ - token(TokenKind::Minus), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp(UnaryOp::Neg, Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(42.into())), - })), - }) - ]))] - #[case::negate_with_identifier( - vec![ - token(TokenKind::Minus), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp(UnaryOp::Neg, Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), - })), - }) - ]))] - #[case::negate_paren_expr( - vec![ - token(TokenKind::Minus), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RParen), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp( - UnaryOp::Neg, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Paren(Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(1.into())), - })), - }) - ), - }) - ]))] - #[case::negate_string( - vec![ - token(TokenKind::Minus), - token(TokenKind::StringLiteral("a".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::UnaryOp( - UnaryOp::Neg, - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("a".to_owned())), - }) - ), - }) - ]))] - #[case::negate_error_missing_rhs( - vec![ - token(TokenKind::Minus), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Minus))))] - #[case::import_simple( - vec![ - token(TokenKind::Import), - token(TokenKind::StringLiteral("name".to_owned())), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Import( - Literal::String("name".to_owned()), - None, - ), - }) - ]))] - #[case::import_as( - vec![ - token(TokenKind::Import), - token(TokenKind::StringLiteral("name".to_owned())), - token(TokenKind::As), - token(TokenKind::Ident(SmolStr::new("alias"))), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Import( - Literal::String("name".to_owned()), - Some(IdentWithToken::new_with_token( - "alias", - Some(1.into()), - )), - ), - }) - ]))] - #[case::import_as_missing_ident( - vec![ - token(TokenKind::Import), - token(TokenKind::StringLiteral("name".to_owned())), - token(TokenKind::As), - token(TokenKind::Eof), - ], - Err(SyntaxError::UnexpectedToken(token(TokenKind::Eof))))] - #[case::qualified_access( - vec![ - token(TokenKind::Ident(SmolStr::new("test"))), - token(TokenKind::DoubleColon), - token(TokenKind::Ident(SmolStr::new("foo"))), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::QualifiedAccess( - vec![IdentWithToken::new_with_token("test", Some(0.into()))], - AccessTarget::Ident(IdentWithToken::new_with_token("foo", Some(1.into())), - )), - }) - ]))] - #[case::qualified_access_with_call( - vec![ - token(TokenKind::Ident(SmolStr::new("mod"))), - token(TokenKind::DoubleColon), - token(TokenKind::Ident(SmolStr::new("func"))), - token(TokenKind::LParen), - token(TokenKind::StringLiteral("arg".to_owned())), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::QualifiedAccess( - vec![IdentWithToken::new_with_token("mod", Some(0.into()))], - AccessTarget::Call( - IdentWithToken::new_with_token("func", Some(2.into())), - Box::new(smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("arg".to_owned())), - }), - ]), - ), - ), - }) - ]))] - #[case::qualified_access_multi_level( - vec![ - token(TokenKind::Ident(SmolStr::new("mod1"))), - token(TokenKind::DoubleColon), - token(TokenKind::Ident(SmolStr::new("mod2"))), - token(TokenKind::DoubleColon), - token(TokenKind::Ident(SmolStr::new("func"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::QualifiedAccess( - vec![ - IdentWithToken::new_with_token("mod1", Some(0.into())), - IdentWithToken::new_with_token("mod2", Some(1.into())), - ], - AccessTarget::Call( - IdentWithToken::new_with_token("func", Some(2.into())), - Box::new(smallvec![]), - ), - ), - }) - ]))] - #[case::slice_access_with_start_only( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Colon), - token(TokenKind::RBracket), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("len", None), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - ], - ), - }), - ], - ), - }) - ]))] - #[case::slice_access_with_end_only( - vec![ - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::LBracket), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ], - ), - }) - ]))] - #[case::qualified_access_with_call_and_slice( - vec![ - token(TokenKind::Ident(SmolStr::new("mod"))), - token(TokenKind::DoubleColon), - token(TokenKind::Ident(SmolStr::new("func"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::QualifiedAccess( - vec![IdentWithToken::new_with_token("mod", Some(0.into()))], - AccessTarget::Call( - IdentWithToken::new_with_token("func", Some(1.into())), - Box::new(smallvec![]), - ), - ), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(2.into())), - }), - ], - ), - }) - ]))] - #[case::qualified_access_with_call_and_end_only_slice( - vec![ - token(TokenKind::Ident(SmolStr::new("mod"))), - token(TokenKind::DoubleColon), - token(TokenKind::Ident(SmolStr::new("func"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::LBracket), - token(TokenKind::Colon), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::RBracket), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::QualifiedAccess( - vec![IdentWithToken::new_with_token("mod", Some(0.into()))], - AccessTarget::Call( - IdentWithToken::new_with_token("func", Some(1.into())), - Box::new(smallvec![]), - ), - ), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(0.into())), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::Number(1.into())), - }), - ], - ), - }) - ]))] - #[case::selector_dot_is_self( - vec![ - token(TokenKind::Selector(SmolStr::new("."))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Self_, - }) - ]))] - #[case::ident_with_single_attr( - vec![ - token(TokenKind::Ident(SmolStr::new("obj"))), - token(TokenKind::Selector(SmolStr::new(".name"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("name".to_owned())), - }), - ], - ), - }) - ]))] - #[case::function_call_result_with_attr( - vec![ - token(TokenKind::Ident(SmolStr::new("get_user"))), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Selector(SmolStr::new(".name"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("get_user", Some(0.into())), - SmallVec::new(), - ), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("name".to_owned())), - }), - ], - ), - }) - ]))] - #[case::ident_with_attr_in_pipe( - vec![ - token(TokenKind::Ident(SmolStr::new("data"))), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("obj"))), - token(TokenKind::Selector(SmolStr::new(".value"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("data", Some(0.into()))), - }), - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(2.into())), - smallvec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(1.into()))), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - ], - ), - }) - ]))] - #[case::self_with_attr( - vec![ - token(TokenKind::Self_), - token(TokenKind::Selector(SmolStr::new(".value"))), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Self_, - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - ], - ), - }) - ]))] - #[case::pipe_equal_with_selector( - vec![ - token(TokenKind::Selector(SmolStr::new(".h1"))), - token(TokenKind::Selector(SmolStr::new(".value"))), - token(TokenKind::PipeEqual), - token(TokenKind::StringLiteral("new_id".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(2.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Selector(selector::Selector::Heading(Some(1))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("new_id".to_owned())), - }), - ], - ), - }) - ]))] - #[case::pipe_equal_with_ident_and_attr( - vec![ - token(TokenKind::Ident(SmolStr::new("obj"))), - token(TokenKind::Selector(SmolStr::new(".value"))), - token(TokenKind::PipeEqual), - token(TokenKind::StringLiteral("John".to_owned())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(2.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(0.into()))), - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("John".to_owned())), - }), - ], - ), - }) - ]))] - #[case::pipe_equal_with_self_and_attr( - vec![ - token(TokenKind::Self_), - token(TokenKind::Selector(SmolStr::new(".value"))), - token(TokenKind::PipeEqual), - token(TokenKind::NumberLiteral(42.into())), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Call( - IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(2.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Self_, - }), - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::String("value".to_owned())), - }), - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::Number(42.into())), - }), - ], - ), - }) - ]))] - #[case::let_with_reserved_keyword_as_value( - vec![ - token(TokenKind::Let), - token(TokenKind::Ident(SmolStr::new("aaa"))), - token(TokenKind::Equal), - token(TokenKind::Let), // Using "let" as a value (should error) - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token { - range: Range::default(), - kind: TokenKind::Let, - module_id: 1.into(), - })) - )] - #[case::let_with_reserved_keyword_as_variable_and_value( - vec![ - token(TokenKind::Let), - token(TokenKind::Let), // Using "let" as a variable name (should error) - token(TokenKind::Equal), - token(TokenKind::Ident(SmolStr::new("vvv"))), - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token { - range: Range::default(), - kind: TokenKind::Let, - module_id: 1.into(), - })) - )] - #[case::let_with_reserved_keyword_as_variable_and_value2( - vec![ - token(TokenKind::Let), - token(TokenKind::Let), // Using "let" as a variable name (should error) - token(TokenKind::Equal), - token(TokenKind::Let), // Using "let" as a value (should error) - token(TokenKind::Eof) - ], - Err(SyntaxError::UnexpectedToken(Token { - range: Range::default(), - kind: TokenKind::Let, - module_id: 1.into(), - })) - )] - #[case::def_with_variadic_param( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::LParen), - token(TokenKind::Asterisk), - token(TokenKind::Ident(SmolStr::new("args"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("args"))), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("f", Some(4.into())), - vec![ - Param::variadic(IdentWithToken::new_with_token("args", Some(1.into()))), - ], - vec![Shared::new(Node { - token_id: 3.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(3.into()))), - })], - ), - }), - ]))] - #[case::def_with_regular_and_variadic_param( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Comma), - token(TokenKind::Asterisk), - token(TokenKind::Ident(SmolStr::new("rest"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("rest"))), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("f", Some(5.into())), - vec![ - Param::new(IdentWithToken::new_with_token("a", Some(1.into()))), - Param::variadic(IdentWithToken::new_with_token("rest", Some(2.into()))), - ], - vec![Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("rest", Some(4.into()))), - })], - ), - }), - ]))] - #[case::def_variadic_param_not_last( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::LParen), - token(TokenKind::Asterisk), - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::RParen), - ], - Err(SyntaxError::VariadicParameterMustBeLast(Token{range: Range::default(), kind: TokenKind::Ident(SmolStr::new("b")), module_id: 1.into()})))] - #[case::def_multiple_variadic_params( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::LParen), - token(TokenKind::Asterisk), - token(TokenKind::Ident(SmolStr::new("a"))), - token(TokenKind::Comma), - token(TokenKind::Asterisk), - token(TokenKind::Ident(SmolStr::new("b"))), - token(TokenKind::RParen), - ], - Err(SyntaxError::MultipleVariadicParameters(Token{range: Range::default(), kind: TokenKind::Asterisk, module_id: 1.into()})))] - #[case::def_without_params( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("args"))), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("f", Some(3.into())), - vec![], - vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(2.into()))), - })], - ), - }), - ]))] - #[case::def_without_params_with_do( - vec![ - token(TokenKind::Def), - token(TokenKind::Ident(SmolStr::new("f"))), - token(TokenKind::Do), - token(TokenKind::Ident(SmolStr::new("args"))), - token(TokenKind::SemiColon) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Def( - IdentWithToken::new_with_token("f", Some(3.into())), - vec![], - vec![Shared::new(Node { - token_id: 2.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(2.into()))), - })], - ), - }), - ]))] - #[case::arrow_simple( - vec![ - token(TokenKind::Arrow), - token(TokenKind::LParen), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("result".to_owned())), - token(TokenKind::SemiColon), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - Vec::new(), - vec![ - Shared::new(Node { - token_id: 2.into(), - expr: Expr::Literal(Literal::String("result".to_owned())), - }) - ], - ), - }) - ]))] - #[case::arrow_with_args( - vec![ - token(TokenKind::Arrow), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("contains"))), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::RParen), - token(TokenKind::SemiColon), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - vec![ - Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), - Param::new(IdentWithToken::new_with_token("y", Some(2.into()))), - ], - vec![ - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("contains", Some(6.into())), - smallvec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(4.into()))), - }), - Shared::new(Node { - token_id: 5.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(5.into()))), - }), - ], - ), - }) - ], - ), - }) - ]))] - #[case::arrow_nested_in_call( - vec![ - token(TokenKind::Ident(SmolStr::new("apply"))), - token(TokenKind::LParen), - token(TokenKind::Arrow), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("processed".to_owned())), - token(TokenKind::SemiColon), - token(TokenKind::RParen), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Call( - IdentWithToken::new_with_token("apply", Some(4.into())), - smallvec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Fn( - vec![ - Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), - ], - vec![ - Shared::new(Node { - token_id: 3.into(), - expr: Expr::Literal(Literal::String("processed".to_owned())), - }) - ], - ), - }) - ], - ), - }) - ]))] - fn test_parse(#[case] input: Vec, #[case] expected: Result) { - let mut arena = Arena::new(10); - 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(); - assert_eq!(actual_exprs, expected_exprs); - } - (Err(actual), Err(expected)) => { - assert_eq!(actual, expected); - } - _ => { - panic!("Mismatch: actual = {:?}, expected = {:?}", result, expected) - } - } - } - - #[rstest] - #[case::heading(".h1", Selector::Heading(Some(1)))] - #[case::heading_h3(".h3", Selector::Heading(Some(3)))] - #[case::blockquote(".>", Selector::Blockquote)] - #[case::blockquote_full(".blockquote", Selector::Blockquote)] - #[case::footnote(".^", Selector::Footnote)] - #[case::footnote_full(".footnote", Selector::Footnote)] - #[case::mdx_jsx_flow(".mdx_jsx_flow_element", Selector::MdxJsxFlowElement)] - #[case::mdx_jsx_flow_short(".<", Selector::MdxJsxFlowElement)] - #[case::emphasis(".**", Selector::Emphasis)] - #[case::emphasis_full(".emphasis", Selector::Emphasis)] - #[case::math(".$$", Selector::Math)] - #[case::math_full(".math", Selector::Math)] - #[case::horizontal_rule(".---", Selector::HorizontalRule)] - #[case::horizontal_rule_alt(".***", Selector::HorizontalRule)] - #[case::horizontal_rule_full(".horizontal_rule", Selector::HorizontalRule)] - #[case::mdx_text_expression(".{}", Selector::MdxTextExpression)] - #[case::mdx_text_expression_full(".mdx_text_expression", Selector::MdxTextExpression)] - #[case::footnote_ref(".[^]", Selector::FootnoteRef)] - #[case::footnote_ref_full(".footnote_ref", Selector::FootnoteRef)] - #[case::definition(".definition", Selector::Definition)] - #[case::break_selector(".break", Selector::Break)] - #[case::delete(".delete", Selector::Delete)] - #[case::html(".<>", Selector::Html)] - #[case::html_full(".html", Selector::Html)] - #[case::image(".image", Selector::Image)] - #[case::image_ref(".image_ref", Selector::ImageRef)] - #[case::code(".code", Selector::Code)] - #[case::code_inline(".code_inline", Selector::InlineCode)] - #[case::math_inline(".math_inline", Selector::InlineMath)] - #[case::link(".link", Selector::Link)] - #[case::link_ref(".link_ref", Selector::LinkRef)] - #[case::list(".list", Selector::List(None, None))] - #[case::toml(".toml", Selector::Toml)] - #[case::strong(".strong", Selector::Strong)] - #[case::yaml(".yaml", Selector::Yaml)] - #[case::text(".text", Selector::Text)] - #[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm)] - #[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement)] - #[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression)] - fn test_parse_selector(#[case] selector_str: &str, #[case] expected_selector: Selector) { - let mut arena = Arena::new(10); - let token = Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(selector_str)), - module_id: 1.into(), - }; - - let tokens = [ - token.clone(), - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - - let result = Parser::new(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 { - assert_eq!(*selector, expected_selector); - } else { - panic!("Expected Selector expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[rstest] - #[case(".", Some(1), None, Selector::List(Some(1), None))] - #[case(".", Some(2), Some(3), Selector::Table(Some(2), Some(3)))] - fn test_parse_array_selector( - #[case] selector_str: &str, - #[case] first_idx: Option, - #[case] second_idx: Option, - #[case] expected_selector: Selector, - ) { - let mut arena = Arena::new(10); - let mut tokens = vec![ - Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(selector_str)), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::LBracket, - module_id: 1.into(), - }, - ]; - - if let Some(idx) = first_idx { - tokens.push(Token { - range: Range::default(), - kind: TokenKind::NumberLiteral(idx.into()), - module_id: 1.into(), - }); - } - - tokens.push(Token { - range: Range::default(), - kind: TokenKind::RBracket, - module_id: 1.into(), - }); - - if second_idx.is_some() { - tokens.push(Token { - range: Range::default(), - kind: TokenKind::LBracket, - module_id: 1.into(), - }); - - if let Some(idx) = second_idx { - tokens.push(Token { - range: Range::default(), - kind: TokenKind::NumberLiteral(idx.into()), - module_id: 1.into(), - }); - } - - tokens.push(Token { - range: Range::default(), - kind: TokenKind::RBracket, - module_id: 1.into(), - }); - } - - tokens.push(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }); - - let result = Parser::new(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 { - assert_eq!(*selector, expected_selector); - } else { - panic!("Expected Selector expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - // ."key"[] or ."key"[n] โ€” single property selector followed by array iterator/index - #[rstest] - #[case::all_elements(".\"items\"", None, vec![Selector::Property("items".into()), Selector::List(None, None)])] - #[case::index_0(".\"items\"", Some(0usize), vec![Selector::Property("items".into()), Selector::List(Some(0), None)])] - #[case::index_2(".\"items\"", Some(2usize), vec![Selector::Property("items".into()), Selector::List(Some(2), None)])] - fn test_parse_property_selector_iterator( - #[case] selector_str: &str, - #[case] index: Option, - #[case] expected: Vec, - ) { - let mut arena = Arena::new(10); - let mut tokens = vec![ - Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(selector_str)), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::LBracket, - module_id: 1.into(), - }, - ]; - if let Some(idx) = index { - tokens.push(Token { - range: Range::default(), - kind: TokenKind::NumberLiteral(idx.into()), - module_id: 1.into(), - }); - } - tokens.push(Token { - range: Range::default(), - kind: TokenKind::RBracket, - module_id: 1.into(), - }); - tokens.push(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }); - - let result = Parser::new(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 { - 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 { - panic!("expected Selector, got {:?}", node.expr); - }; - assert_eq!(actual, sel); - } - } - - // ."a"."b"[] or ."a"."b"[n] โ€” chained property selectors followed by array iterator/index - #[rstest] - #[case::all_elements(".\"a\"", ".\"b\"", None, vec![Selector::Property("a".into()), Selector::Property("b".into()), Selector::List(None, None)])] - #[case::index_0(".\"a\"", ".\"b\"", Some(0usize), vec![Selector::Property("a".into()), Selector::Property("b".into()), Selector::List(Some(0), None)])] - #[case::index_1(".\"a\"", ".\"b\"", Some(1usize), vec![Selector::Property("a".into()), Selector::Property("b".into()), Selector::List(Some(1), None)])] - fn test_parse_chained_property_selector_iterator( - #[case] first: &str, - #[case] second: &str, - #[case] index: Option, - #[case] expected: Vec, - ) { - let mut arena = Arena::new(10); - let mut tokens = vec![ - Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(first)), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(second)), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::LBracket, - module_id: 1.into(), - }, - ]; - if let Some(idx) = index { - tokens.push(Token { - range: Range::default(), - kind: TokenKind::NumberLiteral(idx.into()), - module_id: 1.into(), - }); - } - tokens.push(Token { - range: Range::default(), - kind: TokenKind::RBracket, - module_id: 1.into(), - }); - tokens.push(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }); - - let result = Parser::new(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 { - 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 { - panic!("expected Selector, got {:?}", node.expr); - }; - assert_eq!(actual, sel); - } - } - - #[test] - fn test_parse_as_binding() { - let mut arena = Arena::new(10); - let tokens = [ - Token { - range: Range::default(), - kind: TokenKind::NumberLiteral(42.into()), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::As, - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Ident("x".into()), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - let result = Parser::new(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 { - panic!("expected Expr::As, got {:?}", program[0].expr); - }; - assert_eq!(ident.name.to_string(), "x"); - assert!(matches!(&inner.expr, Expr::Literal(Literal::Number(_)))); - } - - #[test] - fn test_parse_env() { - unsafe { std::env::set_var("MQ_TEST_VAR", "test_value") }; - let _guard = io_context::scoped(Shared::new( - crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true), - )); - - let mut arena = Arena::new(10); - let tokens = [ - Token { - range: Range::default(), - kind: TokenKind::Env("MQ_TEST_VAR".into()), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - - let result = Parser::new(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 { - assert_eq!(value, "test_value"); - } else { - panic!("Expected String literal, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[test] - fn test_parse_env_not_found() { - let _guard = io_context::scoped(Shared::new( - crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true), - )); - - let mut arena = Arena::new(10); - let token = Token { - range: Range::default(), - kind: TokenKind::Env("MQ_NONEXISTENT_VAR".into()), - module_id: 1.into(), - }; - - let tokens = [ - token.clone(), - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - - let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); - - assert!(matches!( - result, - Err(SyntaxError::EnvNotFound(_, var)) if var == "MQ_NONEXISTENT_VAR" - )); - } - - #[test] - fn test_parse_env_in_arguments() { - unsafe { std::env::set_var("MQ_ARG_TEST", "env_arg_value") }; - let _guard = io_context::scoped(Shared::new( - crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true), - )); - - let mut arena = Arena::new(10); - let tokens = [ - Token { - range: Range::default(), - kind: TokenKind::Ident(SmolStr::new("function")), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::LParen, - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Env("MQ_ARG_TEST".into()), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::RParen, - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - - let result = Parser::new(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 { - assert_eq!(ident.name, "function".into()); - assert_eq!(args.len(), 1); - 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); - } - } else { - panic!("Expected Call expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[rstest] - #[case::lang(".lang", "lang")] - #[case::value(".value", "value")] - #[case::depth(".depth", "depth")] - fn test_parse_standalone_attr_selector(#[case] selector: &str, #[case] attribute: &str) { - let mut arena = Arena::new(10); - let tokens = [ - Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(selector)), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - - let result = Parser::new(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 { - 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_eq!(attr_str, attribute); - } else { - panic!("Expected String literal in second argument, got {:?}", args[1].expr); - } - } else { - panic!("Expected Call expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[rstest] - #[case::h_value(vec![".h", ".value"], "h", "value")] - #[case::h1_value(vec![".h1", ".value"], "h1", "value")] - #[case::code_lang(vec![".code", ".lang"], "code", "lang")] - #[case::text_value(vec![".text", ".value"], "text", "value")] - fn test_parse_selector_with_attribute( - #[case] selectors: Vec<&str>, - #[case] base_selector: &str, - #[case] attribute: &str, - ) { - let mut arena = Arena::new(10); - let mut tokens = selectors - .iter() - .map(|selector| Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(selector)), - module_id: 1.into(), - }) - .collect::>(); - - tokens.push(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }); - - let result = Parser::new(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 { - // 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 { - match base_selector { - "h" => assert_eq!(*selector, Selector::Heading(None)), - "h1" => assert_eq!(*selector, Selector::Heading(Some(1))), - "code" => assert_eq!(*selector, Selector::Code), - "text" => assert_eq!(*selector, Selector::Text), - _ => panic!("Unexpected base selector: {}", base_selector), - } - } else { - panic!("Expected Selector expression in first argument, got {:?}", args[0].expr); - } - - // Second argument should be the attribute string - 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); - } - } else { - panic!("Expected Call expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[rstest] - // Two selectors: base, then a single descendant hop. - #[case::two_levels(vec![".blockquote", ".code"], vec![Selector::Blockquote, Selector::Code])] - // Three selectors: two descendant hops. - #[case::three_levels( - vec![".blockquote", ".list", ".code"], - vec![Selector::Blockquote, Selector::List(None, None), Selector::Code] - )] - fn test_parse_selector_descendant_chain(#[case] selectors: Vec<&str>, #[case] expected_selectors: Vec) { - let mut arena = Arena::new(10); - let mut tokens = selectors - .iter() - .map(|selector| Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(selector)), - module_id: 1.into(), - }) - .collect::>(); - tokens.push(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }); - - let result = Parser::new(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 { - // Desugars to `base | .. | step1 | .. | step2 | ...`, i.e. a plain - // selector interleaved with a `Recursive` selector between each hop. - let expected: Vec = expected_selectors - .into_iter() - .enumerate() - .flat_map(|(i, sel)| { - if i == 0 { - vec![sel] - } else { - vec![Selector::Recursive, sel] - } - }) - .collect(); - assert_eq!(nodes.len(), expected.len()); - for (node, expected_sel) in nodes.iter().zip(expected.iter()) { - if let Expr::Selector(sel) = &node.expr { - assert_eq!(sel, expected_sel); - } else { - panic!("Expected Selector expression, got {:?}", node.expr); - } - } - } else { - panic!("Expected Block expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[test] - fn test_parse_selector_descendant_chain_trailing_attribute() { - // `.blockquote .code.lang` โ†’ attr(Block([.blockquote, .., .code]), "lang") - let mut arena = Arena::new(10); - let selectors = [".blockquote", ".code", ".lang"]; - let mut tokens = selectors - .iter() - .map(|selector| Token { - range: Range::default(), - kind: TokenKind::Selector(SmolStr::new(*selector)), - module_id: 1.into(), - }) - .collect::>(); - tokens.push(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }); - - let result = Parser::new(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 { - 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")); - } else { - panic!("Expected Call expression, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[rstest] - #[case::match_simple_literal( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("one".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("two".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("_"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("other".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Literal(Literal::Number(1.into())), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("one".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Literal(Literal::Number(2.into())), - guard: None, - body: Shared::new(Node { - token_id: 8.into(), - expr: Expr::Literal(Literal::String("two".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: Shared::new(Node { - token_id: 11.into(), - expr: Expr::Literal(Literal::String("other".to_owned())) - }) - } - ] - ) - }) - ]))] - #[case::match_type_pattern( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("value"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("string"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("is string".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("number"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("is number".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("value", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Type(Ident::new("string")), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("is string".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Type(Ident::new("number")), - guard: None, - body: Shared::new(Node { - token_id: 8.into(), - expr: Expr::Literal(Literal::String("is number".to_owned())) - }) - } - ] - ) - }) - ]))] - #[case::match_array_pattern( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("y"))), - token(TokenKind::RBracket), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("two elements".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Array(vec![ - Pattern::Ident(IdentWithToken::new("x")), - Pattern::Ident(IdentWithToken::new("y")) - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("two elements".to_owned())) - }) - } - ] - ) - }) - ]))] - #[case::match_array_rest_pattern( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("arr"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::LBracket), - token(TokenKind::Ident(SmolStr::new("first"))), - token(TokenKind::Comma), - token(TokenKind::DoubleDot), - token(TokenKind::Ident(SmolStr::new("rest"))), - token(TokenKind::RBracket), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("first"))), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::ArrayRest( - vec![Pattern::Ident(IdentWithToken::new("first"))], - IdentWithToken::new("rest") - ), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("first", Some(5.into()))) - }) - } - ] - ) - }) - ]))] - #[case::match_dict_pattern( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("obj"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::LBrace), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::Comma), - token(TokenKind::Ident(SmolStr::new("age"))), - token(TokenKind::RBrace), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("name"))), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Dict(vec![ - (IdentWithToken::new("name"), Pattern::Ident(IdentWithToken::new("name"))), - (IdentWithToken::new("age"), Pattern::Ident(IdentWithToken::new("age"))) - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("name", Some(5.into()))) - }) - } - ] - ) - }) - ]))] - #[case::match_with_guard( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("n"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Gt), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("positive".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::If), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::Lt), - token(TokenKind::NumberLiteral(0.into())), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("negative".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("_"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("zero".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("n", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Ident(IdentWithToken::new("x")), - guard: Some(Shared::new(Node { - token_id: 5.into(), - expr: Expr::BinaryOp( - BinaryOp::Gt, - Shared::new(Node { - token_id: 4.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(4.into()))) - }), - Shared::new(Node { - token_id: 6.into(), - expr: Expr::Literal(Literal::Number(0.into())) - }) - ) - })), - body: Shared::new(Node { - token_id: 8.into(), - 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: Expr::BinaryOp( - BinaryOp::Lt, - Shared::new(Node { - token_id: 10.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(10.into()))) - }), - Shared::new(Node { - token_id: 12.into(), - expr: Expr::Literal(Literal::Number(0.into())) - }) - ) - })), - body: Shared::new(Node { - token_id: 14.into(), - expr: Expr::Literal(Literal::String("negative".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: Shared::new(Node { - token_id: 17.into(), - expr: Expr::Literal(Literal::String("zero".to_owned())) - }) - } - ] - ) - }) - ]))] - #[case::match_do_end( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::RParen), - token(TokenKind::Do), - token(TokenKind::Pipe), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("one".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("two".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("_"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("other".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof) - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Literal(Literal::Number(2.into())) - }), - vec![ - MatchArm { - pattern: Pattern::Literal(Literal::Number(1.into())), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("one".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Literal(Literal::Number(2.into())), - guard: None, - body: Shared::new(Node { - token_id: 8.into(), - expr: Expr::Literal(Literal::String("two".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: Shared::new(Node { - token_id: 11.into(), - expr: Expr::Literal(Literal::String("other".to_owned())) - }) - } - ] - ) - }) - ]))] - // --- or-pattern parser tests --- - // Arena allocation order for match(x): COLON | pattern COLON body ... - // id 0: Match, id 1: Ident("x"), id 2: Colon (consume_colon_or_do) - // per arm: id N: Pipe, id N+1: Colon, id N+2: body literal - #[case::match_or_two_literal_numbers( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Or), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("matched".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("_"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("other".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(Literal::Number(1.into())), - Pattern::Literal(Literal::Number(2.into())), - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("matched".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: Shared::new(Node { - token_id: 8.into(), - expr: Expr::Literal(Literal::String("other".to_owned())) - }) - }, - ] - ) - }) - ]))] - #[case::match_or_string_literals( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::StringLiteral("a".to_owned())), - token(TokenKind::Or), - token(TokenKind::StringLiteral("b".to_owned())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("matched".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("_"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("other".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(Literal::String("a".to_owned())), - Pattern::Literal(Literal::String("b".to_owned())), - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("matched".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: Shared::new(Node { - token_id: 8.into(), - expr: Expr::Literal(Literal::String("other".to_owned())) - }) - }, - ] - ) - }) - ]))] - #[case::match_or_type_patterns( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("string"))), - token(TokenKind::Or), - token(TokenKind::Colon), - token(TokenKind::Ident(SmolStr::new("number"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("str or num".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Type(Ident::new("string")), - Pattern::Type(Ident::new("number")), - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("str or num".to_owned())) - }) - }, - ] - ) - }) - ]))] - #[case::match_or_three_alternatives( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::NumberLiteral(1.into())), - token(TokenKind::Or), - token(TokenKind::NumberLiteral(2.into())), - token(TokenKind::Or), - token(TokenKind::NumberLiteral(3.into())), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("matched".to_owned())), - token(TokenKind::Pipe), - token(TokenKind::Ident(SmolStr::new("_"))), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("other".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(Literal::Number(1.into())), - Pattern::Literal(Literal::Number(2.into())), - Pattern::Literal(Literal::Number(3.into())), - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - expr: Expr::Literal(Literal::String("matched".to_owned())) - }) - }, - MatchArm { - pattern: Pattern::Wildcard, - guard: None, - body: Shared::new(Node { - token_id: 8.into(), - expr: Expr::Literal(Literal::String("other".to_owned())) - }) - }, - ] - ) - }) - ]))] - #[case::match_or_bool_literals( - vec![ - token(TokenKind::Match), - token(TokenKind::LParen), - token(TokenKind::Ident(SmolStr::new("x"))), - token(TokenKind::RParen), - token(TokenKind::Colon), - token(TokenKind::Pipe), - token(TokenKind::BoolLiteral(true)), - token(TokenKind::Or), - token(TokenKind::BoolLiteral(false)), - token(TokenKind::Colon), - token(TokenKind::StringLiteral("bool".to_owned())), - token(TokenKind::End), - token(TokenKind::Eof), - ], - Ok(vec![ - Shared::new(Node { - token_id: 0.into(), - expr: Expr::Match( - Shared::new(Node { - token_id: 1.into(), - expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) - }), - vec![ - MatchArm { - pattern: Pattern::Or(vec![ - Pattern::Literal(Literal::Bool(true)), - Pattern::Literal(Literal::Bool(false)), - ]), - guard: None, - body: Shared::new(Node { - token_id: 5.into(), - 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 result = Parser::new(input.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); - assert_eq!(result, expected); - } - - #[rstest] - #[case::expr_literal( - vec![ - lexer::token::StringSegment::Text("Value: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("42".into(), Range::default()), - ], - 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(_)))) - } - )] - #[case::expr_string( - vec![ - lexer::token::StringSegment::Text("Result: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("\"hello\"".into(), Range::default()), - ], - 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")) - } - )] - #[case::expr_call( - vec![ - lexer::token::StringSegment::Text("Result: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("add(1, 2)".into(), Range::default()), - ], - 2, - |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 { - ident.name == "add".into() && args.len() == 2 - } else { - false - } - } else { - false - } - } - )] - #[case::expr_self( - vec![ - lexer::token::StringSegment::Text("Value: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("self".into(), Range::default()), - ], - 2, - |segments: &[StringSegment]| { - matches!(&segments[0], StringSegment::Text(s) if s == "Value: ") && - matches!(&segments[1], StringSegment::Self_) - } - )] - #[case::expr_env( - vec![ - lexer::token::StringSegment::Text("Env: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("$MQ_TEST_INTERPOLATION".into(), Range::default()), - ], - 2, - |segments: &[StringSegment]| { - unsafe { std::env::set_var("MQ_TEST_INTERPOLATION", "test_value") }; - matches!(&segments[0], StringSegment::Text(s) if s == "Env: ") && - matches!(&segments[1], StringSegment::Env(var) if var == "MQ_TEST_INTERPOLATION") - } - )] - #[case::multiple_exprs( - vec![ - lexer::token::StringSegment::Text("A: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("1".into(), Range::default()), - lexer::token::StringSegment::Text(", B: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("2".into(), Range::default()), - ], - 4, - |segments: &[StringSegment]| { - matches!(&segments[0], StringSegment::Text(s) if s == "A: ") && - matches!(&segments[1], StringSegment::Expr(_)) && - matches!(&segments[2], StringSegment::Text(s) if s == ", B: ") && - matches!(&segments[3], StringSegment::Expr(_)) - } - )] - #[case::mixed_segments( - vec![ - lexer::token::StringSegment::Text("Literal, ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("self".into(), Range::default()), - lexer::token::StringSegment::Text(", ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("$MQ_MIXED_TEST".into(), Range::default()), - lexer::token::StringSegment::Text(", ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("42".into(), Range::default()), - ], - 6, - |segments: &[StringSegment]| { - unsafe { std::env::set_var("MQ_MIXED_TEST", "env_value") }; - matches!(&segments[0], StringSegment::Text(s) if s == "Literal, ") && - matches!(&segments[1], StringSegment::Self_) && - matches!(&segments[2], StringSegment::Text(s) if s == ", ") && - matches!(&segments[3], StringSegment::Env(var) if var == "MQ_MIXED_TEST") && - matches!(&segments[4], StringSegment::Text(s) if s == ", ") && - matches!(&segments[5], StringSegment::Expr(_)) - } - )] - #[case::expr_bool( - vec![ - lexer::token::StringSegment::Text("Bool: ".to_string(), Range::default()), - lexer::token::StringSegment::Expr("true".into(), Range::default()), - ], - 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)))) - } - )] - fn test_parse_interpolated_string_with_expr( - #[case] input_segments: Vec, - #[case] expected_len: usize, - #[case] validator: fn(&[StringSegment]) -> bool, - ) { - let mut arena = Arena::new(10); - let tokens = [ - Token { - range: Range::default(), - kind: TokenKind::InterpolatedString(input_segments), - module_id: 1.into(), - }, - Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: 1.into(), - }, - ]; - - let result = Parser::new(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 { - assert_eq!(segments.len(), expected_len); - assert!(validator(segments), "Validator failed for segments"); - } else { - panic!("Expected InterpolatedString, got {:?}", program[0].expr); - } - } - Err(err) => panic!("Parse error: {:?}", err), - } - } - - #[test] - fn test_interpolated_string_inner_syntax_error_is_propagated() { - let result = parse_source("let a = 1\n| s\"a ${ 1 + } b\""); - match result { - Err(SyntaxError::UnexpectedEOFAfterToken(token)) => { - assert_eq!(token.kind, TokenKind::Plus); - assert_eq!(token.range.start, Position { line: 2, column: 12 }); - } - other => panic!("expected the inner UnexpectedEOFAfterToken, got {:?}", other), - } - } - - /// For each argument of the top-level call, the statement count of its body if it is a lambda. - fn lambda_arg_body_lens(source: &str) -> Vec> { - let program = parse_source(source).expect("source should parse"); - let [node] = program.as_slice() else { - panic!("expected a single top-level expression"); - }; - let Expr::Call(_, args) = &node.expr else { - panic!("expected a call, got {:?}", node.expr); - }; - args.iter() - .map(|arg| match &arg.expr { - Expr::Fn(_, body) => Some(body.len()), - _ => None, - }) - .collect() - } - - #[rstest] - #[case::only_arg("f(fn(x): x + 1)", vec![Some(1)])] - #[case::arrow("f(->(x): x + 1)", vec![Some(1)])] - #[case::last_arg("f(a, fn(x): x + 1)", vec![None, Some(1)])] - #[case::multiple_params("f(a, 0, fn(acc, x): acc + x)", vec![None, None, Some(1)])] - #[case::ends_at_comma("f(fn(x): x, 1)", vec![Some(1), None])] - #[case::lambda_between_args("f(a, fn(x): x, b)", vec![None, Some(1), None])] - #[case::two_lambdas("f(fn(x): x + 1, fn(x): x * 2)", vec![Some(1), Some(1)])] - #[case::pipe_stays_in_body("f(fn(x): x | g(.h))", vec![Some(2)])] - #[case::pipe_before_comma("f(fn(x): x | g(.h), 1)", vec![Some(2), None])] - #[case::call_with_commas_in_body("f(fn(x): g(x, 1, 2), 3)", vec![Some(1), None])] - #[case::array_in_body("f(fn(x): [x, 1], 2)", vec![Some(1), None])] - #[case::dict_in_body("f(fn(x): {\"a\": x, \"b\": 1}, 2)", vec![Some(1), None])] - #[case::paren_in_body("f(fn(x): (x + 1) * 2, 3)", vec![Some(1), None])] - #[case::if_else_in_body("f(fn(x): if (x): 1 else: 2, 3)", vec![Some(1), None])] - #[case::elif_in_body("f(fn(x): if (x): 1 elif (y): 2 else: 3)", vec![Some(1)])] - #[case::do_end_in_body("f(fn(x): do 1 | 2 end, 3)", vec![Some(1), None])] - #[case::foreach_in_body("f(fn(x): foreach (i, x): i end, 3)", vec![Some(1), None])] - #[case::match_in_body("f(fn(x): match (x): | 1: 2 | _: 3 end, 4)", vec![Some(1), None])] - #[case::nested_call_lambda("f(fn(x): g(x, fn(y): y + 1))", vec![Some(1)])] - #[case::nested_call_lambda_then_comma("f(fn(x): g(x, fn(y): y + 1), 2)", vec![Some(1), None])] - #[case::let_in_body("f(fn(x): let y = x + 1 | y, 2)", vec![Some(2), None])] - #[case::lambda_in_param_default("f(fn(x = g(fn(y): y)): x, 1)", vec![Some(1), None])] - #[case::semicolon_form("f(fn(x): x + 1;)", vec![Some(1)])] - #[case::semicolon_form_then_arg("f(fn(x): x; , 1)", vec![Some(1), None])] - #[case::end_form("f(fn(x): do x end; , 1)", vec![Some(1), None])] - #[case::inner_end_then_semicolon("f(fn(x): foreach (i, x): i end;)", vec![Some(1)])] - fn test_lambda_in_call_args_may_omit_terminator(#[case] source: &str, #[case] expected: Vec>) { - assert_eq!(lambda_arg_body_lens(source), expected); - } - - // Body lengths exclude the leading `fn:` argument statement. - #[rstest] - #[case::only_arg("f(fn: self + 1)", vec![Some(1)])] - #[case::last_arg("f(a, fn: self + 1)", vec![None, Some(1)])] - #[case::ends_at_comma("f(fn: self, 1)", vec![Some(1), None])] - #[case::pipe_stays_in_body("f(fn: g(1) | h(2))", vec![Some(2)])] - #[case::pipe_before_comma("f(fn: g | h, 1)", vec![Some(2), None])] - #[case::call_with_commas_in_body("f(fn: g(1, 2), 3)", vec![Some(1), None])] - #[case::nested("f(fn: g(fn: self + 1))", vec![Some(1)])] - #[case::nested_then_comma("f(fn: g(fn: self + 1), 2)", vec![Some(1), None])] - #[case::if_else_in_body("f(fn: if (self): 1 else: 2, 3)", vec![Some(1), None])] - #[case::semicolon_form("f(fn: self + 1;)", vec![Some(1)])] - #[case::end_form("f(fn: do self end;, 1)", vec![Some(1), None])] - fn test_lambda_without_params(#[case] source: &str, #[case] expected: Vec>) { - let expected: Vec<_> = expected.into_iter().map(|len| len.map(|len| len + 1)).collect(); - assert_eq!(lambda_arg_body_lens(source), expected); - } - - #[test] - fn test_lambda_without_params_takes_one_argument() { - let program = parse_source("f(fn: self, fn(): 1)").expect("source should parse"); - let Expr::Call(_, args) = &program[0].expr else { - panic!("expected a call"); - }; - let Expr::Fn(params, body) = &args[0].expr else { - panic!("expected a function"); - }; - let [param] = params.as_slice() else { - panic!("expected one parameter"); - }; - assert!(matches!(&body[0].expr, Expr::Ident(ident) if ident.name == param.ident.name)); - assert!(matches!(&args[1].expr, Expr::Fn(params, _) if params.is_empty())); - } - - #[test] - fn test_lambda_without_params_outside_call_args_with_terminator() { - let program = parse_source("let g = fn: self + 1;").expect("source should parse"); - assert!(matches!(&program[0].expr, Expr::Let(..))); - } - - #[rstest] - #[case::let_value_without_terminator("let g = fn: self + 1)")] - #[case::empty_body("f(fn: )")] - #[case::empty_body_then_comma("f(fn:, 1)")] - #[case::unclosed_call("f(fn: self")] - #[case::arrow_has_no_paramless_form("f(->: self)")] - #[case::missing_colon("f(fn self)")] - fn test_lambda_without_params_reports_malformed_input(#[case] source: &str) { - assert!(parse_source(source).is_err(), "{source} should not parse"); - } - - #[rstest] - #[case::let_value("let g = fn(x): x + 1)")] - #[case::empty_body("f(fn(x): )")] - #[case::empty_body_then_comma("f(fn(x):, 1)")] - #[case::dangling_operator("f(fn(x): x +)")] - #[case::unclosed_call("f(fn(x): x + 1")] - #[case::stray_close_paren("f(fn(x): x + 1))")] - fn test_lambda_reports_malformed_input(#[case] source: &str) { - assert!(parse_source(source).is_err(), "{source} should not parse"); - } - - #[rstest] - #[case::paren_group("(fn(x): x + 1)(2)")] - #[case::array_element("[fn(x): x, 1]")] - #[case::implicit_array_element("[fn: self, 1]")] - #[case::dict_value("{\"a\": fn(x): x, \"b\": 1}")] - #[case::lambda_body_tail("f(fn(x): fn(y): x + y)")] - #[case::after_pipe("f(1 | fn(x): x + 1, 2)")] - fn test_lambda_ends_at_the_closer_of_its_enclosing_construct(#[case] source: &str) { - parse_source(source).expect("source should parse"); - } - - #[test] - fn test_lambda_array_element_ends_before_comma() { - let program = parse_source("[fn(x): x + 1, 2]").expect("source should parse"); - let Expr::Array(elements) = &program[0].expr else { - panic!("expected an array"); - }; - assert_eq!(elements.len(), 2); - assert!(matches!(&elements[0].expr, Expr::Fn(..))); - } - - #[rstest] - #[case::empty_body("f(fn(x): )", TokenKind::RParen)] - #[case::empty_body_then_comma("f(fn(x):, 1)", TokenKind::Comma)] - fn test_lambda_in_call_args_with_empty_body_reports_the_delimiter(#[case] source: &str, #[case] kind: TokenKind) { - match parse_source(source) { - Err(SyntaxError::UnexpectedToken(token)) => assert_eq!(token.kind, kind), - other => panic!("expected UnexpectedToken, got {other:?}"), - } - } - - #[test] - fn test_unclosed_call_after_unterminated_lambda_reports_missing_paren() { - assert!(matches!( - parse_source("f(fn(x): x + 1"), - Err(SyntaxError::ExpectedClosingParen(..)) - )); - } - - const STACK_KIB: usize = 2048; - - fn parse_source(source: &str) -> Result { - let tokens = Lexer::new(lexer::Options::default()) - .tokenize(source, Module::TOP_LEVEL_MODULE_ID) - .expect("source should tokenize"); - let mut arena = Arena::new(16); - Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse() - } - - /// Runs `f` on a 2 MiB stack (the default for spawned and tokio worker threads) in release - /// builds. Debug frames are much larger, so debug builds get more room. - fn on_small_stack(f: impl FnOnce() -> T + Send + 'static) -> T { - let stack_size = if cfg!(debug_assertions) { 64 * 1024 } else { STACK_KIB } * 1024; - std::thread::Builder::new() - .stack_size(stack_size) - .spawn(f) - .unwrap() - .join() - .expect("parser must not overflow the stack") - } - - #[rstest] - #[case::paren("(", "1", ")")] - #[case::array("[", "1", "]")] - #[case::dict("{\"a\": ", "1", "}")] - #[case::not("!", "true", "")] - #[case::block("do ", "1", " end")] - #[case::if_else("if (true): ", "1", " else: 2")] - #[case::fn_body("fn(): ", "1", "; ")] - #[case::call("f(", "1", ")")] - fn test_nesting_within_limit_is_accepted(#[case] open: &str, #[case] core: &str, #[case] close: &str) { - let source = format!( - "{}{}{}", - open.repeat(MAX_PARSE_DEPTH - 1), - core, - close.repeat(MAX_PARSE_DEPTH - 1) - ); - let result = on_small_stack(move || parse_source(&source).map(|_| ())); - assert!( - !matches!(result, Err(SyntaxError::TooDeeplyNested(..))), - "nesting of {} should be accepted, got {:?}", - MAX_PARSE_DEPTH - 1, - result - ); - } - - #[rstest] - #[case::paren("(", "1", ")")] - #[case::array("[", "1", "]")] - #[case::dict("{\"a\": ", "1", "}")] - #[case::not("!", "true", "")] - #[case::block("do ", "1", " end")] - #[case::if_else("if (true): ", "1", " else: 2")] - #[case::fn_body("fn(): ", "1", "; ")] - #[case::call("f(", "1", ")")] - fn test_nesting_beyond_limit_returns_error(#[case] open: &str, #[case] core: &str, #[case] close: &str) { - let n = MAX_PARSE_DEPTH * 2; - let source = format!("{}{}{}", open.repeat(n), core, close.repeat(n)); - let result = on_small_stack(move || parse_source(&source).map(|_| ())); - assert!( - matches!(result, Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH))), - "expected TooDeeplyNested, got {:?}", - result - ); - } - - #[rstest] - #[case::let_array("let ", "[", "x", "]", " = 1")] - #[case::let_dict("let ", "{a: ", "x", "}", " = 1")] - #[case::match_array("match (1): | ", "[", "x", "]", ": 1 end")] - fn test_pattern_nesting_beyond_limit_returns_error( - #[case] prefix: &str, - #[case] open: &str, - #[case] core: &str, - #[case] close: &str, - #[case] suffix: &str, - ) { - let n = MAX_PARSE_DEPTH * 2; - let source = format!("{prefix}{}{core}{}{suffix}", open.repeat(n), close.repeat(n)); - let result = on_small_stack(move || parse_source(&source).map(|_| ())); - assert!( - matches!(result, Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH))), - "expected TooDeeplyNested, got {:?}", - result - ); - } - - #[test] - fn test_long_index_chain_returns_error() { - let source = format!("a{}", "[0]".repeat(MAX_PARSE_DEPTH * 2)); - let result = on_small_stack(move || parse_source(&source).map(|_| ())); - assert!( - matches!(result, Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH))), - "expected TooDeeplyNested, got {:?}", - result - ); - } - - #[test] - fn test_interpolated_expr_nesting_reports_nesting_error() { - let n = MAX_PARSE_DEPTH * 2; - let source = format!("s\"${{{}1{}}}\"", "(".repeat(n), ")".repeat(n)); - let prefix_len = "s\"".len(); - let result = on_small_stack(move || parse_source(&source).map(|_| ())); - match result { - Err(SyntaxError::TooDeeplyNested(token, _)) => { - assert!( - token.range.start.column >= prefix_len, - "range should point into the original source, got {:?}", - token.range - ); - } - other => panic!("expected TooDeeplyNested, got {:?}", other), - } - } - - #[test] - fn test_nesting_limit_boundary() { - let at_limit = format!( - "{}1{}", - "(".repeat(MAX_PARSE_DEPTH - 1), - ")".repeat(MAX_PARSE_DEPTH - 1) - ); - let over_limit = format!("{}1{}", "(".repeat(MAX_PARSE_DEPTH), ")".repeat(MAX_PARSE_DEPTH)); - let (at_limit_ok, over_limit_err) = on_small_stack(move || { - ( - parse_source(&at_limit).is_ok(), - matches!( - parse_source(&over_limit), - Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH)) - ), - ) - }); - assert!(at_limit_ok); - assert!(over_limit_err); - } - - #[test] - fn test_depth_is_restored_after_each_expression() { - let nested = format!( - "{}1{}", - "(".repeat(MAX_PARSE_DEPTH - 1), - ")".repeat(MAX_PARSE_DEPTH - 1) - ); - let source = format!("{nested} | {nested} | {nested}"); - assert!(on_small_stack(move || parse_source(&source).is_ok())); - } - - /// Renders `node` fully parenthesized with the source symbol of each operator. - fn shape(node: &Node, arena: &Arena) -> String { - fn compound_rhs(node: &Node) -> &Node { - match &node.expr { - Expr::BinaryOp(_, _, rhs) => rhs, - Expr::Call(_, args) if args.len() == 2 => &args[1], - Expr::Call(_, args) if args.len() == 1 => compound_rhs(&args[0]), - _ => node, - } - } - - let operator = arena[node.token_id].to_string(); - let fold = |operands: &[Shared], symbol: &str| { - operands - .iter() - .map(|operand| shape(operand, arena)) - .reduce(|lhs, rhs| format!("({lhs} {symbol} {rhs})")) - .unwrap() - }; - - match &node.expr { - Expr::Ident(ident) => ident.name.to_string(), - Expr::Literal(literal) => literal.to_string(), - Expr::Paren(inner) => format!("<{}>", shape(inner, arena)), - Expr::UnaryOp(op, operand) => format!("({op} {})", shape(operand, arena)), - Expr::BinaryOp(_, lhs, rhs) => format!("({} {operator} {})", shape(lhs, arena), shape(rhs, arena)), - Expr::Call(_, args) if args.len() == 2 && arena[node.token_id].kind.binary_op_precedence().is_some() => { - format!("({} {operator} {})", shape(&args[0], arena), shape(&args[1], arena)) - } - Expr::And(operands) => fold(operands, "&&"), - Expr::Or(operands) => fold(operands, "||"), - Expr::Assign(ident, rhs) if operator == "=" => format!("({} = {})", ident.name, shape(rhs, arena)), - Expr::Assign(ident, rhs) => format!("({} {operator} {})", ident.name, shape(compound_rhs(rhs), arena)), - Expr::As(ident, expr) => format!("({} as {})", shape(expr, arena), ident.name), - other => panic!("shape does not support {other:?}"), - } - } - - fn parse_with_arena(code: &str) -> (Program, Arena) { - let tokens = Lexer::new(lexer::Options::default()) - .tokenize(code, Module::TOP_LEVEL_MODULE_ID) - .expect("code should tokenize"); - let mut arena = Arena::new(16); - let program = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID) - .parse() - .expect("code should parse"); - (program, arena) - } - - #[test] - fn test_ident_token_resolves_to_its_lexer_token() { - let (program, arena) = parse_with_arena("def foo(a): a;"); - let Expr::Def(name, params, body) = &program[0].expr else { - panic!("expected def"); - }; - - let name_token = &arena[name.token.expect("def name keeps its token")]; - assert_eq!(name_token.kind, TokenKind::Ident("foo".into())); - assert_eq!(name_token.range.start.column, 5); - - let param_token = &arena[params[0].ident.token.expect("param keeps its token")]; - assert_eq!(param_token.kind, TokenKind::Ident("a".into())); - assert_eq!(param_token.range.start.column, 9); - - let Expr::Ident(ident) = &body[0].expr else { - panic!("expected ident"); - }; - assert_eq!(ident.token, Some(body[0].token_id), "an ident shares its node's entry"); - } - - #[test] - fn test_nodes_built_from_one_lexer_token_have_distinct_ids() { - let (program, _) = parse_with_arena("arr[0](1)"); - let Expr::CallDynamic(callee, _) = &program[0].expr else { - panic!("expected dynamic call"); - }; - assert_ne!(program[0].token_id, callee.token_id); - } - - #[rstest] - #[case::self_("self.url")] - #[case::ident("v.url")] - #[case::call("f().url")] - fn test_attr_call_and_its_literal_have_distinct_ids(#[case] code: &str) { - let (program, arena) = parse_with_arena(code); - let Expr::Call(attr, args) = &program[0].expr else { - panic!("expected attr call"); - }; - assert_eq!(attr.name, Ident::new(constants::builtins::ATTR)); - assert_ne!(program[0].token_id, args[1].token_id); - assert_eq!( - attr.token, - Some(args[1].token_id), - "attr resolves to the selector token" - ); - assert_eq!(arena[args[1].token_id].kind, TokenKind::Selector(".url".into())); - } - - fn parse_shape(code: &str) -> Result { - let tokens = Lexer::new(lexer::Options::default()) - .tokenize(code, Module::TOP_LEVEL_MODULE_ID) - .expect("code should tokenize"); - let mut arena = Arena::new(16); - let program = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse()?; - assert_eq!(program.len(), 1, "expected one expression for {code:?}"); - Ok(shape(&program[0], &arena)) - } - - #[rstest] - #[case::add("a + b", "(a + b)")] - #[case::sub("a - b", "(a - b)")] - #[case::mul("a * b", "(a * b)")] - #[case::div("a / b", "(a / b)")] - #[case::modulo("a % b", "(a % b)")] - #[case::eq("a == b", "(a == b)")] - #[case::ne("a != b", "(a != b)")] - #[case::lt("a < b", "(a < b)")] - #[case::lte("a <= b", "(a <= b)")] - #[case::gt("a > b", "(a > b)")] - #[case::gte("a >= b", "(a >= b)")] - #[case::and("a && b", "(a && b)")] - #[case::or("a || b", "(a || b)")] - #[case::left_shift("a << b", "(a << b)")] - #[case::right_shift("a >> b", "(a >> b)")] - #[case::convert("a @ b", "(a @ b)")] - #[case::range("a .. b", "(a .. b)")] - #[case::coalesce("a ?? b", "(a ?? b)")] - #[case::regex_match("a =~ b", "(a =~ b)")] - #[case::not_regex_match("a !~ b", "(a !~ b)")] - fn test_every_binary_operator_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), Ok(expected.to_string())); - } - - #[rstest] - #[case::and_over_or("a || b && c", "(a || (b && c))")] - #[case::or_after_and("a && b || c", "((a && b) || c)")] - #[case::comparison_over_and("a && b == c", "(a && (b == c))")] - #[case::and_after_comparison("a == b && c", "((a == b) && c)")] - #[case::additive_over_comparison("a == b + c", "(a == (b + c))")] - #[case::comparison_after_additive("a + b == c", "((a + b) == c)")] - #[case::multiplicative_over_additive("a + b * c", "(a + (b * c))")] - #[case::additive_after_multiplicative("a * b + c", "((a * b) + c)")] - #[case::range_over_multiplicative("a * b .. c", "(a * (b .. c))")] - #[case::multiplicative_after_range("a .. b * c", "((a .. b) * c)")] - #[case::coalesce_over_multiplicative("a * b ?? c", "(a * (b ?? c))")] - #[case::multiplicative_after_coalesce("a ?? b * c", "((a ?? b) * c)")] - #[case::convert_with_multiplicative("a + b @ c", "(a + (b @ c))")] - #[case::convert_is_left_associative_with_mul("a * b @ c", "((a * b) @ c)")] - #[case::shift_with_additive("a << b + c", "((a << b) + c)")] - #[case::additive_with_shift("a + b >> c", "((a + b) >> c)")] - #[case::regex_match_with_comparison("a =~ b == c", "((a =~ b) == c)")] - #[case::regex_over_and("a !~ b && c", "((a !~ b) && c)")] - #[case::three_levels("a || b && c == d + e * f .. g", "(a || (b && (c == (d + (e * (f .. g))))))")] - #[case::three_levels_reversed("a .. b * c + d == e && f || g", "((((((a .. b) * c) + d) == e) && f) || g)")] - fn test_binary_op_precedence_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), Ok(expected.to_string())); - } - - #[rstest] - #[case::or("a || b || c", "((a || b) || c)")] - #[case::and("a && b && c", "((a && b) && c)")] - #[case::comparison("a == b == c", "((a == b) == c)")] - #[case::mixed_comparison("a < b != c", "((a < b) != c)")] - #[case::sub("a - b - c", "((a - b) - c)")] - #[case::add_then_sub("a + b - c", "((a + b) - c)")] - #[case::shifts("a << b >> c", "((a << b) >> c)")] - #[case::div("a / b / c", "((a / b) / c)")] - #[case::mul_div_mod("a * b / c % d", "(((a * b) / c) % d)")] - #[case::range("a .. b .. c", "((a .. b) .. c)")] - #[case::coalesce("a ?? b ?? c", "((a ?? b) ?? c)")] - #[case::range_then_coalesce("a .. b ?? c", "((a .. b) ?? c)")] - #[case::coalesce_then_range("a ?? b .. c", "((a ?? b) .. c)")] - #[case::convert("a @ b @ c", "((a @ b) @ c)")] - fn test_binary_op_associativity_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), Ok(expected.to_string())); - } - - #[rstest] - #[case::parens_override_precedence("(a + b) * c", "(<(a + b)> * c)")] - #[case::parens_on_the_right("a * (b + c)", "(a * <(b + c)>)")] - #[case::parens_override_associativity("a - (b - c)", "(a - <(b - c)>)")] - #[case::neg_binds_tighter_than_mul("-a * b", "((- a) * b)")] - #[case::not_binds_tighter_than_eq("!a == b", "((! a) == b)")] - #[case::not_binds_tighter_than_and("!a && !b", "((! a) && (! b))")] - #[case::unary_on_the_right("a - -b", "(a - (- b))")] - #[case::not_on_the_right("a * !b", "(a * (! b))")] - #[case::as_binds_the_whole_expression("a + b * c as x", "((a + (b * c)) as x)")] - #[case::as_after_logic("a && b as x", "((a && b) as x)")] - fn test_operand_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), Ok(expected.to_string())); - } - - #[rstest] - #[case::assign("x = a", "(x = a)")] - #[case::assign_is_lowest("x = a || b && c == d + e * f", "(x = (a || (b && (c == (d + (e * f))))))")] - #[case::assign_takes_the_whole_expression("x = a + b * c", "(x = (a + (b * c)))")] - #[case::plus_assign("x += a * b", "(x += (a * b))")] - #[case::minus_assign("x -= a * b", "(x -= (a * b))")] - #[case::mul_assign("x *= a + b", "(x *= (a + b))")] - #[case::div_assign("x /= a + b", "(x /= (a + b))")] - #[case::mod_assign("x %= a + b", "(x %= (a + b))")] - #[case::floor_div_assign("x //= a + b", "(x //= (a + b))")] - fn test_assignment_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), Ok(expected.to_string())); - } - - #[rstest] - #[case::chained_assignment("a = b = c")] - #[case::assign_to_expression("a + b = c")] - #[case::assign_to_logic("a || b = c")] - #[case::pipe_assign_outside_a_binding("a |= b")] - #[case::missing_right_operand("a +")] - #[case::missing_right_operand_after_assign("a =")] - #[case::missing_right_operand_after_logic("a &&")] - #[case::operator_only("+")] - #[case::as_without_name("a as")] - #[case::as_with_non_identifier("a as 1")] - fn test_binary_op_errors(#[case] code: &str) { - assert!(parse_shape(code).is_err(), "expected an error for {code:?}"); - } - - #[test] - fn test_code_generation_precedence_matches_the_parser() { - let kinds = [ - TokenKind::Plus, - TokenKind::Minus, - TokenKind::Asterisk, - TokenKind::Slash, - TokenKind::Percent, - TokenKind::EqEq, - TokenKind::NeEq, - TokenKind::Lt, - TokenKind::Lte, - TokenKind::Gt, - TokenKind::Gte, - ]; - for x in &kinds { - for y in &kinds { - let (op_x, op_y) = (Parser::binary_op_kind(x).unwrap(), Parser::binary_op_kind(y).unwrap()); - assert_eq!( - crate::ast::code::binary_op_precedence(op_x).cmp(&crate::ast::code::binary_op_precedence(op_y)), - x.binary_op_precedence().cmp(&y.binary_op_precedence()), - "{x} vs {y}" - ); - } - } - } - - #[rstest] - #[case::after_literal("1 |=")] - #[case::after_literal_with_operand("1 |= b")] - #[case::after_paren("(a) |= b")] - #[case::after_string("\"s\" |= b")] - fn test_pipe_assign_is_not_an_infix_operator(#[case] code: &str) { - assert!( - matches!(parse_shape(code), Err(SyntaxError::UnexpectedToken(token)) if token.kind == TokenKind::PipeEqual), - "expected `|=` to be rejected as an unexpected token for {code:?}" - ); - } -} +mod tests; diff --git a/crates/mq-lang/src/ast/parser/tests.rs b/crates/mq-lang/src/ast/parser/tests.rs new file mode 100644 index 000000000..1330aa6ea --- /dev/null +++ b/crates/mq-lang/src/ast/parser/tests.rs @@ -0,0 +1,7411 @@ +use crate::ast::node::StringSegment; +use crate::{Module, ast::node::MatchArm, range::Range, selector}; + +use super::*; +use rstest::rstest; +use smallvec::smallvec; + +fn token(token_kind: TokenKind) -> Token { + Token { + range: Range::default(), + kind: token_kind, + module_id: 1.into(), + } +} + +#[rstest] +#[case::ident1( + vec![ + token(TokenKind::Ident(SmolStr::new("and"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("contains"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("test".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("startswith"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("test2".to_owned())), + token(TokenKind::RParen), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("and", Some(4.into())), + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("contains", Some(1.into())), + smallvec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("test".to_owned())) + })], + ) + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("startswith", Some(3.into())), + smallvec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("test2".to_owned())) + })], + ) + }) + ], + ) + }) + ]))] +#[case::ident2( + vec![ + token(TokenKind::Ident(SmolStr::new("and"))), + token(TokenKind::LParen), + token(TokenKind::Selector(SmolStr::new(".h1"))), + token(TokenKind::Comma), + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 8.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("and", Some(6.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Selector(Selector::Heading(Some(1))), + }), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Selector(Selector::Table(Some(2), None)), + }), + ], + ) + }) + ]))] +#[case::ident3( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("filter"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("arg1"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("arg2"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("contains"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg1".to_owned())), + token(TokenKind::Comma), + token(TokenKind::StringLiteral("arg2".to_owned())), + token(TokenKind::RParen), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("filter", Some(7.into())), + vec![ + Param::new(IdentWithToken::new_with_token("arg1", Some(1.into()))), + Param::new(IdentWithToken::new_with_token("arg2", Some(2.into()))), + ], + vec![Shared::new(Node { + token_id: 6.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("contains", Some(6.into())), + smallvec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::String("arg1".to_owned())), + }), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("arg2".to_owned())), + }), + ], + ), + })], + ), + }), + ]))] +#[case::ident4( + vec![ + token(TokenKind::Ident(SmolStr::new("and"))), + token(TokenKind::LParen), + token(TokenKind::None), + token(TokenKind::Comma), + token(TokenKind::Self_), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("and", Some(2.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::None), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Self_, + }), + ], + ) + }) + ]))] +#[case::ident5( + vec![ + token(TokenKind::Ident(SmolStr::new("and"))), + token(TokenKind::LParen), + token(TokenKind::None), + token(TokenKind::Comma), + token(TokenKind::Self_), + token(TokenKind::RParen), + token(TokenKind::Ident(SmolStr::new("and"))), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident(SmolStr::new("and"))))))] +#[case::ident5( + vec![ + token(TokenKind::Ident(SmolStr::new("and"))), + token(TokenKind::LParen), + token(TokenKind::None), + token(TokenKind::Comma), + token(TokenKind::Self_), + token(TokenKind::RParen), + token(TokenKind::Def), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Def))))] +#[case::ident6( + vec![ + token(TokenKind::Ident(SmolStr::new("and"))), + token(TokenKind::Def), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident(SmolStr::new("and"))))))] +#[case::ident_attribute_access( + vec![ + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Selector(SmolStr::new(".lang"))), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("lang".to_owned())), + }), + ], + ), + }) + ]))] +#[case::error( + vec![ + token(TokenKind::Ident(SmolStr::new("contains"))), + token(TokenKind::LParen), + token(TokenKind::Selector(SmolStr::new("inline_code"))), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnknownSelector(selector::UnknownSelector::new(token(TokenKind::Selector(SmolStr::new("inline_code")))))))] +#[case::def1( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("name", Some(3.into())), + Vec::new(), + vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + })], + ), + }), + ]))] +#[case::def_with_end( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::End) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("name", Some(3.into())), + Vec::new(), + vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + })], + ), + }), + ]))] +#[case::def2( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::Comma), + token(TokenKind::RParen), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::Comma, module_id: 1.into()})))] +#[case::def3( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::Comma), + token(TokenKind::RParen), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] +#[case::def4( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] +#[case::def5( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] +#[case::def6( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::SemiColon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] +#[case::def7( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::StringLiteral("value".to_string()), module_id: 1.into()})))] +#[case::def7( + vec![ + token(TokenKind::Def), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::LParen, module_id: 1.into()})))] +#[case::def_without_colon1( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("name", Some(2.into())), + Vec::new(), + vec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + })], + ), + }), + ]))] +#[case::def_without_colon2( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::End) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("name", Some(2.into())), + Vec::new(), + vec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + })], + ), + }), + ]))] +#[case::def_without_colon_with_args( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("name", Some(3.into())), + vec![ + Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), + ], + vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident( + IdentWithToken::new_with_token("x", Some(2.into())), + ), + })], + ), + }), + ]))] +#[case::unmatched_end_at_root( + // `end` at root level followed by non-EOF: def foo: if (.): . end end | foo + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::Colon), + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::End), // closes def body + token(TokenKind::End), // unmatched โ€“ no open block + token(TokenKind::Pipe), // something after (like `| foo`) + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnmatchedEnd(token(TokenKind::End))))] +#[case::unmatched_end_standalone_if( + // `end` after a standalone single-line if (no open block): if (.): . end end + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::End), // unmatched + token(TokenKind::End), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnmatchedEnd(token(TokenKind::End))))] +#[case::unmatched_end_at_start( + // `end` as the very first token at root level + vec![ + token(TokenKind::End), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnmatchedEnd(token(TokenKind::End))))] +#[case::let_1( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Let( + Pattern::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + ), + }) + ]))] +#[case::let_2( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Equal), + token(TokenKind::StringLiteral("hello".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Let( + Pattern::Ident(IdentWithToken::new_with_token("y", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("hello".to_owned())), + }), + ), + }) + ]))] +#[case::let_3( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("flag"))), + token(TokenKind::Equal), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Let( + Pattern::Ident(IdentWithToken::new_with_token("flag", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + ), + }) + ]))] +#[case::let_4( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("z"))), + token(TokenKind::Equal), + token(TokenKind::Ident(SmolStr::new("some_var"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Let( + Pattern::Ident(IdentWithToken::new_with_token("z", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: + Expr::Ident(IdentWithToken::new_with_token("some_var", + Some(3.into()))) + }), + ), + }) + ]))] +#[case::let_5( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("z"))), + token(TokenKind::Equal), + token(TokenKind::Ident(SmolStr::new("some_var"))), + token(TokenKind::Pipe), + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Pipe))))] +#[case::let_6( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("z"))), + token(TokenKind::Equal), + token(TokenKind::Ident(SmolStr::new("some_var"))), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Let( + Pattern::Ident(IdentWithToken::new_with_token("z", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: + Expr::Ident(IdentWithToken::new_with_token("some_var", Some(3.into()))), + }), + ), + }) + ]))] +#[case::var_1( + vec![ + token(TokenKind::Var), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Var( + Pattern::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + ), + }) + ]))] +#[case::var_2( + vec![ + token(TokenKind::Var), + token(TokenKind::Ident(SmolStr::new("count"))), + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Var( + Pattern::Ident(IdentWithToken::new_with_token("count", Some(1.into()))), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ), + }) + ]))] +#[case::assign_1( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(100.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Assign( + IdentWithToken::new_with_token("x", Some(0.into())), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(100.into())), + }), + ), + }) + ]))] +#[case::assign_2( + vec![ + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::Equal), + token(TokenKind::StringLiteral("Alice".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Assign( + IdentWithToken::new_with_token("name", Some(0.into())), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("Alice".to_owned())), + }), + ), + }) + ]))] +#[case::index_assign( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(10.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::IndexAssign( +IdentWithToken::new_with_token("arr", Some(0.into())), +Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), +Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(10.into())), + }), +), + }) + ]))] +#[case::index_compound_assign( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::PlusEqual), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::IndexAssign( +IdentWithToken::new_with_token("arr", Some(0.into())), +Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), +Shared::new(Node { + token_id: 3.into(), + expr: Expr::BinaryOp( + BinaryOp::Add, + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ), + }), +), + }) + ]))] +#[case::index_double_slash_equal( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::DoubleSlashEqual), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + // arr[0] //= 2 โ†’ arr[0] = floor(div(get(arr, 0), 2)) + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::IndexAssign( +IdentWithToken::new_with_token("arr", Some(0.into())), +Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), +Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::FLOOR, Some(3.into())), + smallvec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::BinaryOp( + BinaryOp::Div, + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + })], + ), + }), +), + }) + ]))] +#[case::root_semicolon_error( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::SemiColon), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident(SmolStr::new("y"))))))] +#[case::if_1( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Else), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("false branch".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 7.into(), + expr: Expr::If(vec![ + ( + Some(Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + })), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("true branch".to_owned())), + }) + ), + ( + None, + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Literal(Literal::String("false branch".to_owned())), + }) + ) + ]), + }) + ]))] +#[case::if_elif_else( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Elif), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("elif branch".to_owned())), + token(TokenKind::Else), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("else branch".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 11.into(), + expr: Expr::If(vec![ + ( + Some(Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + })), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("true branch".to_owned())), + }) + ), + ( + Some(Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::Bool(false)), + })), + Shared::new(Node { + token_id: 7.into(), + expr: Expr::Literal(Literal::String("elif branch".to_owned())), + }) + ), + ( + None, + Shared::new(Node { + token_id: 10.into(), + expr: Expr::Literal(Literal::String("else branch".to_owned())), + }) + ) + ]), + }) + ]))] +#[case::if_only( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::If(vec![ + ( + Some(Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + })), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("true branch".to_owned())), + }) + ), + ]), + }) + ]))] +#[case::if_elif( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Elif), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 8.into(), + expr: Expr::If(vec![ + ( + Some(Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + })), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("true branch".to_owned())), + }) + ), + ( + Some(Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::Bool(true)), + })), + Shared::new(Node { + token_id: 7.into(), + expr: Expr::Literal(Literal::String("true branch".to_owned())), + }) + ), + ]), + }) + ]))] +#[case::if_error( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Elif), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("elif branch".to_owned())), + token(TokenKind::Else), + token(TokenKind::Colon), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFDetected(0.into())))] +#[case::if_error( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Elif), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("elif branch".to_owned())), + token(TokenKind::Else), + token(TokenKind::Colon), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::If, module_id: 1.into()})))] +#[case::elif_error( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("true branch".to_owned())), + token(TokenKind::Elif), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("elif branch".to_owned())), + token(TokenKind::Else), + token(TokenKind::Colon), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Elif, module_id: 1.into()})))] +#[case::h_selector( + vec![ + token(TokenKind::Selector(SmolStr::new(".h"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Selector(Selector::Heading(None)), + }) + ]))] +#[case::h_selector_without_number( + vec![ + token(TokenKind::Selector(SmolStr::new(".h"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Selector(Selector::Heading(None)), + }) + ]))] +#[case::while_( + vec![ + token(TokenKind::While), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::SemiColon), + ], + Ok(vec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::While( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + vec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("loop body".to_owned())), + })], + ), + })]))] +#[case::while_error( + vec![ + token(TokenKind::While), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::SemiColon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::While, module_id: 1.into()})))] +#[case::while_error( + vec![ + token(TokenKind::While), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::While, module_id: 1.into()})))] +#[case::while_do_end( + vec![ + token(TokenKind::While), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Do), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::End), + ], + Ok(vec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::While( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + vec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("loop body".to_owned())), + })], + ), + })]))] +#[case::loop_( + vec![ + token(TokenKind::Loop), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::SemiColon), + ], + Ok(vec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Loop( + vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("loop body".to_owned())), + })], + ), + })]))] +#[case::loop_error_no_body( + vec![ + token(TokenKind::Loop), + token(TokenKind::Colon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Loop, module_id: 1.into()})))] +#[case::until_( + vec![ + token(TokenKind::Until), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::SemiColon), + ], + Ok(vec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Until( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + vec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("loop body".to_owned())), + })], + ), + })]))] +#[case::until_error( + vec![ + token(TokenKind::Until), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("loop body".to_owned())), + token(TokenKind::SemiColon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Until, module_id: 1.into()})))] +#[case::unless_( + vec![ + token(TokenKind::Unless), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("branch".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Unless(vec![ + ( + Some(Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(false)), + })), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("branch".to_owned())), + }) + ), + ]), + }) + ]))] +#[case::unless_error( + vec![ + token(TokenKind::Unless), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("branch".to_owned())), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Unless, module_id: 1.into()})))] +#[case::try_catch( + vec![ + token(TokenKind::Try), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("error_expr"))), + token(TokenKind::Catch), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("fallback".to_owned())), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Try( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(2.into()))), + }), + None, + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("fallback".to_owned())), + }), + ), + })]))] +#[case::try_catch_with_binder( + vec![ + token(TokenKind::Try), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("error_expr"))), + token(TokenKind::Catch), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("e"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("e"))), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Try( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(2.into()))), + }), + Some(IdentWithToken::new_with_token("e", Some(4.into()))), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("e", Some(6.into()))), + }), + ), + })]))] +#[case::foreach( + vec![ + token(TokenKind::Foreach), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::Comma), + token(TokenKind::StringLiteral("array".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("print"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::RParen), + token(TokenKind::SemiColon), + ], + Ok(vec![Shared::new(Node { + token_id: 6.into(), + expr: Expr::Foreach( + IdentWithToken::new_with_token( + "item", + Some(0.into()), + ), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("array".to_owned())), + }), + vec![Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token( + "print", + Some(4.into()), + ), + smallvec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::Ident(IdentWithToken::new_with_token( + "item", + Some(3.into()), + )), + })], + ), + })], + ), + })]))] +#[case::foreach( + vec![ + token(TokenKind::Foreach), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("print"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::RParen), + token(TokenKind::SemiColon), + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind:TokenKind::Foreach, module_id: 1.into()})))] +#[case::foreach_do_end( + vec![ + token(TokenKind::Foreach), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::Comma), + token(TokenKind::StringLiteral("array".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Do), + token(TokenKind::Ident(SmolStr::new("print"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::RParen), + token(TokenKind::End), + ], + Ok(vec![Shared::new(Node { + token_id: 6.into(), + expr: Expr::Foreach( + IdentWithToken::new_with_token( + "item", + Some(0.into()), + ), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("array".to_owned())), + }), + vec![Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token( + "print", + Some(4.into()), + ), + smallvec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::Ident(IdentWithToken::new_with_token( + "item", + Some(3.into()), + )), + })], + ), + })], + ), + })]))] +#[case::self_( + vec![token(TokenKind::Self_), token(TokenKind::Eof)], + Ok(vec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Self_, + })]))] +#[case::include( + vec![ + token(TokenKind::Include), + token(TokenKind::StringLiteral("module_name".to_owned())), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Include(Literal::String("module_name".to_owned())), + })]))] +#[case::code_selector_with_language( + vec![ + token(TokenKind::Selector(SmolStr::new(".code"))), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Selector(Selector::Code), + })]))] +#[case::selector_call_heading_single_arg( + vec![ + token(TokenKind::Selector(SmolStr::new(".h"))), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::SelectorCall( + Selector::Heading(None), + // arg literal is allocated first (id=0), then the selector token (id=1) + Box::new(smallvec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + })]), + ), + })]))] +#[case::selector_call_heading_multi_arg( + vec![ + token(TokenKind::Selector(SmolStr::new(".h"))), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Comma), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::SelectorCall( + Selector::Heading(None), + // args are allocated first (id=0, id=1), then the selector token (id=2) + Box::new(smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ]), + ), + })]))] +#[case::selector_call_code_lang( + vec![ + token(TokenKind::Selector(SmolStr::new(".code"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("rust".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::SelectorCall( + Selector::Code, + // arg literal is allocated first (id=0), then the selector token (id=1) + Box::new(smallvec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("rust".to_owned())), + })]), + ), + })]))] +#[case::selector_call_with_attribute( + vec![ + token(TokenKind::Selector(SmolStr::new(".h"))), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RParen), + token(TokenKind::Selector(SmolStr::new(".level"))), + token(TokenKind::Eof), + ], + 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: Expr::Call( + IdentWithToken::new_with_token( + constants::builtins::ATTR, + Some(1.into()), + ), + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::SelectorCall( + Selector::Heading(None), + Box::new(smallvec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + })]), + ), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("level".to_owned())), + }), + ], + ), + })]))] +#[case::selector_call_code_with_lang_attribute( + vec![ + token(TokenKind::Selector(SmolStr::new(".code"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("rust".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Selector(SmolStr::new(".lang"))), + token(TokenKind::Eof), + ], + 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: Expr::Call( + IdentWithToken::new_with_token( + constants::builtins::ATTR, + Some(1.into()), + ), + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::SelectorCall( + Selector::Code, + Box::new(smallvec![Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("rust".to_owned())), + })]), + ), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("lang".to_owned())), + }), + ], + ), + })]))] +#[case::table_selector( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 8.into(), + expr: Expr::Selector(Selector::Table(Some(1), Some(2))), + })]))] +#[case::selector_call_list_bracket_single_arg( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::SelectorCall( + Selector::List(None, None), + Box::new(smallvec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(2.into())), + })]), + ), + })]))] +#[case::selector_call_table_bracket_single_arg( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::SelectorCall( + Selector::Table(None, None), + Box::new(smallvec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(1.into())), + })]), + ), + })]))] +#[case::selector_call_list_bracket_variable( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("v"))), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::SelectorCall( + Selector::List(None, None), + Box::new(smallvec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token( + "v", + Some(1.into()), + )), + })]), + ), + })]))] +#[case::selector_call_table_bracket_column_variable( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("v"))), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 5.into(), + expr: Expr::SelectorCall( + Selector::Table(None, None), + Box::new(smallvec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::None), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token( + "v", + Some(2.into()), + )), + }), + ]), + ), + })]))] +#[case::selector_call_table_bracket_variable( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("v"))), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 4.into(), + expr: Expr::SelectorCall( + Selector::Table(None, None), + Box::new(smallvec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token( + "v", + Some(1.into()), + )), + })]), + ), + })]))] +#[case::selector_call_table_bracket_row_col_args( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Comma), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 4.into(), + expr: Expr::SelectorCall( + Selector::Table(None, None), + Box::new(smallvec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ]), + ), + })]))] +#[case::foreach_error( + vec![ + token(TokenKind::Foreach), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("item"))), + token(TokenKind::Comma), + token(TokenKind::StringLiteral("array".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnexpectedEOFDetected(Module::TOP_LEVEL_MODULE_ID)))] +#[case::while_error( + vec![ + token(TokenKind::While), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnexpectedEOFDetected(Module::TOP_LEVEL_MODULE_ID)))] +#[case::using_reserved_keyword_let( + vec![ + token(TokenKind::Let), + token(TokenKind::If), // Using "if" as a variable name (should error) + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::If, module_id: 1.into()})))] +#[case::using_reserved_keyword_while( + vec![ + token(TokenKind::Let), + token(TokenKind::While), // Using "while" as a variable name (should error) + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::While, module_id: 1.into()})))] +#[case::using_reserved_keyword_def( + vec![ + token(TokenKind::Let), + token(TokenKind::Def), // Using "def" as a variable name (should error) + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::Def, module_id: 1.into()})))] +#[case::using_reserved_keyword_include( + vec![ + token(TokenKind::Let), + token(TokenKind::Include), // Using "include" as a variable name (should error) + token(TokenKind::Equal), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token{range: Range::default(), kind: TokenKind::Include, module_id: 1.into()})))] +#[case::nodes( + vec![ + token(TokenKind::Nodes), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Nodes, + }) + ]))] +#[case::nodes_error_in_subprogram( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("test"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Nodes), + token(TokenKind::SemiColon) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Nodes))))] +#[case::nodes_then_selector( + vec![ + token(TokenKind::Nodes), + token(TokenKind::Pipe), + token(TokenKind::Selector(SmolStr::new(".h1"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Nodes, + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Selector(Selector::Heading(Some(1))), + }) + ]))] +#[case::root_level_with_multiple_pipes( + vec![ + token(TokenKind::Nodes), + token(TokenKind::Pipe), + token(TokenKind::Nodes), + token(TokenKind::Pipe), + token(TokenKind::Selector(SmolStr::new(".h1"))), + token(TokenKind::Pipe), + token(TokenKind::Selector(SmolStr::new(".text"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Nodes, + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Nodes, + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Selector(Selector::Heading(Some(1))), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Selector(Selector::Text), + }) + ]))] +#[case::fn_simple( + vec![ + token(TokenKind::Fn), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("result".to_owned())), + token(TokenKind::SemiColon), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + Vec::new(), + vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("result".to_owned())), + }) + ], + ), + }) + ]))] +#[case::fn_with_args( + vec![ + token(TokenKind::Fn), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("contains"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::RParen), + token(TokenKind::SemiColon), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + vec![ + Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), + Param::new(IdentWithToken::new_with_token("y", Some(2.into()))), + ], + vec![ + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("contains", Some(6.into())), + smallvec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(4.into()))), + }), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(5.into()))), + }), + ], + ), + }) + ], + ), + }) + ]))] +#[case::fn_with_multiple_statements( + vec![ + token(TokenKind::Fn), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("first".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::StringLiteral("second".to_owned())), + token(TokenKind::SemiColon), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + vec![ + Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), + ], + vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("first".to_owned())), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::String("second".to_owned())), + }) + ], + ), + }) + ]))] +#[case::fn_with_invalid_args( + vec![ + token(TokenKind::Fn), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("invalid".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("result".to_owned())), + token(TokenKind::SemiColon), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::StringLiteral("invalid".to_owned())))))] +#[case::fn_without_body( + vec![ + token(TokenKind::Fn), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::SemiColon), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::SemiColon))))] +#[case::fn_nested_in_call( + vec![ + token(TokenKind::Ident(SmolStr::new("apply"))), + token(TokenKind::LParen), + token(TokenKind::Fn), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("processed".to_owned())), + token(TokenKind::SemiColon), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("apply", Some(4.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + vec![ + Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), + ], + vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("processed".to_owned())), + }) + ], + ), + }) + ], + ), + }) + ]))] +#[case::empty_array( + vec![ + token(TokenKind::LBracket), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array(SmallVec::new()), + }) + ]))] +#[case::array_with_elements( + vec![ + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("first".to_owned())), + token(TokenKind::Comma), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("first".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + ], + ), + }) + ]))] +#[case::array_with_mixed_elements( + vec![ + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("text".to_owned())), + token(TokenKind::Comma), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::Comma), + token(TokenKind::None), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("text".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::None), + }), + ], + ), + }) + ]))] +#[case::array_with_nested_array( + vec![ + token(TokenKind::LBracket), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RBracket), + token(TokenKind::Comma), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RBracket), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ], + ), + }), + ], + ), + }) + ]))] +#[case::array_with_trailing_comma( + vec![ + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::Comma), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + ], + ), + }) + ]))] +#[case::array_unclosed( + vec![ + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::Eof) + ], + Err(SyntaxError::ExpectedClosingBracket(token(TokenKind::Eof), Some(Box::new(token(TokenKind::LBracket))))))] +#[case::array_invalid_token( + vec![ + token(TokenKind::LBracket), + token(TokenKind::Pipe), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Pipe))))] +#[case::array_nested_unclosed( + vec![ + token(TokenKind::LBracket), + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("inner".to_owned())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Err(SyntaxError::ExpectedClosingBracket(token(TokenKind::Eof), Some(Box::new(token(TokenKind::LBracket))))))] +#[case::array_with_ident( + vec![ + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("bar"))), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("foo", Some(1.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("bar", Some(2.into()))), + }), + ], + ), + }) + ]))] +#[case::equality_simple( + vec![ + token(TokenKind::StringLiteral("hello".to_owned())), + token(TokenKind::EqEq), + token(TokenKind::StringLiteral("world".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("hello".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("world".to_owned())), + }), + ), + }) + ]))] +#[case::equality_numbers( + vec![ + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::EqEq), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + ), + }) + ]))] +#[case::equality_booleans( + vec![ + token(TokenKind::BoolLiteral(true)), + token(TokenKind::EqEq), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Bool(false)), + }), + ), + }) + ]))] +#[case::equality_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::EqEq), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), + }), + ), + }) + ]))] +#[case::equality_with_function_call( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg".to_owned())), + token(TokenKind::RParen), + token(TokenKind::EqEq), + token(TokenKind::StringLiteral("result".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("foo", Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("arg".to_owned())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("result".to_owned())), + }), + ), + }) + ]))] +#[case::equality_with_selectors( + vec![ + token(TokenKind::Selector(SmolStr::new(".h1"))), + token(TokenKind::EqEq), + token(TokenKind::Selector(SmolStr::new(".text"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Selector(Selector::Heading(Some(1))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Selector(Selector::Text), + }), + ), + }) + ]))] +#[case::equality_with_none( + vec![ + token(TokenKind::None), + token(TokenKind::EqEq), + token(TokenKind::None), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::None), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::None), + }), + ), + }) + ]))] +#[case::equality_error_missing_rhs( + vec![ + token(TokenKind::StringLiteral("hello".to_owned())), + token(TokenKind::EqEq), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::EqEq))))] +#[case::equality_in_if_condition( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::EqEq), + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("equal".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 6.into(), + expr: Expr::If(vec![ + ( + Some(Shared::new(Node { + token_id: 2.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(5.into())), + }), + ), + })), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("equal".to_owned())), + }) + ), + ]), + }) + ]))] +#[case::not_equality_simple( + vec![ + token(TokenKind::StringLiteral("hello".to_owned())), + token(TokenKind::NeEq), + token(TokenKind::StringLiteral("world".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("hello".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("world".to_owned())), + }), + ), + }) + ]))] +#[case::not_equality_numbers( + vec![ + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::NeEq), + token(TokenKind::NumberLiteral(24.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(24.into())), + }), + ), + }) + ]))] +#[case::not_equality_booleans( + vec![ + token(TokenKind::BoolLiteral(true)), + token(TokenKind::NeEq), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Bool(false)), + }), + ), + }) + ]))] +#[case::not_equality_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::NeEq), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), + }), + ), + }) + ]))] +#[case::not_equality_with_function_call( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg".to_owned())), + token(TokenKind::RParen), + token(TokenKind::NeEq), + token(TokenKind::StringLiteral("result".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("foo", Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("arg".to_owned())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("result".to_owned())), + }), + ), + }) + ]))] +#[case::not_equality_with_selectors( + vec![ + token(TokenKind::Selector(SmolStr::new(".h1"))), + token(TokenKind::NeEq), + token(TokenKind::Selector(SmolStr::new(".text"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Selector(Selector::Heading(Some(1))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Selector(Selector::Text), + }), + ), + }) + ]))] +#[case::not_equality_with_none( + vec![ + token(TokenKind::None), + token(TokenKind::NeEq), + token(TokenKind::StringLiteral("something".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::None), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("something".to_owned())), + }), + ), + }) + ]))] +#[case::not_equality_error_missing_rhs( + vec![ + token(TokenKind::StringLiteral("hello".to_owned())), + token(TokenKind::NeEq), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::NeEq))))] +#[case::not_equality_in_if_condition( + vec![ + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::NeEq), + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("not equal".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 6.into(), + expr: Expr::If(vec![ + ( + Some(Shared::new(Node { + token_id: 2.into(), + expr: Expr::BinaryOp( + BinaryOp::Ne, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(5.into())), + }), + ), + })), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("not equal".to_owned())), + }) + ), + ]), + }) + ]))] +#[case::plus_simple( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Plus), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Add, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ]))] +#[case::plus_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Plus), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Add, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), + }), + ), + }) + ]))] +#[case::plus_error_missing_rhs( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Plus), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Plus))))] +#[case::lt_simple( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Lt), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Lt, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ]))] +#[case::lte_simple( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Lte), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Le, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ]))] +#[case::gt_simple( + vec![ + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Gt), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Gt, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ]))] +#[case::gte_simple( + vec![ + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Gte), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Ge, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ]))] +#[case::dict_empty( + vec![ + token(TokenKind::LBrace), + token(TokenKind::RBrace), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Dict(SmallVec::new()), + }) + ]))] +#[case::dict_single_pair( + vec![ + token(TokenKind::LBrace), + token(TokenKind::Ident(SmolStr::new("key"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("value".to_owned())), + token(TokenKind::RBrace), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Dict( + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Symbol(Ident::new("key"))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + ], + ), + }), + ], + ), + }) + ]))] +#[case::dict_multiple_pairs( + vec![ + token(TokenKind::LBrace), + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Comma), + token(TokenKind::StringLiteral("b".to_owned())), + token(TokenKind::Colon), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::RBrace), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Dict( + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Symbol(Ident::new("a"))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("b".to_owned())), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Bool(true)), + }), + ], + ), + }), + ], + ), + }) + ]))] +#[case::dict_trailing_comma( + vec![ + token(TokenKind::LBrace), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(10.into())), + token(TokenKind::Comma), + token(TokenKind::RBrace), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Dict( + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Symbol(Ident::new("x"))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(10.into())), + }), + ], + ), + }), + ], + ), + }) + ]))] +#[case::dict_unclosed( + vec![ + token(TokenKind::LBrace), + token(TokenKind::Ident(SmolStr::new("k"))), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Eof) + ], + Err(SyntaxError::ExpectedClosingBrace(token(TokenKind::Eof), Some(Box::new(token(TokenKind::LBrace))))))] +#[case::dict_missing_colon( + vec![ + token(TokenKind::LBrace), + token(TokenKind::Ident(SmolStr::new("k"))), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RBrace), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::NumberLiteral(1.into())))))] +#[case::dict_invalid_key( + vec![ + token(TokenKind::LBrace), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("v".to_owned())), + token(TokenKind::RBrace), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::NumberLiteral(1.into())))))] +#[case::attr_h_value( + vec![ + token(TokenKind::Selector(".h".into())), + token(TokenKind::Selector(".value".into())), + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Selector(Selector::Heading(None)), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + + ], + )})]))] +#[case::attr( + vec![ + token(TokenKind::Selector(".list".into())), + token(TokenKind::Selector(".checked".into())), + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call(IdentWithToken::new_with_token(constants::builtins::ATTR, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Selector(Selector::List(None, None)), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("checked".to_owned())), + }), + + ], + )})]))] +#[case::paren( + vec![ + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Plus), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RParen), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Paren( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::BinaryOp( + BinaryOp::Add, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ), + }) + ]))] +#[case::minus_simple( + vec![ + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::Minus), + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Sub, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(5.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + ), + }) + ]))] +#[case::minus_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Minus), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Sub, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + ), + }) + ]))] +#[case::slash_simple( + vec![ + token(TokenKind::NumberLiteral(6.into())), + token(TokenKind::Slash), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Div, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(6.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }) + ]))] +#[case::percent_simple( + vec![ + token(TokenKind::NumberLiteral(10.into())), + token(TokenKind::Percent), + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Mod, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(10.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + ), + }) + ]))] +#[case::percent_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Percent), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Mod, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + ), + }) + ]))] +#[case::percent_error_missing_rhs( + vec![ + token(TokenKind::NumberLiteral(10.into())), + token(TokenKind::Percent), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Percent))))] +#[case::mul_simple( + vec![ + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Asterisk), + token(TokenKind::NumberLiteral(4.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Mul, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(4.into())), + }), + ), + }) + ]))] +#[case::mul_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Asterisk), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Mul, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + ), + }) + ]))] +#[case::mul_error_missing_rhs( + vec![ + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::Asterisk), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Asterisk))))] +#[case::convert_simple( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Convert), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::CONVERT, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + ], + ), + }) + ]))] +#[case::convert_error_missing_rhs( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Convert), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Convert))))] +#[case::multiple_binary_operators( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Asterisk), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Asterisk), + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::BinaryOp( + BinaryOp::Mul, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Mul, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + ), + }) + ]))] +#[case::multiple_binary_operators_eq( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Plus), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::EqEq), + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::BinaryOp( + BinaryOp::Eq, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Add, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + ), + }) + ]))] +#[case::multiple_and_operators( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + ]), + }) + ]))] +#[case::multiple_or_operators( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("z"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("z", Some(4.into()))), + }), + ]), + }) + ]))] +#[case::and_or_mixed( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + ]), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + ]), + }) + ]))] +#[case::four_and_operators( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("d"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), + }), + ]), + }) + ]))] +#[case::four_or_operators( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("d"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), + }), + ]), + }) + ]))] +#[case::or_with_and_in_middle( + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("d"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + ]), + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), + }), + ]), + }) + ]))] +#[case::and_or_and_mixed( + // a && b || c && d => Or([And([a, b]), And([c, d])]) + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("d"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + ]), + }), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), + }), + ]), + }), + ]), + }) + ]))] +#[case::or_and_or_mixed( + // a || b && c || d => Or([a, And([b, c]), d]) + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("d"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + ]), + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), + }), + ]), + }) + ]))] +#[case::and_and_or_and_and_mixed( + // a && b && c || d && e && f => Or([And([a,b,c]), And([d,e,f])]) + vec![ + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("c"))), + token(TokenKind::Or), + token(TokenKind::Ident(SmolStr::new("d"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("e"))), + token(TokenKind::And), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("a", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("b", Some(2.into()))), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("c", Some(4.into()))), + }), + ]), + }), + Shared::new(Node { + token_id: 9.into(), + expr: Expr::And(vec![ + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("d", Some(6.into()))), + }), + Shared::new(Node { + token_id: 8.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("e", Some(8.into()))), + }), + Shared::new(Node { + token_id: 10.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("f", Some(10.into()))), + }), + ]), + }), + ]), + }) + ]))] +#[case::range_simple( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::DoubleDot), + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::RANGE, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(5.into())), + }), + ], + ), + }) + ]))] +#[case::range_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("start"))), + token(TokenKind::DoubleDot), + token(TokenKind::Ident(SmolStr::new("end"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::RANGE, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("start", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("end", Some(2.into()))), + }), + ], + ), + }) + ]))] +#[case::range_error_missing_rhs( + vec![ + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::DoubleDot), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::DoubleDot))))] +#[case::args_missing_rparen( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("bar".to_owned())), + // Missing RParen + token(TokenKind::Eof) + ], + Err(SyntaxError::ExpectedClosingParen( + token(TokenKind::Eof), + Some(Box::new(token(TokenKind::LParen))), + )) + )] +#[case::args_unexpected_token( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), // Invalid token in args + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Err(SyntaxError::ExpectedClosingParen( + token(TokenKind::Colon), + Some(Box::new(token(TokenKind::LParen))), + )) + )] +#[case::args_leading_comma( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("bar"))), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Comma))) + )] +#[case::args_double_comma( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("bar"))), + token(TokenKind::Comma), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("baz"))), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Comma))) + )] +#[case::binary_operator_chaining( + vec![ + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Gt), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Or), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Gt), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Or(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::BinaryOp( + BinaryOp::Gt, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ), + }), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::BinaryOp( + BinaryOp::Gt, + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ), + }), + ]), + }) + ]))] +#[case::not_simple( + vec![ + token(TokenKind::Not), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp(UnaryOp::Not, Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Bool(false)), + })), + }) + ]))] +#[case::not_with_expr( + vec![ + token(TokenKind::Not), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp(UnaryOp::Not, Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + })), + }) + ]))] +#[case::bracket_access_with_number( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(5.into())), + }), + ], + ), + }) + ]))] +#[case::bracket_access_with_string( + vec![ + token(TokenKind::Ident(SmolStr::new("dict"))), + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("key".to_owned())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token(constants::builtins::DICT, Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("key".to_owned())), + }), + ], + ), + }) + ]))] +#[case::bracket_access_error_missing_rbracket( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::Eof) + ], + Err(SyntaxError::ExpectedClosingBracket( + token(TokenKind::Eof), + Some(Box::new(token(TokenKind::LBracket))), + )))] +#[case::slice_access_with_numbers( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(3.into())), + }), + ], + ), + }) + ]))] +#[case::slice_access_with_variables( + vec![ + token(TokenKind::Ident(SmolStr::new("items"))), + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("start"))), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("end"))), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("items", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("start", Some(1.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("end", Some(2.into()))), + }), + ], + ), + }) + ]))] +#[case::not_with_paren_expr( + vec![ + token(TokenKind::Not), + token(TokenKind::LParen), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp(UnaryOp::Not, Shared::new(Node { + token_id: 1.into(), + expr: Expr::Paren( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Bool(false)), + }) + ), + })), + }) + ]))] +#[case::not_none( + vec![ + token(TokenKind::Not), + token(TokenKind::None), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp( + UnaryOp::Not, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::None), + }) + ), + }) + ]))] +#[case::not_error_missing_rhs( + vec![ + token(TokenKind::Not), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Not))))] +#[case::break_( + vec![ + token(TokenKind::Break), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Break(None), + }) + ]))] +#[case::continue_( + vec![ + token(TokenKind::Continue), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Continue, + }) + ]))] +#[case::self_bracket_access_with_number( + vec![ + token(TokenKind::Self_), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(5.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Self_, + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(5.into())), + }), + ], + ), + }) + ]))] +#[case::self_bracket_access_with_string( + vec![ + token(TokenKind::Self_), + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("key".to_owned())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Self_, + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("key".to_owned())), + }), + ], + ), + }) + ]))] +// Test function call followed by index access (e.g., foo()[0]) +#[case::function_call_with_index_access( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("foo", Some(0.into())), + SmallVec::new(), + ), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }) + ]))] +// Test function call with arguments followed by index access +#[case::function_call_with_args_and_index_access( + vec![ + token(TokenKind::Ident(SmolStr::new("bar"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg".to_owned())), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::StringLiteral("key".to_owned())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("bar", Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("arg".to_owned())), + }) + ], + ), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("key".to_owned())), + }), + ], + ), + }) + ]))] +// Test chained index access on function call result +#[case::function_call_with_chained_index_access( + vec![ + token(TokenKind::Ident(SmolStr::new("baz"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("baz", Some(0.into())), + SmallVec::new(), + ), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ], + ), + }) + ]))] +#[case::try_without_catch( + vec![ + token(TokenKind::Try), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("error_expr"))), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Try( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("error_expr", Some(2.into()))), + }), + None, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::None), + }), + ), + })]) + )] +// Test index access followed by function call (e.g., arr[0]()) +#[case::index_access_with_function_call( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::CallDynamic( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }), + SmallVec::new(), + ), + }) + ]))] +// Test index access with args followed by function call (e.g., arr[0](arg)) +#[case::index_access_with_function_call_and_args( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("test".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::CallDynamic( + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }), + smallvec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("test".to_owned())), + }) + ], + ), + }) + ]))] +// Test group expr with index access: (x)[0] โ†’ get(Paren(x), 0) +#[case::group_expr_with_index_access( + vec![ + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Paren( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + }), + ), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }) + ]))] +// Test group expr with dynamic call: (x)("test") โ†’ CallDynamic(Paren(x), ["test"]) +#[case::group_expr_with_dynamic_call( + vec![ + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("test".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::CallDynamic( + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Paren( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + }), + ), + }), + smallvec![ + Shared::new(Node { + token_id: 2.into(), + 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) +#[case::group_expr_with_chained_call_and_index( + vec![ + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::RParen), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("a".to_owned())), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::CallDynamic( + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Paren( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("f", Some(1.into()))), + }), + ), + }), + smallvec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("a".to_owned())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }) + ]))] +// Test array literal with index access: [1,2][0] โ†’ get(array([1,2]), 0) +#[case::array_literal_with_index_access( + vec![ + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Comma), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Array( + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }) + ]))] +// Test chained index access followed by function call (e.g., arr[0][1]()) +#[case::chained_index_access_with_function_call( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RBracket), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RBracket), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::CallDynamic( + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::GET, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + ], + ), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ], + ), + }), + SmallVec::new(), + ), + }) + ]))] +#[case::function_call_with_question_mark( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Question), + token(TokenKind::Eof), + ], + Ok(vec![Shared::new(Node { + token_id: 1.into(), + expr: Expr::Try( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("foo", Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("arg".to_owned())), + }), + ], + ), + }), + None, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::None), + }), + ), + })]) + )] +#[case::question_mark_after_call( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Question), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Try( + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("foo", Some(0.into())), + SmallVec::new(), + ), + }), + None, + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::None), + }), + ), + }) + ]))] +#[case::question_mark_after_call_with_args( + vec![ + token(TokenKind::Ident(SmolStr::new("bar"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Question), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Try( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("bar", Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("arg".to_owned())), + }), + ], + ), + }), + None, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::None), + }), + ), + }) + ]))] +#[case::question_mark_after_call_error( + vec![ + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::Question), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Ident("foo".into())))))] +#[case::coalesce_simple( + vec![ + token(TokenKind::StringLiteral("foo".to_owned())), + token(TokenKind::Coalesce), + token(TokenKind::StringLiteral("bar".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::String("foo".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("bar".to_owned())), + }), + ], + ), + }) + ]))] +#[case::coalesce_with_none( + vec![ + token(TokenKind::None), + token(TokenKind::Coalesce), + token(TokenKind::StringLiteral("default".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Literal(Literal::None), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("default".to_owned())), + }), + ], + ), + }) + ]))] +#[case::coalesce_with_identifiers( + vec![ + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Coalesce), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::COALESCE, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(0.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(2.into()))), + }), + ], + ), + }) + ]))] +#[case::coalesce_error_missing_rhs( + vec![ + token(TokenKind::StringLiteral("foo".to_owned())), + token(TokenKind::Coalesce), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Coalesce))))] +#[case::negate_simple( + vec![ + token(TokenKind::Minus), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp(UnaryOp::Neg, Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(42.into())), + })), + }) + ]))] +#[case::negate_with_identifier( + vec![ + token(TokenKind::Minus), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp(UnaryOp::Neg, Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))), + })), + }) + ]))] +#[case::negate_paren_expr( + vec![ + token(TokenKind::Minus), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RParen), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp( + UnaryOp::Neg, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Paren(Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(1.into())), + })), + }) + ), + }) + ]))] +#[case::negate_string( + vec![ + token(TokenKind::Minus), + token(TokenKind::StringLiteral("a".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::UnaryOp( + UnaryOp::Neg, + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("a".to_owned())), + }) + ), + }) + ]))] +#[case::negate_error_missing_rhs( + vec![ + token(TokenKind::Minus), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedEOFAfterToken(token(TokenKind::Minus))))] +#[case::import_simple( + vec![ + token(TokenKind::Import), + token(TokenKind::StringLiteral("name".to_owned())), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Import( + Literal::String("name".to_owned()), + None, + ), + }) + ]))] +#[case::import_as( + vec![ + token(TokenKind::Import), + token(TokenKind::StringLiteral("name".to_owned())), + token(TokenKind::As), + token(TokenKind::Ident(SmolStr::new("alias"))), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Import( + Literal::String("name".to_owned()), + Some(IdentWithToken::new_with_token( + "alias", + Some(1.into()), + )), + ), + }) + ]))] +#[case::import_as_missing_ident( + vec![ + token(TokenKind::Import), + token(TokenKind::StringLiteral("name".to_owned())), + token(TokenKind::As), + token(TokenKind::Eof), + ], + Err(SyntaxError::UnexpectedToken(token(TokenKind::Eof))))] +#[case::qualified_access( + vec![ + token(TokenKind::Ident(SmolStr::new("test"))), + token(TokenKind::DoubleColon), + token(TokenKind::Ident(SmolStr::new("foo"))), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::QualifiedAccess( + vec![IdentWithToken::new_with_token("test", Some(0.into()))], + AccessTarget::Ident(IdentWithToken::new_with_token("foo", Some(1.into())), + )), + }) + ]))] +#[case::qualified_access_with_call( + vec![ + token(TokenKind::Ident(SmolStr::new("mod"))), + token(TokenKind::DoubleColon), + token(TokenKind::Ident(SmolStr::new("func"))), + token(TokenKind::LParen), + token(TokenKind::StringLiteral("arg".to_owned())), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::QualifiedAccess( + vec![IdentWithToken::new_with_token("mod", Some(0.into()))], + AccessTarget::Call( + IdentWithToken::new_with_token("func", Some(2.into())), + Box::new(smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("arg".to_owned())), + }), + ]), + ), + ), + }) + ]))] +#[case::qualified_access_multi_level( + vec![ + token(TokenKind::Ident(SmolStr::new("mod1"))), + token(TokenKind::DoubleColon), + token(TokenKind::Ident(SmolStr::new("mod2"))), + token(TokenKind::DoubleColon), + token(TokenKind::Ident(SmolStr::new("func"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::QualifiedAccess( + vec![ + IdentWithToken::new_with_token("mod1", Some(0.into())), + IdentWithToken::new_with_token("mod2", Some(1.into())), + ], + AccessTarget::Call( + IdentWithToken::new_with_token("func", Some(2.into())), + Box::new(smallvec![]), + ), + ), + }) + ]))] +#[case::slice_access_with_start_only( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), + token(TokenKind::RBracket), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("len", None), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + ], + ), + }), + ], + ), + }) + ]))] +#[case::slice_access_with_end_only( + vec![ + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::LBracket), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ], + ), + }) + ]))] +#[case::qualified_access_with_call_and_slice( + vec![ + token(TokenKind::Ident(SmolStr::new("mod"))), + token(TokenKind::DoubleColon), + token(TokenKind::Ident(SmolStr::new("func"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::QualifiedAccess( + vec![IdentWithToken::new_with_token("mod", Some(0.into()))], + AccessTarget::Call( + IdentWithToken::new_with_token("func", Some(1.into())), + Box::new(smallvec![]), + ), + ), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(2.into())), + }), + ], + ), + }) + ]))] +#[case::qualified_access_with_call_and_end_only_slice( + vec![ + token(TokenKind::Ident(SmolStr::new("mod"))), + token(TokenKind::DoubleColon), + token(TokenKind::Ident(SmolStr::new("func"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::LBracket), + token(TokenKind::Colon), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::RBracket), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SLICE, Some(0.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::QualifiedAccess( + vec![IdentWithToken::new_with_token("mod", Some(0.into()))], + AccessTarget::Call( + IdentWithToken::new_with_token("func", Some(1.into())), + Box::new(smallvec![]), + ), + ), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(0.into())), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::Number(1.into())), + }), + ], + ), + }) + ]))] +#[case::selector_dot_is_self( + vec![ + token(TokenKind::Selector(SmolStr::new("."))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Self_, + }) + ]))] +#[case::ident_with_single_attr( + vec![ + token(TokenKind::Ident(SmolStr::new("obj"))), + token(TokenKind::Selector(SmolStr::new(".name"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("name".to_owned())), + }), + ], + ), + }) + ]))] +#[case::function_call_result_with_attr( + vec![ + token(TokenKind::Ident(SmolStr::new("get_user"))), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Selector(SmolStr::new(".name"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("get_user", Some(0.into())), + SmallVec::new(), + ), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("name".to_owned())), + }), + ], + ), + }) + ]))] +#[case::ident_with_attr_in_pipe( + vec![ + token(TokenKind::Ident(SmolStr::new("data"))), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("obj"))), + token(TokenKind::Selector(SmolStr::new(".value"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("data", Some(0.into()))), + }), + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ATTR, Some(2.into())), + smallvec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(1.into()))), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + ], + ), + }) + ]))] +#[case::self_with_attr( + vec![ + token(TokenKind::Self_), + token(TokenKind::Selector(SmolStr::new(".value"))), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::ATTR, Some(1.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Self_, + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + ], + ), + }) + ]))] +#[case::pipe_equal_with_selector( + vec![ + token(TokenKind::Selector(SmolStr::new(".h1"))), + token(TokenKind::Selector(SmolStr::new(".value"))), + token(TokenKind::PipeEqual), + token(TokenKind::StringLiteral("new_id".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(2.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Selector(selector::Selector::Heading(Some(1))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("new_id".to_owned())), + }), + ], + ), + }) + ]))] +#[case::pipe_equal_with_ident_and_attr( + vec![ + token(TokenKind::Ident(SmolStr::new("obj"))), + token(TokenKind::Selector(SmolStr::new(".value"))), + token(TokenKind::PipeEqual), + token(TokenKind::StringLiteral("John".to_owned())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(2.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(0.into()))), + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("John".to_owned())), + }), + ], + ), + }) + ]))] +#[case::pipe_equal_with_self_and_attr( + vec![ + token(TokenKind::Self_), + token(TokenKind::Selector(SmolStr::new(".value"))), + token(TokenKind::PipeEqual), + token(TokenKind::NumberLiteral(42.into())), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Call( + IdentWithToken::new_with_token(constants::builtins::SET_ATTR, Some(2.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Self_, + }), + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::String("value".to_owned())), + }), + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::Number(42.into())), + }), + ], + ), + }) + ]))] +#[case::let_with_reserved_keyword_as_value( + vec![ + token(TokenKind::Let), + token(TokenKind::Ident(SmolStr::new("aaa"))), + token(TokenKind::Equal), + token(TokenKind::Let), // Using "let" as a value (should error) + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Let, + module_id: 1.into(), + })) + )] +#[case::let_with_reserved_keyword_as_variable_and_value( + vec![ + token(TokenKind::Let), + token(TokenKind::Let), // Using "let" as a variable name (should error) + token(TokenKind::Equal), + token(TokenKind::Ident(SmolStr::new("vvv"))), + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Let, + module_id: 1.into(), + })) + )] +#[case::let_with_reserved_keyword_as_variable_and_value2( + vec![ + token(TokenKind::Let), + token(TokenKind::Let), // Using "let" as a variable name (should error) + token(TokenKind::Equal), + token(TokenKind::Let), // Using "let" as a value (should error) + token(TokenKind::Eof) + ], + Err(SyntaxError::UnexpectedToken(Token { + range: Range::default(), + kind: TokenKind::Let, + module_id: 1.into(), + })) + )] +#[case::def_with_variadic_param( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::LParen), + token(TokenKind::Asterisk), + token(TokenKind::Ident(SmolStr::new("args"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("args"))), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("f", Some(4.into())), + vec![ + Param::variadic(IdentWithToken::new_with_token("args", Some(1.into()))), + ], + vec![Shared::new(Node { + token_id: 3.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(3.into()))), + })], + ), + }), + ]))] +#[case::def_with_regular_and_variadic_param( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Comma), + token(TokenKind::Asterisk), + token(TokenKind::Ident(SmolStr::new("rest"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("rest"))), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("f", Some(5.into())), + vec![ + Param::new(IdentWithToken::new_with_token("a", Some(1.into()))), + Param::variadic(IdentWithToken::new_with_token("rest", Some(2.into()))), + ], + vec![Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("rest", Some(4.into()))), + })], + ), + }), + ]))] +#[case::def_variadic_param_not_last( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::LParen), + token(TokenKind::Asterisk), + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::RParen), + ], + Err(SyntaxError::VariadicParameterMustBeLast(Token{range: Range::default(), kind: TokenKind::Ident(SmolStr::new("b")), module_id: 1.into()})))] +#[case::def_multiple_variadic_params( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::LParen), + token(TokenKind::Asterisk), + token(TokenKind::Ident(SmolStr::new("a"))), + token(TokenKind::Comma), + token(TokenKind::Asterisk), + token(TokenKind::Ident(SmolStr::new("b"))), + token(TokenKind::RParen), + ], + Err(SyntaxError::MultipleVariadicParameters(Token{range: Range::default(), kind: TokenKind::Asterisk, module_id: 1.into()})))] +#[case::def_without_params( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("args"))), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("f", Some(3.into())), + vec![], + vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(2.into()))), + })], + ), + }), + ]))] +#[case::def_without_params_with_do( + vec![ + token(TokenKind::Def), + token(TokenKind::Ident(SmolStr::new("f"))), + token(TokenKind::Do), + token(TokenKind::Ident(SmolStr::new("args"))), + token(TokenKind::SemiColon) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Def( + IdentWithToken::new_with_token("f", Some(3.into())), + vec![], + vec![Shared::new(Node { + token_id: 2.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("args", Some(2.into()))), + })], + ), + }), + ]))] +#[case::arrow_simple( + vec![ + token(TokenKind::Arrow), + token(TokenKind::LParen), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("result".to_owned())), + token(TokenKind::SemiColon), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + Vec::new(), + vec![ + Shared::new(Node { + token_id: 2.into(), + expr: Expr::Literal(Literal::String("result".to_owned())), + }) + ], + ), + }) + ]))] +#[case::arrow_with_args( + vec![ + token(TokenKind::Arrow), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("contains"))), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::RParen), + token(TokenKind::SemiColon), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + vec![ + Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), + Param::new(IdentWithToken::new_with_token("y", Some(2.into()))), + ], + vec![ + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("contains", Some(6.into())), + smallvec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(4.into()))), + }), + Shared::new(Node { + token_id: 5.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("y", Some(5.into()))), + }), + ], + ), + }) + ], + ), + }) + ]))] +#[case::arrow_nested_in_call( + vec![ + token(TokenKind::Ident(SmolStr::new("apply"))), + token(TokenKind::LParen), + token(TokenKind::Arrow), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("processed".to_owned())), + token(TokenKind::SemiColon), + token(TokenKind::RParen), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Call( + IdentWithToken::new_with_token("apply", Some(4.into())), + smallvec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Fn( + vec![ + Param::new(IdentWithToken::new_with_token("x", Some(1.into()))), + ], + vec![ + Shared::new(Node { + token_id: 3.into(), + expr: Expr::Literal(Literal::String("processed".to_owned())), + }) + ], + ), + }) + ], + ), + }) + ]))] +fn test_parse(#[case] input: Vec, #[case] expected: Result) { + let mut arena = Arena::new(10); + 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(); + assert_eq!(actual_exprs, expected_exprs); + } + (Err(actual), Err(expected)) => { + assert_eq!(actual, expected); + } + _ => { + panic!("Mismatch: actual = {:?}, expected = {:?}", result, expected) + } + } +} + +#[rstest] +#[case::heading(".h1", Selector::Heading(Some(1)))] +#[case::heading_h3(".h3", Selector::Heading(Some(3)))] +#[case::blockquote(".>", Selector::Blockquote)] +#[case::blockquote_full(".blockquote", Selector::Blockquote)] +#[case::footnote(".^", Selector::Footnote)] +#[case::footnote_full(".footnote", Selector::Footnote)] +#[case::mdx_jsx_flow(".mdx_jsx_flow_element", Selector::MdxJsxFlowElement)] +#[case::mdx_jsx_flow_short(".<", Selector::MdxJsxFlowElement)] +#[case::emphasis(".**", Selector::Emphasis)] +#[case::emphasis_full(".emphasis", Selector::Emphasis)] +#[case::math(".$$", Selector::Math)] +#[case::math_full(".math", Selector::Math)] +#[case::horizontal_rule(".---", Selector::HorizontalRule)] +#[case::horizontal_rule_alt(".***", Selector::HorizontalRule)] +#[case::horizontal_rule_full(".horizontal_rule", Selector::HorizontalRule)] +#[case::mdx_text_expression(".{}", Selector::MdxTextExpression)] +#[case::mdx_text_expression_full(".mdx_text_expression", Selector::MdxTextExpression)] +#[case::footnote_ref(".[^]", Selector::FootnoteRef)] +#[case::footnote_ref_full(".footnote_ref", Selector::FootnoteRef)] +#[case::definition(".definition", Selector::Definition)] +#[case::break_selector(".break", Selector::Break)] +#[case::delete(".delete", Selector::Delete)] +#[case::html(".<>", Selector::Html)] +#[case::html_full(".html", Selector::Html)] +#[case::image(".image", Selector::Image)] +#[case::image_ref(".image_ref", Selector::ImageRef)] +#[case::code(".code", Selector::Code)] +#[case::code_inline(".code_inline", Selector::InlineCode)] +#[case::math_inline(".math_inline", Selector::InlineMath)] +#[case::link(".link", Selector::Link)] +#[case::link_ref(".link_ref", Selector::LinkRef)] +#[case::list(".list", Selector::List(None, None))] +#[case::toml(".toml", Selector::Toml)] +#[case::strong(".strong", Selector::Strong)] +#[case::yaml(".yaml", Selector::Yaml)] +#[case::text(".text", Selector::Text)] +#[case::mdx_js_esm(".mdx_js_esm", Selector::MdxJsEsm)] +#[case::mdx_jsx_text_element(".mdx_jsx_text_element", Selector::MdxJsxTextElement)] +#[case::mdx_flow_expression(".mdx_flow_expression", Selector::MdxFlowExpression)] +fn test_parse_selector(#[case] selector_str: &str, #[case] expected_selector: Selector) { + let mut arena = Arena::new(10); + let token = Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(selector_str)), + module_id: 1.into(), + }; + + let tokens = [ + token.clone(), + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + + let result = Parser::new(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 { + assert_eq!(*selector, expected_selector); + } else { + panic!("Expected Selector expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[rstest] +#[case(".", Some(1), None, Selector::List(Some(1), None))] +#[case(".", Some(2), Some(3), Selector::Table(Some(2), Some(3)))] +fn test_parse_array_selector( + #[case] selector_str: &str, + #[case] first_idx: Option, + #[case] second_idx: Option, + #[case] expected_selector: Selector, +) { + let mut arena = Arena::new(10); + let mut tokens = vec![ + Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(selector_str)), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::LBracket, + module_id: 1.into(), + }, + ]; + + if let Some(idx) = first_idx { + tokens.push(Token { + range: Range::default(), + kind: TokenKind::NumberLiteral(idx.into()), + module_id: 1.into(), + }); + } + + tokens.push(Token { + range: Range::default(), + kind: TokenKind::RBracket, + module_id: 1.into(), + }); + + if second_idx.is_some() { + tokens.push(Token { + range: Range::default(), + kind: TokenKind::LBracket, + module_id: 1.into(), + }); + + if let Some(idx) = second_idx { + tokens.push(Token { + range: Range::default(), + kind: TokenKind::NumberLiteral(idx.into()), + module_id: 1.into(), + }); + } + + tokens.push(Token { + range: Range::default(), + kind: TokenKind::RBracket, + module_id: 1.into(), + }); + } + + tokens.push(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }); + + let result = Parser::new(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 { + assert_eq!(*selector, expected_selector); + } else { + panic!("Expected Selector expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +// ."key"[] or ."key"[n]: single property selector followed by array iterator/index +#[rstest] +#[case::all_elements(".\"items\"", None, vec![Selector::Property("items".into()), Selector::List(None, None)])] +#[case::index_0(".\"items\"", Some(0usize), vec![Selector::Property("items".into()), Selector::List(Some(0), None)])] +#[case::index_2(".\"items\"", Some(2usize), vec![Selector::Property("items".into()), Selector::List(Some(2), None)])] +fn test_parse_property_selector_iterator( + #[case] selector_str: &str, + #[case] index: Option, + #[case] expected: Vec, +) { + let mut arena = Arena::new(10); + let mut tokens = vec![ + Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(selector_str)), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::LBracket, + module_id: 1.into(), + }, + ]; + if let Some(idx) = index { + tokens.push(Token { + range: Range::default(), + kind: TokenKind::NumberLiteral(idx.into()), + module_id: 1.into(), + }); + } + tokens.push(Token { + range: Range::default(), + kind: TokenKind::RBracket, + module_id: 1.into(), + }); + tokens.push(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }); + + let result = Parser::new(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 { + 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 { + panic!("expected Selector, got {:?}", node.expr); + }; + assert_eq!(actual, sel); + } +} + +// ."a"."b"[] or ."a"."b"[n]: chained property selectors followed by array iterator/index +#[rstest] +#[case::all_elements(".\"a\"", ".\"b\"", None, vec![Selector::Property("a".into()), Selector::Property("b".into()), Selector::List(None, None)])] +#[case::index_0(".\"a\"", ".\"b\"", Some(0usize), vec![Selector::Property("a".into()), Selector::Property("b".into()), Selector::List(Some(0), None)])] +#[case::index_1(".\"a\"", ".\"b\"", Some(1usize), vec![Selector::Property("a".into()), Selector::Property("b".into()), Selector::List(Some(1), None)])] +fn test_parse_chained_property_selector_iterator( + #[case] first: &str, + #[case] second: &str, + #[case] index: Option, + #[case] expected: Vec, +) { + let mut arena = Arena::new(10); + let mut tokens = vec![ + Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(first)), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(second)), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::LBracket, + module_id: 1.into(), + }, + ]; + if let Some(idx) = index { + tokens.push(Token { + range: Range::default(), + kind: TokenKind::NumberLiteral(idx.into()), + module_id: 1.into(), + }); + } + tokens.push(Token { + range: Range::default(), + kind: TokenKind::RBracket, + module_id: 1.into(), + }); + tokens.push(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }); + + let result = Parser::new(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 { + 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 { + panic!("expected Selector, got {:?}", node.expr); + }; + assert_eq!(actual, sel); + } +} + +#[test] +fn test_parse_as_binding() { + let mut arena = Arena::new(10); + let tokens = [ + Token { + range: Range::default(), + kind: TokenKind::NumberLiteral(42.into()), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::As, + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Ident("x".into()), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + let result = Parser::new(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 { + panic!("expected Expr::As, got {:?}", program[0].expr); + }; + assert_eq!(ident.name.to_string(), "x"); + assert!(matches!(&inner.expr, Expr::Literal(Literal::Number(_)))); +} + +#[test] +fn test_parse_env() { + unsafe { std::env::set_var("MQ_TEST_VAR", "test_value") }; + let _guard = io_context::scoped(Shared::new( + crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true), + )); + + let mut arena = Arena::new(10); + let tokens = [ + Token { + range: Range::default(), + kind: TokenKind::Env("MQ_TEST_VAR".into()), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + + let result = Parser::new(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 { + assert_eq!(value, "test_value"); + } else { + panic!("Expected String literal, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[test] +fn test_parse_env_not_found() { + let _guard = io_context::scoped(Shared::new( + crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true), + )); + + let mut arena = Arena::new(10); + let token = Token { + range: Range::default(), + kind: TokenKind::Env("MQ_NONEXISTENT_VAR".into()), + module_id: 1.into(), + }; + + let tokens = [ + token.clone(), + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + + let result = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + + assert!(matches!( + result, + Err(SyntaxError::EnvNotFound(_, var)) if var == "MQ_NONEXISTENT_VAR" + )); +} + +#[test] +fn test_parse_env_in_arguments() { + unsafe { std::env::set_var("MQ_ARG_TEST", "env_arg_value") }; + let _guard = io_context::scoped(Shared::new( + crate::io::SandboxedIo::new(crate::io::NativeIo::default()).allow_env(true), + )); + + let mut arena = Arena::new(10); + let tokens = [ + Token { + range: Range::default(), + kind: TokenKind::Ident(SmolStr::new("function")), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::LParen, + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Env("MQ_ARG_TEST".into()), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::RParen, + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + + let result = Parser::new(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 { + assert_eq!(ident.name, "function".into()); + assert_eq!(args.len(), 1); + 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); + } + } else { + panic!("Expected Call expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[rstest] +#[case::lang(".lang", "lang")] +#[case::value(".value", "value")] +#[case::depth(".depth", "depth")] +fn test_parse_standalone_attr_selector(#[case] selector: &str, #[case] attribute: &str) { + let mut arena = Arena::new(10); + let tokens = [ + Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(selector)), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + + let result = Parser::new(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 { + 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_eq!(attr_str, attribute); + } else { + panic!("Expected String literal in second argument, got {:?}", args[1].expr); + } + } else { + panic!("Expected Call expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[rstest] +#[case::h_value(vec![".h", ".value"], "h", "value")] +#[case::h1_value(vec![".h1", ".value"], "h1", "value")] +#[case::code_lang(vec![".code", ".lang"], "code", "lang")] +#[case::text_value(vec![".text", ".value"], "text", "value")] +fn test_parse_selector_with_attribute( + #[case] selectors: Vec<&str>, + #[case] base_selector: &str, + #[case] attribute: &str, +) { + let mut arena = Arena::new(10); + let mut tokens = selectors + .iter() + .map(|selector| Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(selector)), + module_id: 1.into(), + }) + .collect::>(); + + tokens.push(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }); + + let result = Parser::new(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 { + // 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 { + match base_selector { + "h" => assert_eq!(*selector, Selector::Heading(None)), + "h1" => assert_eq!(*selector, Selector::Heading(Some(1))), + "code" => assert_eq!(*selector, Selector::Code), + "text" => assert_eq!(*selector, Selector::Text), + _ => panic!("Unexpected base selector: {}", base_selector), + } + } else { + panic!("Expected Selector expression in first argument, got {:?}", args[0].expr); + } + + // Second argument should be the attribute string + 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); + } + } else { + panic!("Expected Call expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[rstest] +// Two selectors: base, then a single descendant hop. +#[case::two_levels(vec![".blockquote", ".code"], vec![Selector::Blockquote, Selector::Code])] +// Three selectors: two descendant hops. +#[case::three_levels( + vec![".blockquote", ".list", ".code"], + vec![Selector::Blockquote, Selector::List(None, None), Selector::Code] + )] +fn test_parse_selector_descendant_chain(#[case] selectors: Vec<&str>, #[case] expected_selectors: Vec) { + let mut arena = Arena::new(10); + let mut tokens = selectors + .iter() + .map(|selector| Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(selector)), + module_id: 1.into(), + }) + .collect::>(); + tokens.push(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }); + + let result = Parser::new(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 { + // Desugars to `base | .. | step1 | .. | step2 | ...`, i.e. a plain + // selector interleaved with a `Recursive` selector between each hop. + let expected: Vec = expected_selectors + .into_iter() + .enumerate() + .flat_map(|(i, sel)| { + if i == 0 { + vec![sel] + } else { + vec![Selector::Recursive, sel] + } + }) + .collect(); + assert_eq!(nodes.len(), expected.len()); + for (node, expected_sel) in nodes.iter().zip(expected.iter()) { + if let Expr::Selector(sel) = &node.expr { + assert_eq!(sel, expected_sel); + } else { + panic!("Expected Selector expression, got {:?}", node.expr); + } + } + } else { + panic!("Expected Block expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[test] +fn test_parse_selector_descendant_chain_trailing_attribute() { + // `.blockquote .code.lang` โ†’ attr(Block([.blockquote, .., .code]), "lang") + let mut arena = Arena::new(10); + let selectors = [".blockquote", ".code", ".lang"]; + let mut tokens = selectors + .iter() + .map(|selector| Token { + range: Range::default(), + kind: TokenKind::Selector(SmolStr::new(*selector)), + module_id: 1.into(), + }) + .collect::>(); + tokens.push(Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }); + + let result = Parser::new(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 { + 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")); + } else { + panic!("Expected Call expression, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[rstest] +#[case::match_simple_literal( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("one".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("two".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("other".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Literal(Literal::Number(1.into())), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("one".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Literal(Literal::Number(2.into())), + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Expr::Literal(Literal::String("two".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 11.into(), + expr: Expr::Literal(Literal::String("other".to_owned())) + }) + } + ] + ) + }) + ]))] +#[case::match_type_pattern( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("value"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("string"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("is string".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("number"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("is number".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("value", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Type(Ident::new("string")), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("is string".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Type(Ident::new("number")), + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Expr::Literal(Literal::String("is number".to_owned())) + }) + } + ] + ) + }) + ]))] +#[case::match_array_pattern( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("y"))), + token(TokenKind::RBracket), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("two elements".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Array(vec![ + Pattern::Ident(IdentWithToken::new("x")), + Pattern::Ident(IdentWithToken::new("y")) + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("two elements".to_owned())) + }) + } + ] + ) + }) + ]))] +#[case::match_array_rest_pattern( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("arr"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::LBracket), + token(TokenKind::Ident(SmolStr::new("first"))), + token(TokenKind::Comma), + token(TokenKind::DoubleDot), + token(TokenKind::Ident(SmolStr::new("rest"))), + token(TokenKind::RBracket), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("first"))), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("arr", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::ArrayRest( + vec![Pattern::Ident(IdentWithToken::new("first"))], + IdentWithToken::new("rest") + ), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("first", Some(5.into()))) + }) + } + ] + ) + }) + ]))] +#[case::match_dict_pattern( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("obj"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::LBrace), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::Comma), + token(TokenKind::Ident(SmolStr::new("age"))), + token(TokenKind::RBrace), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("name"))), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("obj", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Dict(vec![ + (IdentWithToken::new("name"), Pattern::Ident(IdentWithToken::new("name"))), + (IdentWithToken::new("age"), Pattern::Ident(IdentWithToken::new("age"))) + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("name", Some(5.into()))) + }) + } + ] + ) + }) + ]))] +#[case::match_with_guard( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("n"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Gt), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("positive".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::If), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::Lt), + token(TokenKind::NumberLiteral(0.into())), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("negative".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("zero".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("n", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Ident(IdentWithToken::new("x")), + guard: Some(Shared::new(Node { + token_id: 5.into(), + expr: Expr::BinaryOp( + BinaryOp::Gt, + Shared::new(Node { + token_id: 4.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(4.into()))) + }), + Shared::new(Node { + token_id: 6.into(), + expr: Expr::Literal(Literal::Number(0.into())) + }) + ) + })), + body: Shared::new(Node { + token_id: 8.into(), + 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: Expr::BinaryOp( + BinaryOp::Lt, + Shared::new(Node { + token_id: 10.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(10.into()))) + }), + Shared::new(Node { + token_id: 12.into(), + expr: Expr::Literal(Literal::Number(0.into())) + }) + ) + })), + body: Shared::new(Node { + token_id: 14.into(), + expr: Expr::Literal(Literal::String("negative".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 17.into(), + expr: Expr::Literal(Literal::String("zero".to_owned())) + }) + } + ] + ) + }) + ]))] +#[case::match_do_end( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::RParen), + token(TokenKind::Do), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("one".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("two".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("other".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof) + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Literal(Literal::Number(2.into())) + }), + vec![ + MatchArm { + pattern: Pattern::Literal(Literal::Number(1.into())), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("one".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Literal(Literal::Number(2.into())), + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Expr::Literal(Literal::String("two".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 11.into(), + expr: Expr::Literal(Literal::String("other".to_owned())) + }) + } + ] + ) + }) + ]))] +// --- or-pattern parser tests --- +// Arena allocation order for match(x): COLON | pattern COLON body ... +// id 0: Match, id 1: Ident("x"), id 2: Colon (consume_colon_or_do) +// per arm: id N: Pipe, id N+1: Colon, id N+2: body literal +#[case::match_or_two_literal_numbers( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Or), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("matched".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("other".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Or(vec![ + Pattern::Literal(Literal::Number(1.into())), + Pattern::Literal(Literal::Number(2.into())), + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("matched".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Expr::Literal(Literal::String("other".to_owned())) + }) + }, + ] + ) + }) + ]))] +#[case::match_or_string_literals( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::StringLiteral("a".to_owned())), + token(TokenKind::Or), + token(TokenKind::StringLiteral("b".to_owned())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("matched".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("other".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Or(vec![ + Pattern::Literal(Literal::String("a".to_owned())), + Pattern::Literal(Literal::String("b".to_owned())), + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("matched".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Expr::Literal(Literal::String("other".to_owned())) + }) + }, + ] + ) + }) + ]))] +#[case::match_or_type_patterns( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("string"))), + token(TokenKind::Or), + token(TokenKind::Colon), + token(TokenKind::Ident(SmolStr::new("number"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("str or num".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Or(vec![ + Pattern::Type(Ident::new("string")), + Pattern::Type(Ident::new("number")), + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("str or num".to_owned())) + }) + }, + ] + ) + }) + ]))] +#[case::match_or_three_alternatives( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::NumberLiteral(1.into())), + token(TokenKind::Or), + token(TokenKind::NumberLiteral(2.into())), + token(TokenKind::Or), + token(TokenKind::NumberLiteral(3.into())), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("matched".to_owned())), + token(TokenKind::Pipe), + token(TokenKind::Ident(SmolStr::new("_"))), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("other".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Or(vec![ + Pattern::Literal(Literal::Number(1.into())), + Pattern::Literal(Literal::Number(2.into())), + Pattern::Literal(Literal::Number(3.into())), + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + expr: Expr::Literal(Literal::String("matched".to_owned())) + }) + }, + MatchArm { + pattern: Pattern::Wildcard, + guard: None, + body: Shared::new(Node { + token_id: 8.into(), + expr: Expr::Literal(Literal::String("other".to_owned())) + }) + }, + ] + ) + }) + ]))] +#[case::match_or_bool_literals( + vec![ + token(TokenKind::Match), + token(TokenKind::LParen), + token(TokenKind::Ident(SmolStr::new("x"))), + token(TokenKind::RParen), + token(TokenKind::Colon), + token(TokenKind::Pipe), + token(TokenKind::BoolLiteral(true)), + token(TokenKind::Or), + token(TokenKind::BoolLiteral(false)), + token(TokenKind::Colon), + token(TokenKind::StringLiteral("bool".to_owned())), + token(TokenKind::End), + token(TokenKind::Eof), + ], + Ok(vec![ + Shared::new(Node { + token_id: 0.into(), + expr: Expr::Match( + Shared::new(Node { + token_id: 1.into(), + expr: Expr::Ident(IdentWithToken::new_with_token("x", Some(1.into()))) + }), + vec![ + MatchArm { + pattern: Pattern::Or(vec![ + Pattern::Literal(Literal::Bool(true)), + Pattern::Literal(Literal::Bool(false)), + ]), + guard: None, + body: Shared::new(Node { + token_id: 5.into(), + 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 result = Parser::new(input.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse(); + assert_eq!(result, expected); +} + +#[rstest] +#[case::expr_literal( + vec![ + lexer::token::StringSegment::Text("Value: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("42".into(), Range::default()), + ], + 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(_)))) + } + )] +#[case::expr_string( + vec![ + lexer::token::StringSegment::Text("Result: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("\"hello\"".into(), Range::default()), + ], + 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")) + } + )] +#[case::expr_call( + vec![ + lexer::token::StringSegment::Text("Result: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("add(1, 2)".into(), Range::default()), + ], + 2, + |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 { + ident.name == "add".into() && args.len() == 2 + } else { + false + } + } else { + false + } + } + )] +#[case::expr_self( + vec![ + lexer::token::StringSegment::Text("Value: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("self".into(), Range::default()), + ], + 2, + |segments: &[StringSegment]| { + matches!(&segments[0], StringSegment::Text(s) if s == "Value: ") && + matches!(&segments[1], StringSegment::Self_) + } + )] +#[case::expr_env( + vec![ + lexer::token::StringSegment::Text("Env: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("$MQ_TEST_INTERPOLATION".into(), Range::default()), + ], + 2, + |segments: &[StringSegment]| { + unsafe { std::env::set_var("MQ_TEST_INTERPOLATION", "test_value") }; + matches!(&segments[0], StringSegment::Text(s) if s == "Env: ") && + matches!(&segments[1], StringSegment::Env(var) if var == "MQ_TEST_INTERPOLATION") + } + )] +#[case::multiple_exprs( + vec![ + lexer::token::StringSegment::Text("A: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("1".into(), Range::default()), + lexer::token::StringSegment::Text(", B: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("2".into(), Range::default()), + ], + 4, + |segments: &[StringSegment]| { + matches!(&segments[0], StringSegment::Text(s) if s == "A: ") && + matches!(&segments[1], StringSegment::Expr(_)) && + matches!(&segments[2], StringSegment::Text(s) if s == ", B: ") && + matches!(&segments[3], StringSegment::Expr(_)) + } + )] +#[case::mixed_segments( + vec![ + lexer::token::StringSegment::Text("Literal, ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("self".into(), Range::default()), + lexer::token::StringSegment::Text(", ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("$MQ_MIXED_TEST".into(), Range::default()), + lexer::token::StringSegment::Text(", ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("42".into(), Range::default()), + ], + 6, + |segments: &[StringSegment]| { + unsafe { std::env::set_var("MQ_MIXED_TEST", "env_value") }; + matches!(&segments[0], StringSegment::Text(s) if s == "Literal, ") && + matches!(&segments[1], StringSegment::Self_) && + matches!(&segments[2], StringSegment::Text(s) if s == ", ") && + matches!(&segments[3], StringSegment::Env(var) if var == "MQ_MIXED_TEST") && + matches!(&segments[4], StringSegment::Text(s) if s == ", ") && + matches!(&segments[5], StringSegment::Expr(_)) + } + )] +#[case::expr_bool( + vec![ + lexer::token::StringSegment::Text("Bool: ".to_string(), Range::default()), + lexer::token::StringSegment::Expr("true".into(), Range::default()), + ], + 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)))) + } + )] +fn test_parse_interpolated_string_with_expr( + #[case] input_segments: Vec, + #[case] expected_len: usize, + #[case] validator: fn(&[StringSegment]) -> bool, +) { + let mut arena = Arena::new(10); + let tokens = [ + Token { + range: Range::default(), + kind: TokenKind::InterpolatedString(input_segments), + module_id: 1.into(), + }, + Token { + range: Range::default(), + kind: TokenKind::Eof, + module_id: 1.into(), + }, + ]; + + let result = Parser::new(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 { + assert_eq!(segments.len(), expected_len); + assert!(validator(segments), "Validator failed for segments"); + } else { + panic!("Expected InterpolatedString, got {:?}", program[0].expr); + } + } + Err(err) => panic!("Parse error: {:?}", err), + } +} + +#[test] +fn test_interpolated_string_inner_syntax_error_is_propagated() { + let result = parse_source("let a = 1\n| s\"a ${ 1 + } b\""); + match result { + Err(SyntaxError::UnexpectedEOFAfterToken(token)) => { + assert_eq!(token.kind, TokenKind::Plus); + assert_eq!(token.range.start, Position { line: 2, column: 12 }); + } + other => panic!("expected the inner UnexpectedEOFAfterToken, got {:?}", other), + } +} + +/// For each argument of the top-level call, the statement count of its body if it is a lambda. +fn lambda_arg_body_lens(source: &str) -> Vec> { + let program = parse_source(source).expect("source should parse"); + let [node] = program.as_slice() else { + panic!("expected a single top-level expression"); + }; + let Expr::Call(_, args) = &node.expr else { + panic!("expected a call, got {:?}", node.expr); + }; + args.iter() + .map(|arg| match &arg.expr { + Expr::Fn(_, body) => Some(body.len()), + _ => None, + }) + .collect() +} + +#[rstest] +#[case::only_arg("f(fn(x): x + 1)", vec![Some(1)])] +#[case::arrow("f(->(x): x + 1)", vec![Some(1)])] +#[case::last_arg("f(a, fn(x): x + 1)", vec![None, Some(1)])] +#[case::multiple_params("f(a, 0, fn(acc, x): acc + x)", vec![None, None, Some(1)])] +#[case::ends_at_comma("f(fn(x): x, 1)", vec![Some(1), None])] +#[case::lambda_between_args("f(a, fn(x): x, b)", vec![None, Some(1), None])] +#[case::two_lambdas("f(fn(x): x + 1, fn(x): x * 2)", vec![Some(1), Some(1)])] +#[case::pipe_stays_in_body("f(fn(x): x | g(.h))", vec![Some(2)])] +#[case::pipe_before_comma("f(fn(x): x | g(.h), 1)", vec![Some(2), None])] +#[case::call_with_commas_in_body("f(fn(x): g(x, 1, 2), 3)", vec![Some(1), None])] +#[case::array_in_body("f(fn(x): [x, 1], 2)", vec![Some(1), None])] +#[case::dict_in_body("f(fn(x): {\"a\": x, \"b\": 1}, 2)", vec![Some(1), None])] +#[case::paren_in_body("f(fn(x): (x + 1) * 2, 3)", vec![Some(1), None])] +#[case::if_else_in_body("f(fn(x): if (x): 1 else: 2, 3)", vec![Some(1), None])] +#[case::elif_in_body("f(fn(x): if (x): 1 elif (y): 2 else: 3)", vec![Some(1)])] +#[case::do_end_in_body("f(fn(x): do 1 | 2 end, 3)", vec![Some(1), None])] +#[case::foreach_in_body("f(fn(x): foreach (i, x): i end, 3)", vec![Some(1), None])] +#[case::match_in_body("f(fn(x): match (x): | 1: 2 | _: 3 end, 4)", vec![Some(1), None])] +#[case::nested_call_lambda("f(fn(x): g(x, fn(y): y + 1))", vec![Some(1)])] +#[case::nested_call_lambda_then_comma("f(fn(x): g(x, fn(y): y + 1), 2)", vec![Some(1), None])] +#[case::let_in_body("f(fn(x): let y = x + 1 | y, 2)", vec![Some(2), None])] +#[case::lambda_in_param_default("f(fn(x = g(fn(y): y)): x, 1)", vec![Some(1), None])] +#[case::semicolon_form("f(fn(x): x + 1;)", vec![Some(1)])] +#[case::semicolon_form_then_arg("f(fn(x): x; , 1)", vec![Some(1), None])] +#[case::end_form("f(fn(x): do x end; , 1)", vec![Some(1), None])] +#[case::inner_end_then_semicolon("f(fn(x): foreach (i, x): i end;)", vec![Some(1)])] +fn test_lambda_in_call_args_may_omit_terminator(#[case] source: &str, #[case] expected: Vec>) { + assert_eq!(lambda_arg_body_lens(source), expected); +} + +// Body lengths exclude the leading `fn:` argument statement. +#[rstest] +#[case::only_arg("f(fn: self + 1)", vec![Some(1)])] +#[case::last_arg("f(a, fn: self + 1)", vec![None, Some(1)])] +#[case::ends_at_comma("f(fn: self, 1)", vec![Some(1), None])] +#[case::pipe_stays_in_body("f(fn: g(1) | h(2))", vec![Some(2)])] +#[case::pipe_before_comma("f(fn: g | h, 1)", vec![Some(2), None])] +#[case::call_with_commas_in_body("f(fn: g(1, 2), 3)", vec![Some(1), None])] +#[case::nested("f(fn: g(fn: self + 1))", vec![Some(1)])] +#[case::nested_then_comma("f(fn: g(fn: self + 1), 2)", vec![Some(1), None])] +#[case::if_else_in_body("f(fn: if (self): 1 else: 2, 3)", vec![Some(1), None])] +#[case::semicolon_form("f(fn: self + 1;)", vec![Some(1)])] +#[case::end_form("f(fn: do self end;, 1)", vec![Some(1), None])] +fn test_lambda_without_params(#[case] source: &str, #[case] expected: Vec>) { + let expected: Vec<_> = expected.into_iter().map(|len| len.map(|len| len + 1)).collect(); + assert_eq!(lambda_arg_body_lens(source), expected); +} + +#[test] +fn test_lambda_without_params_takes_one_argument() { + let program = parse_source("f(fn: self, fn(): 1)").expect("source should parse"); + let Expr::Call(_, args) = &program[0].expr else { + panic!("expected a call"); + }; + let Expr::Fn(params, body) = &args[0].expr else { + panic!("expected a function"); + }; + let [param] = params.as_slice() else { + panic!("expected one parameter"); + }; + assert!(matches!(&body[0].expr, Expr::Ident(ident) if ident.name == param.ident.name)); + assert!(matches!(&args[1].expr, Expr::Fn(params, _) if params.is_empty())); +} + +#[test] +fn test_lambda_without_params_outside_call_args_with_terminator() { + let program = parse_source("let g = fn: self + 1;").expect("source should parse"); + assert!(matches!(&program[0].expr, Expr::Let(..))); +} + +#[rstest] +#[case::let_value_without_terminator("let g = fn: self + 1)")] +#[case::empty_body("f(fn: )")] +#[case::empty_body_then_comma("f(fn:, 1)")] +#[case::unclosed_call("f(fn: self")] +#[case::arrow_has_no_paramless_form("f(->: self)")] +#[case::missing_colon("f(fn self)")] +fn test_lambda_without_params_reports_malformed_input(#[case] source: &str) { + assert!(parse_source(source).is_err(), "{source} should not parse"); +} + +#[rstest] +#[case::let_value("let g = fn(x): x + 1)")] +#[case::empty_body("f(fn(x): )")] +#[case::empty_body_then_comma("f(fn(x):, 1)")] +#[case::dangling_operator("f(fn(x): x +)")] +#[case::unclosed_call("f(fn(x): x + 1")] +#[case::stray_close_paren("f(fn(x): x + 1))")] +fn test_lambda_reports_malformed_input(#[case] source: &str) { + assert!(parse_source(source).is_err(), "{source} should not parse"); +} + +#[rstest] +#[case::paren_group("(fn(x): x + 1)(2)")] +#[case::array_element("[fn(x): x, 1]")] +#[case::implicit_array_element("[fn: self, 1]")] +#[case::dict_value("{\"a\": fn(x): x, \"b\": 1}")] +#[case::lambda_body_tail("f(fn(x): fn(y): x + y)")] +#[case::after_pipe("f(1 | fn(x): x + 1, 2)")] +fn test_lambda_ends_at_the_closer_of_its_enclosing_construct(#[case] source: &str) { + parse_source(source).expect("source should parse"); +} + +#[test] +fn test_lambda_array_element_ends_before_comma() { + let program = parse_source("[fn(x): x + 1, 2]").expect("source should parse"); + let Expr::Array(elements) = &program[0].expr else { + panic!("expected an array"); + }; + assert_eq!(elements.len(), 2); + assert!(matches!(&elements[0].expr, Expr::Fn(..))); +} + +#[rstest] +#[case::empty_body("f(fn(x): )", TokenKind::RParen)] +#[case::empty_body_then_comma("f(fn(x):, 1)", TokenKind::Comma)] +fn test_lambda_in_call_args_with_empty_body_reports_the_delimiter(#[case] source: &str, #[case] kind: TokenKind) { + match parse_source(source) { + Err(SyntaxError::UnexpectedToken(token)) => assert_eq!(token.kind, kind), + other => panic!("expected UnexpectedToken, got {other:?}"), + } +} + +#[test] +fn test_unclosed_call_after_unterminated_lambda_reports_missing_paren() { + assert!(matches!( + parse_source("f(fn(x): x + 1"), + Err(SyntaxError::ExpectedClosingParen(..)) + )); +} + +const STACK_KIB: usize = 2048; + +fn parse_source(source: &str) -> Result { + let tokens = Lexer::new(lexer::Options::default()) + .tokenize(source, Module::TOP_LEVEL_MODULE_ID) + .expect("source should tokenize"); + let mut arena = Arena::new(16); + Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse() +} + +/// Runs `f` on a 2 MiB stack (the default for spawned and tokio worker threads) in release +/// builds. Debug frames are much larger, so debug builds get more room. +fn on_small_stack(f: impl FnOnce() -> T + Send + 'static) -> T { + let stack_size = if cfg!(debug_assertions) { 64 * 1024 } else { STACK_KIB } * 1024; + std::thread::Builder::new() + .stack_size(stack_size) + .spawn(f) + .unwrap() + .join() + .expect("parser must not overflow the stack") +} + +#[rstest] +#[case::paren("(", "1", ")")] +#[case::array("[", "1", "]")] +#[case::dict("{\"a\": ", "1", "}")] +#[case::not("!", "true", "")] +#[case::block("do ", "1", " end")] +#[case::if_else("if (true): ", "1", " else: 2")] +#[case::fn_body("fn(): ", "1", "; ")] +#[case::call("f(", "1", ")")] +fn test_nesting_within_limit_is_accepted(#[case] open: &str, #[case] core: &str, #[case] close: &str) { + let source = format!( + "{}{}{}", + open.repeat(MAX_PARSE_DEPTH - 1), + core, + close.repeat(MAX_PARSE_DEPTH - 1) + ); + let result = on_small_stack(move || parse_source(&source).map(|_| ())); + assert!( + !matches!(result, Err(SyntaxError::TooDeeplyNested(..))), + "nesting of {} should be accepted, got {:?}", + MAX_PARSE_DEPTH - 1, + result + ); +} + +#[rstest] +#[case::paren("(", "1", ")")] +#[case::array("[", "1", "]")] +#[case::dict("{\"a\": ", "1", "}")] +#[case::not("!", "true", "")] +#[case::block("do ", "1", " end")] +#[case::if_else("if (true): ", "1", " else: 2")] +#[case::fn_body("fn(): ", "1", "; ")] +#[case::call("f(", "1", ")")] +fn test_nesting_beyond_limit_returns_error(#[case] open: &str, #[case] core: &str, #[case] close: &str) { + let n = MAX_PARSE_DEPTH * 2; + let source = format!("{}{}{}", open.repeat(n), core, close.repeat(n)); + let result = on_small_stack(move || parse_source(&source).map(|_| ())); + assert!( + matches!(result, Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH))), + "expected TooDeeplyNested, got {:?}", + result + ); +} + +#[rstest] +#[case::let_array("let ", "[", "x", "]", " = 1")] +#[case::let_dict("let ", "{a: ", "x", "}", " = 1")] +#[case::match_array("match (1): | ", "[", "x", "]", ": 1 end")] +fn test_pattern_nesting_beyond_limit_returns_error( + #[case] prefix: &str, + #[case] open: &str, + #[case] core: &str, + #[case] close: &str, + #[case] suffix: &str, +) { + let n = MAX_PARSE_DEPTH * 2; + let source = format!("{prefix}{}{core}{}{suffix}", open.repeat(n), close.repeat(n)); + let result = on_small_stack(move || parse_source(&source).map(|_| ())); + assert!( + matches!(result, Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH))), + "expected TooDeeplyNested, got {:?}", + result + ); +} + +#[test] +fn test_long_index_chain_returns_error() { + let source = format!("a{}", "[0]".repeat(MAX_PARSE_DEPTH * 2)); + let result = on_small_stack(move || parse_source(&source).map(|_| ())); + assert!( + matches!(result, Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH))), + "expected TooDeeplyNested, got {:?}", + result + ); +} + +#[test] +fn test_interpolated_expr_nesting_reports_nesting_error() { + let n = MAX_PARSE_DEPTH * 2; + let source = format!("s\"${{{}1{}}}\"", "(".repeat(n), ")".repeat(n)); + let prefix_len = "s\"".len(); + let result = on_small_stack(move || parse_source(&source).map(|_| ())); + match result { + Err(SyntaxError::TooDeeplyNested(token, _)) => { + assert!( + token.range.start.column >= prefix_len, + "range should point into the original source, got {:?}", + token.range + ); + } + other => panic!("expected TooDeeplyNested, got {:?}", other), + } +} + +#[test] +fn test_nesting_limit_boundary() { + let at_limit = format!( + "{}1{}", + "(".repeat(MAX_PARSE_DEPTH - 1), + ")".repeat(MAX_PARSE_DEPTH - 1) + ); + let over_limit = format!("{}1{}", "(".repeat(MAX_PARSE_DEPTH), ")".repeat(MAX_PARSE_DEPTH)); + let (at_limit_ok, over_limit_err) = on_small_stack(move || { + ( + parse_source(&at_limit).is_ok(), + matches!( + parse_source(&over_limit), + Err(SyntaxError::TooDeeplyNested(_, MAX_PARSE_DEPTH)) + ), + ) + }); + assert!(at_limit_ok); + assert!(over_limit_err); +} + +#[test] +fn test_depth_is_restored_after_each_expression() { + let nested = format!( + "{}1{}", + "(".repeat(MAX_PARSE_DEPTH - 1), + ")".repeat(MAX_PARSE_DEPTH - 1) + ); + let source = format!("{nested} | {nested} | {nested}"); + assert!(on_small_stack(move || parse_source(&source).is_ok())); +} + +/// Renders `node` fully parenthesized with the source symbol of each operator. +fn shape(node: &Node, arena: &Arena) -> String { + fn compound_rhs(node: &Node) -> &Node { + match &node.expr { + Expr::BinaryOp(_, _, rhs) => rhs, + Expr::Call(_, args) if args.len() == 2 => &args[1], + Expr::Call(_, args) if args.len() == 1 => compound_rhs(&args[0]), + _ => node, + } + } + + let operator = arena[node.token_id].to_string(); + let fold = |operands: &[Shared], symbol: &str| { + operands + .iter() + .map(|operand| shape(operand, arena)) + .reduce(|lhs, rhs| format!("({lhs} {symbol} {rhs})")) + .unwrap() + }; + + match &node.expr { + Expr::Ident(ident) => ident.name.to_string(), + Expr::Literal(literal) => literal.to_string(), + Expr::Paren(inner) => format!("<{}>", shape(inner, arena)), + Expr::UnaryOp(op, operand) => format!("({op} {})", shape(operand, arena)), + Expr::BinaryOp(_, lhs, rhs) => format!("({} {operator} {})", shape(lhs, arena), shape(rhs, arena)), + Expr::Call(_, args) if args.len() == 2 && arena[node.token_id].kind.binary_op_precedence().is_some() => { + format!("({} {operator} {})", shape(&args[0], arena), shape(&args[1], arena)) + } + Expr::And(operands) => fold(operands, "&&"), + Expr::Or(operands) => fold(operands, "||"), + Expr::Assign(ident, rhs) if operator == "=" => format!("({} = {})", ident.name, shape(rhs, arena)), + Expr::Assign(ident, rhs) => format!("({} {operator} {})", ident.name, shape(compound_rhs(rhs), arena)), + Expr::As(ident, expr) => format!("({} as {})", shape(expr, arena), ident.name), + other => panic!("shape does not support {other:?}"), + } +} + +fn parse_with_arena(code: &str) -> (Program, Arena) { + let tokens = Lexer::new(lexer::Options::default()) + .tokenize(code, Module::TOP_LEVEL_MODULE_ID) + .expect("code should tokenize"); + let mut arena = Arena::new(16); + let program = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID) + .parse() + .expect("code should parse"); + (program, arena) +} + +#[test] +fn test_ident_token_resolves_to_its_lexer_token() { + let (program, arena) = parse_with_arena("def foo(a): a;"); + let Expr::Def(name, params, body) = &program[0].expr else { + panic!("expected def"); + }; + + let name_token = &arena[name.token.expect("def name keeps its token")]; + assert_eq!(name_token.kind, TokenKind::Ident("foo".into())); + assert_eq!(name_token.range.start.column, 5); + + let param_token = &arena[params[0].ident.token.expect("param keeps its token")]; + assert_eq!(param_token.kind, TokenKind::Ident("a".into())); + assert_eq!(param_token.range.start.column, 9); + + let Expr::Ident(ident) = &body[0].expr else { + panic!("expected ident"); + }; + assert_eq!(ident.token, Some(body[0].token_id), "an ident shares its node's entry"); +} + +#[test] +fn test_nodes_built_from_one_lexer_token_have_distinct_ids() { + let (program, _) = parse_with_arena("arr[0](1)"); + let Expr::CallDynamic(callee, _) = &program[0].expr else { + panic!("expected dynamic call"); + }; + assert_ne!(program[0].token_id, callee.token_id); +} + +#[rstest] +#[case::self_("self.url")] +#[case::ident("v.url")] +#[case::call("f().url")] +fn test_attr_call_and_its_literal_have_distinct_ids(#[case] code: &str) { + let (program, arena) = parse_with_arena(code); + let Expr::Call(attr, args) = &program[0].expr else { + panic!("expected attr call"); + }; + assert_eq!(attr.name, Ident::new(constants::builtins::ATTR)); + assert_ne!(program[0].token_id, args[1].token_id); + assert_eq!( + attr.token, + Some(args[1].token_id), + "attr resolves to the selector token" + ); + assert_eq!(arena[args[1].token_id].kind, TokenKind::Selector(".url".into())); +} + +fn parse_shape(code: &str) -> Result { + let tokens = Lexer::new(lexer::Options::default()) + .tokenize(code, Module::TOP_LEVEL_MODULE_ID) + .expect("code should tokenize"); + let mut arena = Arena::new(16); + let program = Parser::new(tokens.iter(), &mut arena, Module::TOP_LEVEL_MODULE_ID).parse()?; + assert_eq!(program.len(), 1, "expected one expression for {code:?}"); + Ok(shape(&program[0], &arena)) +} + +#[rstest] +#[case::add("a + b", "(a + b)")] +#[case::sub("a - b", "(a - b)")] +#[case::mul("a * b", "(a * b)")] +#[case::div("a / b", "(a / b)")] +#[case::modulo("a % b", "(a % b)")] +#[case::eq("a == b", "(a == b)")] +#[case::ne("a != b", "(a != b)")] +#[case::lt("a < b", "(a < b)")] +#[case::lte("a <= b", "(a <= b)")] +#[case::gt("a > b", "(a > b)")] +#[case::gte("a >= b", "(a >= b)")] +#[case::and("a && b", "(a && b)")] +#[case::or("a || b", "(a || b)")] +#[case::left_shift("a << b", "(a << b)")] +#[case::right_shift("a >> b", "(a >> b)")] +#[case::convert("a @ b", "(a @ b)")] +#[case::range("a .. b", "(a .. b)")] +#[case::coalesce("a ?? b", "(a ?? b)")] +#[case::regex_match("a =~ b", "(a =~ b)")] +#[case::not_regex_match("a !~ b", "(a !~ b)")] +fn test_every_binary_operator_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), Ok(expected.to_string())); +} + +#[rstest] +#[case::and_over_or("a || b && c", "(a || (b && c))")] +#[case::or_after_and("a && b || c", "((a && b) || c)")] +#[case::comparison_over_and("a && b == c", "(a && (b == c))")] +#[case::and_after_comparison("a == b && c", "((a == b) && c)")] +#[case::additive_over_comparison("a == b + c", "(a == (b + c))")] +#[case::comparison_after_additive("a + b == c", "((a + b) == c)")] +#[case::multiplicative_over_additive("a + b * c", "(a + (b * c))")] +#[case::additive_after_multiplicative("a * b + c", "((a * b) + c)")] +#[case::range_over_multiplicative("a * b .. c", "(a * (b .. c))")] +#[case::multiplicative_after_range("a .. b * c", "((a .. b) * c)")] +#[case::coalesce_over_multiplicative("a * b ?? c", "(a * (b ?? c))")] +#[case::multiplicative_after_coalesce("a ?? b * c", "((a ?? b) * c)")] +#[case::convert_with_multiplicative("a + b @ c", "(a + (b @ c))")] +#[case::convert_is_left_associative_with_mul("a * b @ c", "((a * b) @ c)")] +#[case::shift_with_additive("a << b + c", "((a << b) + c)")] +#[case::additive_with_shift("a + b >> c", "((a + b) >> c)")] +#[case::regex_match_with_comparison("a =~ b == c", "((a =~ b) == c)")] +#[case::regex_over_and("a !~ b && c", "((a !~ b) && c)")] +#[case::three_levels("a || b && c == d + e * f .. g", "(a || (b && (c == (d + (e * (f .. g))))))")] +#[case::three_levels_reversed("a .. b * c + d == e && f || g", "((((((a .. b) * c) + d) == e) && f) || g)")] +fn test_binary_op_precedence_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), Ok(expected.to_string())); +} + +#[rstest] +#[case::or("a || b || c", "((a || b) || c)")] +#[case::and("a && b && c", "((a && b) && c)")] +#[case::comparison("a == b == c", "((a == b) == c)")] +#[case::mixed_comparison("a < b != c", "((a < b) != c)")] +#[case::sub("a - b - c", "((a - b) - c)")] +#[case::add_then_sub("a + b - c", "((a + b) - c)")] +#[case::shifts("a << b >> c", "((a << b) >> c)")] +#[case::div("a / b / c", "((a / b) / c)")] +#[case::mul_div_mod("a * b / c % d", "(((a * b) / c) % d)")] +#[case::range("a .. b .. c", "((a .. b) .. c)")] +#[case::coalesce("a ?? b ?? c", "((a ?? b) ?? c)")] +#[case::range_then_coalesce("a .. b ?? c", "((a .. b) ?? c)")] +#[case::coalesce_then_range("a ?? b .. c", "((a ?? b) .. c)")] +#[case::convert("a @ b @ c", "((a @ b) @ c)")] +fn test_binary_op_associativity_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), Ok(expected.to_string())); +} + +#[rstest] +#[case::parens_override_precedence("(a + b) * c", "(<(a + b)> * c)")] +#[case::parens_on_the_right("a * (b + c)", "(a * <(b + c)>)")] +#[case::parens_override_associativity("a - (b - c)", "(a - <(b - c)>)")] +#[case::neg_binds_tighter_than_mul("-a * b", "((- a) * b)")] +#[case::not_binds_tighter_than_eq("!a == b", "((! a) == b)")] +#[case::not_binds_tighter_than_and("!a && !b", "((! a) && (! b))")] +#[case::unary_on_the_right("a - -b", "(a - (- b))")] +#[case::not_on_the_right("a * !b", "(a * (! b))")] +#[case::as_binds_the_whole_expression("a + b * c as x", "((a + (b * c)) as x)")] +#[case::as_after_logic("a && b as x", "((a && b) as x)")] +fn test_operand_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), Ok(expected.to_string())); +} + +#[rstest] +#[case::assign("x = a", "(x = a)")] +#[case::assign_is_lowest("x = a || b && c == d + e * f", "(x = (a || (b && (c == (d + (e * f))))))")] +#[case::assign_takes_the_whole_expression("x = a + b * c", "(x = (a + (b * c)))")] +#[case::plus_assign("x += a * b", "(x += (a * b))")] +#[case::minus_assign("x -= a * b", "(x -= (a * b))")] +#[case::mul_assign("x *= a + b", "(x *= (a + b))")] +#[case::div_assign("x /= a + b", "(x /= (a + b))")] +#[case::mod_assign("x %= a + b", "(x %= (a + b))")] +#[case::floor_div_assign("x //= a + b", "(x //= (a + b))")] +fn test_assignment_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), Ok(expected.to_string())); +} + +#[rstest] +#[case::chained_assignment("a = b = c")] +#[case::assign_to_expression("a + b = c")] +#[case::assign_to_logic("a || b = c")] +#[case::pipe_assign_outside_a_binding("a |= b")] +#[case::missing_right_operand("a +")] +#[case::missing_right_operand_after_assign("a =")] +#[case::missing_right_operand_after_logic("a &&")] +#[case::operator_only("+")] +#[case::as_without_name("a as")] +#[case::as_with_non_identifier("a as 1")] +fn test_binary_op_errors(#[case] code: &str) { + assert!(parse_shape(code).is_err(), "expected an error for {code:?}"); +} + +#[test] +fn test_code_generation_precedence_matches_the_parser() { + let kinds = [ + TokenKind::Plus, + TokenKind::Minus, + TokenKind::Asterisk, + TokenKind::Slash, + TokenKind::Percent, + TokenKind::EqEq, + TokenKind::NeEq, + TokenKind::Lt, + TokenKind::Lte, + TokenKind::Gt, + TokenKind::Gte, + ]; + for x in &kinds { + for y in &kinds { + let (op_x, op_y) = (Parser::binary_op_kind(x).unwrap(), Parser::binary_op_kind(y).unwrap()); + assert_eq!( + crate::ast::code::binary_op_precedence(op_x).cmp(&crate::ast::code::binary_op_precedence(op_y)), + x.binary_op_precedence().cmp(&y.binary_op_precedence()), + "{x} vs {y}" + ); + } + } +} + +#[rstest] +#[case::after_literal("1 |=")] +#[case::after_literal_with_operand("1 |= b")] +#[case::after_paren("(a) |= b")] +#[case::after_string("\"s\" |= b")] +fn test_pipe_assign_is_not_an_infix_operator(#[case] code: &str) { + assert!( + matches!(parse_shape(code), Err(SyntaxError::UnexpectedToken(token)) if token.kind == TokenKind::PipeEqual), + "expected `|=` to be rejected as an unexpected token for {code:?}" + ); +} diff --git a/crates/mq-lang/src/cst/error.rs b/crates/mq-lang/src/cst/error.rs index 48ea4569d..980c36829 100644 --- a/crates/mq-lang/src/cst/error.rs +++ b/crates/mq-lang/src/cst/error.rs @@ -11,8 +11,6 @@ pub enum ParseError { UnexpectedEOFDetected, #[error("Insufficient tokens `{0}`")] InsufficientTokens(Shared), - #[error("Expected a closing bracket `]` but got `{0}` delimiter")] - ExpectedClosingBracket(Shared), #[error(transparent)] UnknownSelector(selector::UnknownSelector), #[error("Unexpected `end` keyword โ€” no open block to close")] @@ -31,7 +29,6 @@ impl ParseError { match self { ParseError::UnexpectedToken(token) | ParseError::InsufficientTokens(token) - | ParseError::ExpectedClosingBracket(token) | ParseError::UnmatchedEnd(token) | ParseError::Missing { found: token, .. } => Some(token.range), ParseError::UnknownSelector(selector::UnknownSelector(token)) => Some(token.range), @@ -58,9 +55,6 @@ impl ParseError { ParseError::InsufficientTokens(_) => Some( "Parsing could not continue here. Check for missing arguments, operators, or mismatched delimiters.".to_string(), ), - ParseError::ExpectedClosingBracket(_) => Some( - "Expected a closing bracket ']'. Check your brackets for balance.".to_string(), - ), ParseError::Missing { .. } => Some( "A token is missing here. Check for unclosed brackets, parentheses, or blocks.".to_string(), ), @@ -93,7 +87,6 @@ mod tests { #[case::unexpected_token_eof(ParseError::UnexpectedToken(make_token(TokenKind::Eof)), true)] #[case::unexpected_eof_detected(ParseError::UnexpectedEOFDetected, true)] #[case::insufficient_tokens(ParseError::InsufficientTokens(make_token(TokenKind::Comma)), true)] - #[case::expected_closing_bracket(ParseError::ExpectedClosingBracket(make_token(TokenKind::Comma)), true)] #[case::unmatched_end(ParseError::UnmatchedEnd(make_token(TokenKind::End)), true)] fn test_hint_present(#[case] err: ParseError, #[case] has_hint: bool) { assert_eq!(err.hint().is_some(), has_hint); diff --git a/crates/mq-lang/src/cst/parser.rs b/crates/mq-lang/src/cst/parser.rs index 6d2e5e7e4..332bb6e3c 100644 --- a/crates/mq-lang/src/cst/parser.rs +++ b/crates/mq-lang/src/cst/parser.rs @@ -2651,8568 +2651,4 @@ impl<'a> Parser<'a> { } #[cfg(test)] -mod tests { - use std::vec; - - use crate::{lexer::token::StringSegment, range::Range}; - - use super::*; - use crate::cst::node::BinaryOp; - use rstest::rstest; - use smallvec::smallvec; - - fn error_node(kinds: Vec) -> Shared { - Shared::new(Node { - kind: NodeKind::Error { - children: kinds - .into_iter() - .map(|kind| { - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(kind))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) - }) - .collect(), - }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) - } - - fn missing_node(expected: &'static str) -> Shared { - Shared::new(Node { - kind: NodeKind::Missing { - expected, - position: Default::default(), - }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) - } - - fn eof_node() -> Shared { - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) - } - - fn token(token_kind: TokenKind) -> Token { - Token { - range: Range::default(), - kind: token_kind, - module_id: 1.into(), - } - } - - #[rstest] - #[case::def( - vec![ - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Comment("test comment".into()))), - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("test comment2".into()))), - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment2".into()))))], - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1)))), - Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment".into()))))], - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_without_colon1( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: None, program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_without_colon2( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("baz".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::NumberLiteral(42.into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: None, program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_without_colon_with_args( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("func".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("func".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: None, program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::let_( - vec![ - Shared::new(token(TokenKind::Whitespace(4))), - Shared::new(token(TokenKind::Let)), - Shared::new(token(TokenKind::Whitespace(4))), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(42.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Let { lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), eq_token: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Let))), - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::unexpected_token( - vec![ - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Comment("test comment".into()))), - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("test comment2".into()))), - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment2".into()))))], - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1)))), - Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment".into())))) ], - trailing_trivia: TriviaList::new(), - }), - error_node(vec![TokenKind::Comma]), - eof_node(), - ], - ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Comma)))], 100) - ) - )] - #[case::unexpected_eof( - vec![ - Shared::new(token(TokenKind::Whitespace(4))), - Shared::new(token(TokenKind::Let)), - Shared::new(token(TokenKind::Whitespace(4))), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Error { - children: smallvec![ - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Let))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - }, - token: None, - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], - trailing_trivia: TriviaList::new(), - }), - eof_node(), - ], - ErrorReporter::with_error(vec![ParseError::UnexpectedEOFDetected], 100) - ) - )] - #[case::if_( - vec![ - Shared::new(token(TokenKind::If)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch".into()))), - Shared::new(token(TokenKind::Else)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("else_branch".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::If { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), elifs: Elifs::new(), else_branch: Some(Shared::new(Node { - kind: NodeKind::Else { colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("else_branch".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Else))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::If))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::unless_( - vec![ - Shared::new(token(TokenKind::Unless)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Unless { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Unless))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::if_elif_else( - vec![ - Shared::new(token(TokenKind::If)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition1".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch1".into()))), - Shared::new(token(TokenKind::Elif)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition2".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch2".into()))), - Shared::new(token(TokenKind::Else)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("else_branch".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::If { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), elifs: vec![ -Shared::new(Node { - kind: NodeKind::Elif { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Elif))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), -].into(), else_branch: Some(Shared::new(Node { - kind: NodeKind::Else { colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("else_branch".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Else))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::If))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::if_only( - vec![ - Shared::new(token(TokenKind::If)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition1".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch1".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::If { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), elifs: Elifs::new(), else_branch: None }, - token: Some(Shared::new(token(TokenKind::If))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::if_elif_else( - vec![ - Shared::new(token(TokenKind::If)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition1".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch1".into()))), - Shared::new(token(TokenKind::Elif)), - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition2".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch2".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::If { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), elifs: vec![ -Shared::new(Node { - kind: NodeKind::Elif { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Elif))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), -].into(), else_branch: None }, - token: Some(Shared::new(token(TokenKind::If))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::multiple_expr_with_trivia( - vec![ - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("y".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::args_with_function( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::foreach( - vec![ - Shared::new(token(TokenKind::Foreach)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("item".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("collection".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("comment".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("body".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Foreach { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("item".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("collection".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("comment".into())))), Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Foreach))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::foreach_do_end( - vec![ - Shared::new(token(TokenKind::Foreach)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("item".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("collection".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("body".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Foreach { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("item".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("collection".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Do, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Foreach))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::foreach_value_is_function_call( - vec![ - Shared::new(token(TokenKind::Foreach)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("item".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("get_items".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("arg".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("body".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Foreach { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("item".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("arg".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("get_items".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Foreach))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::while_( - vec![ - Shared::new(token(TokenKind::While)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("comment".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("body".into()))), - Shared::new(token(TokenKind::Whitespace(4))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::While { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("comment".into())))), Trivia::NewLine], - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], - }), -].into() }, - token: Some(Shared::new(token(TokenKind::While))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::while_do_end( - vec![ - Shared::new(token(TokenKind::While)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("body".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::While { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Do, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::While))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::until_( - vec![ - Shared::new(token(TokenKind::Until)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("condition".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("body".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Until { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Until))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::loop_( - vec![ - Shared::new(token(TokenKind::Loop)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("loop comment".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("body".into()))), - Shared::new(token(TokenKind::Whitespace(2))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Loop { colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("loop comment".into())))), Trivia::NewLine], - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Loop))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector1( - vec![ - Shared::new(token(TokenKind::Selector(".h1".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".h1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector2( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector3( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector4( - vec![ - Shared::new(token(TokenKind::Selector(".list".into()))), - Shared::new(token(TokenKind::Selector(".checked".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".checked".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".list".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::ident_attribute_access( - vec![ - Shared::new(token(TokenKind::Ident("c".into()))), - Shared::new(token(TokenKind::Selector(".lang".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Ident { attr: Some(Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".lang".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Ident("c".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::self_attribute_access( - vec![ - Shared::new(token(TokenKind::Self_)), - Shared::new(token(TokenKind::Selector(".name".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Self_ { attr: Some(Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".name".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Self_))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::standalone_attr_selector( - vec![ - Shared::new(token(TokenKind::Selector(".lang".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelfAttr, - token: Some(Shared::new(token(TokenKind::Selector(".lang".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::include( - vec![ - Shared::new(token(TokenKind::Include)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::StringLiteral("module".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Include { path: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("module".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Include))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::import( - vec![ - Shared::new(token(TokenKind::Import)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::StringLiteral("module".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Import { path: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("module".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), as_token: None, alias: None }, - token: Some(Shared::new(token(TokenKind::Import))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::import_as( - vec![ - Shared::new(token(TokenKind::Import)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::StringLiteral("module".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::As)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("m".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Import { path: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("module".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), as_token: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::As))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - })), alias: Some(Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("m".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Import))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::root_with_token_after_semicolon( - vec![ - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Ident("y".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Ident("y".into()))))], 100) - ) - )] - #[case::call_with_newlines( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("param comment".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("arg1".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("arg2".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("arg1".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("param comment".into())))), Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("arg2".into())))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::interpolated_string( - vec![ - Shared::new(token(TokenKind::InterpolatedString(vec![StringSegment::Expr("val".into(), Range::default()), StringSegment::Text("hello".into(), Range::default())]))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::InterpolatedString, - token: Some(Shared::new(token(TokenKind::InterpolatedString(vec![StringSegment::Expr("val".into(), Range::default()), StringSegment::Text("hello".into(), Range::default())])))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::nodes( - vec![ - Shared::new(token(TokenKind::Nodes)), - Shared::new(token(TokenKind::Whitespace(1))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Nodes, - token: Some(Shared::new(token(TokenKind::Nodes))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::fn_with_program( - vec![ - Shared::new(token(TokenKind::Fn)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("y".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Fn))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::fn_with_parameter_and_program( - vec![ - Shared::new(token(TokenKind::Fn)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("param".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("body".into()))), - Shared::new(token(TokenKind::SemiColon)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("param".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Fn))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - - ], - ErrorReporter::default() - ) - )] - #[case::fn_with_multiple_parameters( - vec![ - Shared::new(token(TokenKind::Fn)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("param1".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("param2".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("result".into()))), - Shared::new(token(TokenKind::SemiColon)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("param1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("param2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("result".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Fn))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::array_empty( - vec![ - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::array_with_single_element( - vec![ - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(42.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::array_with_multiple_elements( - vec![ - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::StringLiteral("hello".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("hello".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::array_with_trivia( - vec![ - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Comment("array element".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::NumberLiteral(42.into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Whitespace(1))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), - leading_trivia: smallvec![ - Trivia::NewLine, - Trivia::Comment(Shared::new(token(TokenKind::Comment("array element".into())))), - Trivia::NewLine - ], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), -].into() }, - token: None, - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::array_nested( - vec![ - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::array_with_spread( - vec![ - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::DotDotDot)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Spread { operand: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::DotDotDot))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::dict_empty( - vec![ - Shared::new(token(TokenKind::LBrace)), - Shared::new(token(TokenKind::RBrace)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Dict { entries: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::dict_with_single_pair( - vec![ - Shared::new(token(TokenKind::LBrace)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::RBrace)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Dict { entries: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::DictEntry { key: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), value: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::dict_with_multiple_pairs( - vec![ - Shared::new(token(TokenKind::LBrace)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::StringLiteral("foo".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RBrace)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Dict { entries: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::DictEntry { key: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), value: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::DictEntry { key: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), value: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::dict_with_spread( - vec![ - Shared::new(token(TokenKind::LBrace)), - Shared::new(token(TokenKind::DotDotDot)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::RBrace)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Dict { entries: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Spread { operand: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::DotDotDot))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBrace))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::eq_eq( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::EqEq)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Equal, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::EqEq))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::ne_eq( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::NeEq)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::NotEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::NeEq))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::plus( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Plus)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Plus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::coalesce( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Coalesce)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Coalesce, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Coalesce))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::lt( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Lt)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Lt, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Lt))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::lte( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Lte)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Lte, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Lte))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::gt( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Gt)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Gt, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Gt))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::gte( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Gte)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Gte, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Gte))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::range( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::DoubleDot)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::RangeOp, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::DoubleDot))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::minus( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Minus)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Minus, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Minus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::division( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Slash)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Division, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Slash))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::percent( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Percent)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Modulo, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Percent))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::multiplication( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Asterisk)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Multiplication, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Asterisk))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::left_shift( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::LeftShift)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::LeftShift, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::LeftShift))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::right_shift( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::RightShift)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::RightShift, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::RightShift))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::convert( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Convert)), - Shared::new(token(TokenKind::Ident("b".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Convert, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Convert))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::multiple_binary_ops( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Plus)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::Minus)), - Shared::new(token(TokenKind::Ident("c".into()))), - Shared::new(token(TokenKind::Asterisk)), - Shared::new(token(TokenKind::Ident("d".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Minus, lhs: Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Plus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Multiplication, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("c".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("d".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Asterisk))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Minus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::binary_ops_with_trivia( - vec![ - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Plus)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("y".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Minus)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("z".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Minus, lhs: Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }) }, - token: Some(Shared::new(token(TokenKind::Plus))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("z".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Minus))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::string_literal_with_escape_sequences( - vec![ - Shared::new(token(TokenKind::StringLiteral("\\x1b[2J\\x1b[H".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("\\x1b[2J\\x1b[H".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::symbol_with_ident( - vec![ - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("foo".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Symbol { colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), name: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::symbol_with_string( - vec![ - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("bar".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Symbol { colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), name: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::not_operation( - vec![ - Shared::new(token(TokenKind::Not)), - Shared::new(token(TokenKind::BoolLiteral(true))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::UnaryOp { op: UnaryOp::Not, operand: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Not))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::break_in_loop( - vec![ - Shared::new(token(TokenKind::While)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::BoolLiteral(true))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Break)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::While { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Break { colon: None, value: None }, - token: Some(Shared::new(token(TokenKind::Break))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::While))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::break_in_loop_with_value( - vec![ - Shared::new(token(TokenKind::While)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::BoolLiteral(true))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Break)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::BoolLiteral(false))), - Shared::new(token(TokenKind::SemiColon)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::While { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Break { colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), value: Some(Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::BoolLiteral(false)))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Break))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::While))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::continue_in_loop( - vec![ - Shared::new(token(TokenKind::While)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::BoolLiteral(true))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Continue)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::While { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Continue, - token: Some(Shared::new(token(TokenKind::Continue))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::While))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::break_outside_loop( - vec![ - Shared::new(token(TokenKind::Break)), - ], - ( - vec![error_node(vec![TokenKind::Break])], - ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Break)))], 100) - ) - )] - #[case::continue_outside_loop( - vec![ - Shared::new(token(TokenKind::Continue)), - ], - ( - vec![error_node(vec![TokenKind::Continue])], - ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Continue)))], 100) - ) - )] - #[case::bracket_access_with_number( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(5.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(5.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::bracket_access_with_string( - vec![ - Shared::new(token(TokenKind::Ident("dict".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::StringLiteral("key".into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("key".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("dict".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::bracket_access_error_missing_rbracket( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(5.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(5.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - missing_node("`]`"), - ].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - eof_node(), - ], - ErrorReporter::with_error(vec![ParseError::Missing { expected: "`]`", found: Shared::new(token(TokenKind::Eof)) }], 100) - ) - )] - #[case::call_with_not_ident_arg( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Not)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::UnaryOp { op: UnaryOp::Not, operand: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Not))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::slice_access_with_numbers( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::slice_access_with_variables( - vec![ - Shared::new(token(TokenKind::Ident("items".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::Ident("start".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("end".into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("start".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("end".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("items".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::group_expr_single_ident( - vec![ - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Group { lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), expr: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::group_expr_binary_op( - vec![ - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Plus)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Group { lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), expr: Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Plus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::group_expr_with_trivia( - vec![ - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Whitespace(1))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Group { lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), expr: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), rparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }) }, - token: None, - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::group_expr_missing_rparen( - vec![ - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Group { - lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - expr: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - rparen: missing_node("`)`"), - }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - eof_node(), - ], - ErrorReporter::with_error(vec![ParseError::Missing { expected: "`)`", found: Shared::new(token(TokenKind::Eof)) }], 100) - ) - )] - // Test group expr with index access: (x)[0] - #[case::group_expr_with_index_access( - vec![ - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(0.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Group { lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), expr: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - // Test group expr with dynamic call: (x)("test") - #[case::group_expr_with_dynamic_call( - vec![ - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::StringLiteral("test".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::CallDynamic { callee: Shared::new(Node { - kind: NodeKind::Group { lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), expr: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - // Test array literal with index access: [1,2][0] - #[case::array_literal_with_index_access( - vec![ - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(0.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Array { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::if_with_grouped_binary_op_condition( - vec![ - Shared::new(token(TokenKind::If)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Plus)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("then_branch".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::If { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Group { lparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), expr: Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Plus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rparen: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), then_branch: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("then_branch".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), elifs: Elifs::new(), else_branch: None }, - token: Some(Shared::new(token(TokenKind::If))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), - ], - ErrorReporter::default() - ) - )] - #[case::self_bracket_access_with_number( - vec![ - Shared::new(token(TokenKind::Self_)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Self_))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_index_access( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::negate( - vec![ - Shared::new(token(TokenKind::Minus)), - Shared::new(token(TokenKind::NumberLiteral(42.into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::UnaryOp { op: UnaryOp::Negate, operand: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Minus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::do_block_simple( - vec![ - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::Ident("test".into()))), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("test".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::do_block_with_whitespace( - vec![ - Shared::new(token(TokenKind::Whitespace(2))), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Tab(1))), - Shared::new(token(TokenKind::Ident("expression".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("expression".into())))), - leading_trivia: smallvec![Trivia::NewLine, Trivia::Tab(Shared::new(token(TokenKind::Tab(1))))], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::do_block_with_comment( - vec![ - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::Comment("inside block".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Ident("value".into()))), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("value".into())))), - leading_trivia: smallvec![Trivia::Comment(Shared::new(token(TokenKind::Comment("inside block".into())))), Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::do_block_empty( - vec![ - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: smallvec![Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })] }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::End)))], 100) - ) - )] - #[case::do_block_nested( - vec![ - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::Ident("inner".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("inner".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::try_catch( - vec![ - Shared::new(token(TokenKind::Try)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("try_body".into()))), - Shared::new(token(TokenKind::Catch)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("catch_body".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), body: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("try_body".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), catch: Some(Shared::new(Node { - kind: NodeKind::Catch { params: None, colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), body: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("catch_body".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Catch))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Try))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::try_without_catch( - vec![ - Shared::new(token(TokenKind::Try)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("try_body".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), body: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("try_body".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), catch: None }, - token: Some(Shared::new(token(TokenKind::Try))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_question_mark( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Question)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Question))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::match_simple( - vec![ - Shared::new(token(TokenKind::Match)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("one".into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("_".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("other".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Match { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), arms: vec![ -Shared::new(Node { - kind: NodeKind::MatchArm { pipe: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), pattern: Shared::new(Node { - kind: NodeKind::Pattern { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), if_token: None, guard_args: None, colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), body: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("one".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::MatchArm { pipe: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), pattern: Shared::new(Node { - kind: NodeKind::Pattern { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Ident("_".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), if_token: None, guard_args: None, colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), body: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("other".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), end_token: Some(Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Match))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::match_do_end( - vec![ - Shared::new(token(TokenKind::Match)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("one".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("_".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("other".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Match { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Do, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), arms: vec![ -Shared::new(Node { - kind: NodeKind::MatchArm { pipe: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), pattern: Shared::new(Node { - kind: NodeKind::Pattern { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), if_token: None, guard_args: None, colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), body: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("one".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::MatchArm { pipe: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), pattern: Shared::new(Node { - kind: NodeKind::Pattern { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Ident("_".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), if_token: None, guard_args: None, colon: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), body: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("other".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: None, - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: TriviaList::new(), - }), -].into(), end_token: Some(Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Match))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::index_access_with_function_call( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(0.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::CallDynamic { callee: Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::qualified_access_simple( - vec![ - Shared::new(token(TokenKind::Ident("mod1".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Ident("value".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::QualifiedAccess { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("value".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::qualified_access_multi_level( - vec![ - Shared::new(token(TokenKind::Ident("mod1".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Ident("mod2".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Ident("value".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::QualifiedAccess { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("mod2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("value".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::qualified_access_with_call( - vec![ - Shared::new(token(TokenKind::Ident("mod1".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Ident("func".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::StringLiteral("arg".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::QualifiedAccess { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("func".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("arg".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::qualified_access_multi_level_with_call( - vec![ - Shared::new(token(TokenKind::Ident("mod1".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Ident("mod2".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Ident("func".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::QualifiedAccess { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("mod2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("func".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::qualified_access::empty( - vec![ - Shared::new(token(TokenKind::Ident("mod1".into()))), - Shared::new(token(TokenKind::DoubleColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::QualifiedAccess { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::DoubleColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::module_simple( - vec![ - Shared::new(token(TokenKind::Module)), - Shared::new(token(TokenKind::Ident("modname1".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Let)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("bar".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Module { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("modname1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Let { lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), eq_token: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Let))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Module))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::slice_access_with_start_only( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_existing_and_non_existing( - vec![ - Shared::new(token(TokenKind::Selector(".h".into()))), - Shared::new(token(TokenKind::Selector(".value".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".value".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_property( - vec![ - Shared::new(token(TokenKind::Selector(".\"notfound\"".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"notfound\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_property_chained( - vec![ - Shared::new(token(TokenKind::Selector(".\"a\"".into()))), - Shared::new(token(TokenKind::Selector(".\"b\"".into()))), - Shared::new(token(TokenKind::Selector(".\"c\"".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"a\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"b\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"c\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"a\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_property_chained_two( - vec![ - Shared::new(token(TokenKind::Selector(".\"foo\"".into()))), - Shared::new(token(TokenKind::Selector(".\"bar\"".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"foo\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"bar\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"foo\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_property_iterator( - // ."items"[] โ†’ Block([Selector(."items"), Selector(. + [[ + ]]))] - vec![ - Shared::new(token(TokenKind::Selector(".\"items\"".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(Token { - kind: TokenKind::Selector(".".into()), - range: Range::default(), - module_id: 1.into(), - })), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_property_iterator_with_index( - // ."items"[0] โ†’ Block([Selector(."items"), Selector(. + [[ + 0 + ]]))] - vec![ - Shared::new(token(TokenKind::Selector(".\"items\"".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(0.0.into()))), - Shared::new(token(TokenKind::RBracket)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(Token { - kind: TokenKind::Selector(".".into()), - range: Range::default(), - module_id: 1.into(), - })), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_selector_attribute( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Selector(".level".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".level".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[rstest] - #[case::selector_dot_followed_by_pipe( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::Pipe)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_dot_followed_by_semicolon( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::SemiColon)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_dot_followed_by_end( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::End)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_dot_followed_by_binary_op( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::Plus)), - Shared::new(token(TokenKind::Ident("x".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Plus))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::let_followed_by_let_without_pipe( - vec![ - Shared::new(token(TokenKind::Let)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Let)), - Shared::new(token(TokenKind::Ident("y".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Let { lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), eq_token: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Let))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Let { lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), eq_token: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Let))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - eof_node(), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_do_block_argument( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_implicit_pipeline_arg( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("baz".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_implicit_pipeline_arg_multi_step( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("c".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("c".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_implicit_pipeline_first_of_multiple_args( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("c".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("c".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_implicit_pipeline_second_arg( - // foo(x, a | b) โ€” pipeline is the second argument - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_implicit_pipeline_multiline( - // foo(a\n| b) โ€” pipeline spanning two lines - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::NewLine)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: smallvec![Trivia::NewLine], - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_implicit_pipeline_call_exprs( - // foo(bar() | baz()) โ€” function calls inside pipeline - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Whitespace(1))), - Shared::new(token(TokenKind::Ident("baz".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Block { program: vec![ -Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], - }), -Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: None, - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_try_argument( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Try)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), body: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), catch: None }, - token: Some(Shared::new(token(TokenKind::Try))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::call_with_try_catch_argument( - vec![ - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Try)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("bar".into()))), - Shared::new(token(TokenKind::Catch)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("baz".into()))), - Shared::new(token(TokenKind::RParen)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), body: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), catch: Some(Shared::new(Node { - kind: NodeKind::Catch { params: None, colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), body: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Catch))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Try))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::assign( - vec![ - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(10.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::Assign, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(10.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::plus_equal( - vec![ - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::PlusEqual)), - Shared::new(token(TokenKind::NumberLiteral(5.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::PlusEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(5.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::PlusEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::minus_equal( - vec![ - Shared::new(token(TokenKind::Ident("y".into()))), - Shared::new(token(TokenKind::MinusEqual)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::MinusEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::MinusEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::star_equal( - vec![ - Shared::new(token(TokenKind::Ident("z".into()))), - Shared::new(token(TokenKind::StarEqual)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::MultiplicationEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("z".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::StarEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::slash_equal( - vec![ - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::SlashEqual)), - Shared::new(token(TokenKind::NumberLiteral(4.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::DivisionEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(4.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::SlashEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::percent_equal( - vec![ - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::PercentEqual)), - Shared::new(token(TokenKind::NumberLiteral(7.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::ModuloEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(7.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::PercentEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::double_slash_equal( - vec![ - Shared::new(token(TokenKind::Ident("c".into()))), - Shared::new(token(TokenKind::DoubleSlashEqual)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::DoubleDivisionEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("c".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::DoubleSlashEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::pipe_equal_with_selector( - vec![ - Shared::new(token(TokenKind::Selector(".h1".into()))), - Shared::new(token(TokenKind::Selector(".value".into()))), - Shared::new(token(TokenKind::PipeEqual)), - Shared::new(token(TokenKind::StringLiteral("new_id".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::PipeEqual, lhs: Shared::new(Node { - kind: NodeKind::Selector { items: vec![ -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".value".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("new_id".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::PipeEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::pipe_equal_with_ident_and_attr( - vec![ - Shared::new(token(TokenKind::Ident("obj".into()))), - Shared::new(token(TokenKind::Selector(".value".into()))), - Shared::new(token(TokenKind::PipeEqual)), - Shared::new(token(TokenKind::StringLiteral("John".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::PipeEqual, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: Some(Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".value".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Ident("obj".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("John".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::PipeEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::pipe_equal_with_self( - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::PipeEqual)), - Shared::new(token(TokenKind::NumberLiteral(42.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::PipeEqual, lhs: Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::PipeEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::var_then_assign( - vec![ - Shared::new(token(TokenKind::Var)), - Shared::new(token(TokenKind::Ident("count".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(0.into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("count".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(10.into()))), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("count".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Var { lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("count".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), eq_token: Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Var))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::Assign, lhs: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("count".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(10.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("count".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::index_assign( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(0.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(10.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::Assign, lhs: Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(10.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::index_compound_assign( - vec![ - Shared::new(token(TokenKind::Ident("arr".into()))), - Shared::new(token(TokenKind::LBracket)), - Shared::new(token(TokenKind::NumberLiteral(0.into()))), - Shared::new(token(TokenKind::RBracket)), - Shared::new(token(TokenKind::PlusEqual)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Assign { op: BinaryOp::PlusEqual, lhs: Shared::new(Node { - kind: NodeKind::Call { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RBracket))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::PlusEqual))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_with_default_param( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("greet".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("name".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("greeting".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::StringLiteral("Hello".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("greeting".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("greet".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("name".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), default: Some(Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("Hello".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Ident("greeting".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("greeting".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::fn_with_multiple_default_params( - vec![ - Shared::new(token(TokenKind::Fn)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("b".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("c".into()))), - Shared::new(token(TokenKind::Equal)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("a".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), default: Some(Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Ident("b".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Equal))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), default: Some(Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })) }, - token: Some(Shared::new(token(TokenKind::Ident("c".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("a".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Fn))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::arrow_with_program( - vec![ - Shared::new(token(TokenKind::Arrow)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("x".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("y".into()))), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("x".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Arrow))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("y".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::arrow_with_parameter_and_program( - vec![ - Shared::new(token(TokenKind::Arrow)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("param".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("body".into()))), - Shared::new(token(TokenKind::SemiColon)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("param".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("body".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Arrow))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::arrow_with_multiple_parameters( - vec![ - Shared::new(token(TokenKind::Arrow)), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("param1".into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::Ident("param2".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::StringLiteral("result".into()))), - Shared::new(token(TokenKind::SemiColon)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Fn { params: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("param1".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("param2".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into(), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("result".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Arrow))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_with_variadic_param( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("f".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Asterisk)), - Shared::new(token(TokenKind::Ident("args".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("args".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("f".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: Some(vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -// Variadic param: Ident node with Asterisk token as single child - Shared::new(Node { - kind: NodeKind::Param { asterisk: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Asterisk))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), eq_token: None, default: None }, - token: Some(Shared::new(token(TokenKind::Ident("args".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into()), colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("args".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_without_params( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("f".into()))), - Shared::new(token(TokenKind::Colon)), - Shared::new(token(TokenKind::Ident("args".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("f".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: None, colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Colon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("args".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::def_without_params_with_do( - vec![ - Shared::new(token(TokenKind::Def)), - Shared::new(token(TokenKind::Ident("f".into()))), - Shared::new(token(TokenKind::Do)), - Shared::new(token(TokenKind::Ident("args".into()))), - Shared::new(token(TokenKind::SemiColon)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Def { name: Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("f".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), params: None, colon_or_do: Some(Shared::new(Node { - kind: NodeKind::Do, - token: Some(Shared::new(token(TokenKind::Do))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - })), program: vec![ -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("args".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::SemiColon))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Def))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_call_heading_single_arg( - vec![ - Shared::new(token(TokenKind::Selector(".h".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelectorCall { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_call_heading_multi_arg( - vec![ - Shared::new(token(TokenKind::Selector(".h".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::Comma)), - Shared::new(token(TokenKind::NumberLiteral(2.into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelectorCall { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Comma))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_call_code_lang( - vec![ - Shared::new(token(TokenKind::Selector(".code".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::StringLiteral("rust".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelectorCall { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::StringLiteral("rust".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".code".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_call_heading_range( - vec![ - Shared::new(token(TokenKind::Selector(".h".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::DoubleDot)), - Shared::new(token(TokenKind::NumberLiteral(3.into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelectorCall { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -// 1..3 is parsed as a BinaryOp(RangeOp) expression by parse_expr - Shared::new(Node { - kind: NodeKind::BinaryOp { op: BinaryOp::RangeOp, lhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), rhs: Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }) }, - token: Some(Shared::new(token(TokenKind::DoubleDot))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_call_ident_arg( - vec![ - Shared::new(token(TokenKind::Selector(".h".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::Ident("n".into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelectorCall { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("n".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::selector_call_with_attribute( - vec![ - Shared::new(token(TokenKind::Selector(".h".into()))), - Shared::new(token(TokenKind::LParen)), - Shared::new(token(TokenKind::NumberLiteral(1.into()))), - Shared::new(token(TokenKind::RParen)), - Shared::new(token(TokenKind::Selector(".depth".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::SelectorCall { args: vec![ -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::LParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Literal, - token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::RParen))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -Shared::new(Node { - kind: NodeKind::Selector { items: ArgList::new() }, - token: Some(Shared::new(token(TokenKind::Selector(".depth".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), -].into() }, - token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Eof, - token: Some(Shared::new(token(TokenKind::Eof))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - ], - ErrorReporter::default() - ) - )] - #[case::unmatched_end_at_root( - // `. end end eof` โ€” the first `end` closes a root statement, the second `end` has no - // matching open block and should produce `UnmatchedEnd` pointing at the first `end`. - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - eof_node(), - ], - ErrorReporter::with_error(vec![ParseError::UnmatchedEnd(Shared::new(token(TokenKind::End)))], 100) - ) - )] - #[case::unmatched_end_at_root_pipe_follows( - // `. end end | ident eof` โ€” mirrors the real-world `def foo: if (.): . end end | foo` - // scenario where the extra `end` should be flagged, not the pipe after it. - vec![ - Shared::new(token(TokenKind::Selector(".".into()))), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::End)), - Shared::new(token(TokenKind::Pipe)), - Shared::new(token(TokenKind::Ident("foo".into()))), - Shared::new(token(TokenKind::Eof)), - ], - ( - vec![ - Shared::new(Node { - kind: NodeKind::Self_ { attr: None }, - token: Some(Shared::new(token(TokenKind::Selector(".".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::End, - token: Some(Shared::new(token(TokenKind::End))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Token, - token: Some(Shared::new(token(TokenKind::Pipe))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - Shared::new(Node { - kind: NodeKind::Ident { attr: None }, - token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), - leading_trivia: TriviaList::new(), - trailing_trivia: TriviaList::new(), - }), - eof_node(), - ], - ErrorReporter::with_error(vec![ParseError::UnmatchedEnd(Shared::new(token(TokenKind::End)))], 100) - ) - )] - - fn test_parse(#[case] input: Vec>, #[case] expected: (Vec>, ErrorReporter)) { - let (nodes, errors) = Parser::new(&input).parse(); - assert_eq!(errors, expected.1); - assert_eq!(nodes, expected.0); - } - - #[test] - fn test_module_block_can_follow_a_def() { - let (nodes, errors) = crate::parse_recovery("def a(): 1;\nmodule m:\n def b(): 2;\nend\n"); - assert!(!errors.has_errors()); - assert!(nodes.iter().any(|node| matches!(node.kind, NodeKind::Module { .. }))); - } - - #[test] - fn test_descendant_chain_has_no_synthetic_bridge_node() { - // No synthetic DoubleDot node: it would duplicate the next selector's source range. - let (nodes, errors) = crate::parse_recovery(".blockquote .code"); - assert!(!errors.has_errors()); - - let block = nodes - .iter() - .find(|node| matches!(node.kind, NodeKind::Block { .. })) - .expect("descendant chain should parse as a Block"); - let children = block.all_children(); - - assert_eq!( - children.len(), - 2, - "expected exactly the two real selectors, got {:#?}", - children - ); - assert!( - children - .iter() - .all(|child| !matches!(child.token.as_ref().map(|t| &t.kind), Some(TokenKind::DoubleDot))), - "no child should carry a synthetic DoubleDot token: {:#?}", - children - ); - } - - #[test] - fn test_unmatched_end_error_message() { - // Verify the error message text for `UnmatchedEnd`. - let err = ParseError::UnmatchedEnd(Shared::new(token(TokenKind::End))); - assert_eq!(err.to_string(), "Unexpected `end` keyword โ€” no open block to close"); - } - - #[test] - fn test_unmatched_end_error_ranges() { - // Verify that `error_ranges` correctly extracts the range from an `UnmatchedEnd` error. - let end_token = Shared::new(Token { - range: Range { - start: Position { line: 1, column: 20 }, - end: Position { line: 1, column: 23 }, - }, - kind: TokenKind::End, - module_id: 1.into(), - }); - let mut reporter = ErrorReporter::new(100); - reporter.report(ParseError::UnmatchedEnd(Shared::clone(&end_token))); - - let ranges = reporter.error_ranges("def foo: if (.): . end end"); - assert_eq!(ranges.len(), 1); - assert_eq!(ranges[0].1.start.line, 1); - assert_eq!(ranges[0].1.start.column, 20); - assert!(ranges[0].0.contains("end")); - } - - #[test] - fn test_error_reporter_error_ranges() { - let text = "def foo():\n bar()\n"; - - let mut reporter = ErrorReporter::new(100); - - let unexpected_token = Shared::new(Token { - range: Range { - start: Position { line: 1, column: 4 }, - end: Position { line: 1, column: 7 }, - }, - kind: TokenKind::Ident("foo".into()), - module_id: 1.into(), - }); - reporter.report(ParseError::UnexpectedToken(Shared::clone(&unexpected_token))); - - let insufficient_token = Shared::new(Token { - range: Range { - start: Position { line: 1, column: 4 }, - end: Position { line: 1, column: 7 }, - }, - kind: TokenKind::Ident("bar".into()), - module_id: 1.into(), - }); - reporter.report(ParseError::InsufficientTokens(Shared::clone(&insufficient_token))); - - reporter.report(ParseError::UnexpectedEOFDetected); - - let ranges = reporter.error_ranges(text); - - assert_eq!(ranges.len(), 3); - - assert_eq!(ranges[0].1.start.line, 1); - assert_eq!(ranges[0].1.start.column, 4); - assert_eq!(ranges[0].1.end.line, 1); - assert_eq!(ranges[0].1.end.column, 7); - - assert_eq!(ranges[1].1.start.line, 1); - assert_eq!(ranges[1].1.start.column, 4); - assert_eq!(ranges[1].1.end.line, 1); - assert_eq!(ranges[1].1.end.column, 7); - - assert_eq!(ranges[2].1.start.line, 2); - assert_eq!(ranges[2].1.start.column, 6); - assert_eq!(ranges[2].1.end.line, 2); - assert_eq!(ranges[2].1.end.column, 6); - } - - #[test] - fn test_error_reporter_diagnostics_carries_hints_and_ranges() { - let text = "def foo():\n bar()\n"; - - let mut reporter = ErrorReporter::new(100); - - let unexpected_token = Shared::new(Token { - range: Range { - start: Position { line: 1, column: 4 }, - end: Position { line: 1, column: 7 }, - }, - kind: TokenKind::Ident("foo".into()), - module_id: 1.into(), - }); - reporter.report(ParseError::UnexpectedToken(Shared::clone(&unexpected_token))); - reporter.report(ParseError::UnexpectedEOFDetected); - - let diagnostics = reporter.diagnostics(text); - - assert_eq!(diagnostics.len(), 2); - - assert_eq!( - diagnostics[0].message, - ParseError::UnexpectedToken(unexpected_token).to_string() - ); - assert_eq!(diagnostics[0].range.unwrap().start.column, 4); - assert_eq!(diagnostics[0].hints.len(), 1); - - assert_eq!(diagnostics[1].message, ParseError::UnexpectedEOFDetected.to_string()); - assert_eq!(diagnostics[1].hints.len(), 1); - } - - #[test] - fn test_error_reporter_diagnostics_unknown_selector_hint_matches_error_help() { - let mut reporter = ErrorReporter::new(100); - reporter.report(ParseError::UnknownSelector(crate::selector::UnknownSelector::new( - Token { - range: Range::default(), - kind: TokenKind::Selector(smol_str::SmolStr::new(".hedaing")), - module_id: 1.into(), - }, - ))); - - let diagnostics = reporter.diagnostics(""); - assert_eq!( - diagnostics[0].hints[0], - "Unknown selector `.hedaing`. A selector with a similar name exists: `.heading`." - ); - } - - #[test] - fn test_error_reporter_has_errors() { - let mut reporter = ErrorReporter::new(100); - assert!(!reporter.has_errors()); - - reporter.report(ParseError::UnexpectedEOFDetected); - assert!(reporter.has_errors()); - } - - #[test] - fn test_error_reporter_max_errors() { - let mut reporter = ErrorReporter::new(2); - - reporter.report(ParseError::UnexpectedToken(Shared::new(Token { - range: Range::default(), - kind: TokenKind::Ident("foo".into()), - module_id: 1.into(), - }))); - reporter.report(ParseError::UnexpectedToken(Shared::new(Token { - range: Range::default(), - kind: TokenKind::Ident("bar".into()), - module_id: 1.into(), - }))); - reporter.report(ParseError::UnexpectedEOFDetected); - - assert_eq!(reporter.errors.len(), 2); - } - - #[test] - fn test_error_reporter_unexpected_eof_detected() { - let mut reporter = ErrorReporter::new(2); - - reporter.report(ParseError::UnexpectedEOFDetected); - reporter.report(ParseError::UnexpectedEOFDetected); - - assert_eq!(reporter.errors.len(), 1); - } - - #[test] - fn test_error_reporter_display() { - let mut reporter = ErrorReporter::new(100); - - reporter.report(ParseError::UnexpectedEOFDetected); - let token = Shared::new(Token { - range: Range::default(), - kind: TokenKind::Ident("foo".into()), - module_id: 1.into(), - }); - reporter.report(ParseError::UnexpectedToken(token)); - - let display = format!("{}", reporter); - assert!(display.contains("Unexpected EOF detected")); - assert!(display.contains("Unexpected token")); - } - - #[rstest] - #[case::binary_op_without_rhs("1 +")] - #[case::as_without_name("1 as")] - #[case::as_with_non_ident("1 as 2")] - #[case::let_only("let")] - #[case::let_without_rhs("let x =")] - #[case::var_only("var")] - #[case::def_only("def")] - #[case::def_without_body("def f(x):")] - #[case::fn_only("fn")] - #[case::include_only("include")] - #[case::import_only("import")] - #[case::import_as_without_alias("import \"csv\" as")] - #[case::if_only("if")] - #[case::if_without_then("if (1):")] - #[case::elif_without_if("elif (1): 1")] - #[case::match_only("match")] - #[case::match_arm_without_body("match (1): | x:")] - #[case::foreach_open_paren("foreach (")] - #[case::try_only("try")] - #[case::selector_bracket_open(".[")] - #[case::dict_unclosed("{\"a\": ")] - #[case::dict_trailing_comma_unclosed("{\"a\": 1, ")] - #[case::array_unclosed("[1, ")] - #[case::call_unclosed("f(1, ")] - #[case::trailing_pipe("1 |")] - #[case::unmatched_end("end")] - fn test_truncated_input_reports_error(#[case] code: &str) { - let (_, errors) = crate::parse_recovery(code); - assert!(errors.has_errors(), "{code}"); - } - - #[rstest] - #[case::single_line("{\"a\": 1,}")] - #[case::multi_entry("{\"a\": 1, \"b\": [1],}")] - #[case::multi_line("{\n \"a\": 1,\n # c\n}")] - #[case::spread("{...x, \"a\": 1,}")] - fn test_dict_trailing_comma(#[case] code: &str) { - let (nodes, errors) = crate::parse_recovery(code); - assert!(!errors.has_errors(), "{code}: {errors:?}"); - assert!(matches!(nodes[0].kind, NodeKind::Dict { .. }), "{code}"); - } - - #[rstest] - #[case::string("\"abc\"[1]")] - #[case::string_slice("\"abc\"[1:2]")] - #[case::interpolated("s\"a${1}\"[0]")] - fn test_string_literal_index(#[case] code: &str) { - let (nodes, errors) = crate::parse_recovery(code); - assert!(!errors.has_errors(), "{code}: {errors:?}"); - assert!(matches!(nodes[0].kind, NodeKind::Call { .. }), "{code}"); - } - - #[rstest] - #[case::string_key("{\"a\": 1}[\"a\"]")] - #[case::symbol_key("{\"a\": 1}[:a]")] - #[case::chained("{\"a\": {\"b\": 2}}[\"a\"][\"b\"]")] - fn test_dict_literal_index(#[case] code: &str) { - let (nodes, errors) = crate::parse_recovery(code); - assert!(!errors.has_errors(), "{code}: {errors:?}"); - assert!(matches!(nodes[0].kind, NodeKind::Call { .. }), "{code}"); - } - - #[rstest] - #[case::dict_newline("{\"a\": 1}\n[\"a\"]")] - #[case::dict_comment("{\"a\": 1} # c\n[\"a\"]")] - #[case::array_newline("[1, 2]\n[0]")] - #[case::string_newline("\"abc\"\n[0]")] - #[case::ident_newline("x\n[0]")] - #[case::call_newline("foo(1)\n[0]")] - #[case::dynamic_call_newline("x\n(1)")] - fn test_postfix_across_newline(#[case] code: &str) { - let (nodes, errors) = crate::parse_recovery(code); - assert!(!errors.has_errors(), "{code}: {errors:?}"); - let exprs: Vec<_> = nodes.iter().filter(|n| !matches!(n.kind, NodeKind::Eof)).collect(); - assert_eq!(exprs.len(), 1, "{code}"); - assert!( - matches!(exprs[0].kind, NodeKind::Call { .. } | NodeKind::CallDynamic { .. }), - "{code}" - ); - } - - const TOKEN_SOUP: &[&str] = &[ - "def", - "fn", - "let", - "var", - "if", - "elif", - "else", - "unless", - "while", - "until", - "loop", - "foreach", - "match", - "try", - "catch", - "do", - "end", - "module", - "import", - "include", - "as", - "break", - "continue", - "yield", - "self", - "nodes", - "(", - ")", - "[", - "]", - "{", - "}", - ":", - ";", - ",", - "|", - "=", - "+", - "-", - "*", - "/", - "==", - "..", - ".", - ".h1", - ".[0]", - "x", - "f", - "1", - "\"s\"", - "s\"${x}\"", - ":sym", - "$ENV", - "#c\n", - "\n", - " ", - ]; - - const STATEMENTS: &[&str] = &[ - "def f(x, y = 1):\n x + y;", - "let a = [1, 2]", - "if (a):\t1 elif (b): 2 else: 3", - "match (x): | [a, ..rest]: a | {k}: k | _: 0 end", - "foreach (v, a): v;", - "try: 1 catch(e): e", - ".h1.value", - ".[0][1]", - "{\"a\": 1, ...b}", - "fn(x): x;", - "s\"${a} b\"", - "import \"csv\" as c", - "module m: def g(): 1; end", - "while (a): break: 1;", - "loop do continue end", - "1 as n", - "x += 1", - "(a)(1)", - ]; - - const SEPARATORS: &[&str] = &[" | ", "\n| ", " |\n ", "\t|\t", " | # comment\n", "\n\n| "]; - - /// Broken input still yields a tree: the well-formed parts survive, and the - /// damage is confined to `Error`/`Missing` nodes. - #[rstest] - #[case::unclosed_call("foo(1, 2,", "foo", "`)`")] - #[case::unclosed_group("(x", "(", "`)`")] - #[case::unclosed_array("[1, 2", "[", "`]`")] - #[case::unclosed_dict("{a: 1, b: 2", "{", "`}`")] - #[case::unclosed_bracket_access("arr[5", "arr", "`]`")] - fn test_unclosed_delimiter_becomes_missing_node(#[case] code: &str, #[case] kept: &str, #[case] expected: &str) { - let (nodes, errors) = crate::parse_recovery(code); - - assert!(errors.has_errors(), "{code}"); - assert!( - nodes - .iter() - .any(|node| node.to_string() == kept || node.children().any(|c| c.to_string() == kept)), - "{code}: {nodes:?}" - ); - fn has_missing(node: &Shared, expected: &str) -> bool { - matches!(&node.kind, NodeKind::Missing { expected: e, .. } if *e == expected) - || node.children().any(|child| has_missing(child, expected)) - } - assert!( - nodes.iter().any(|node| has_missing(node, expected)), - "{code}: {nodes:?}" - ); - } - - #[rstest] - #[case::leading("foo(, 1)")] - #[case::consecutive("foo(1,, 2)")] - #[case::trailing_pair("foo(1, ,)")] - #[case::only_comma("foo(,)")] - #[case::array_leading("[, 1]")] - fn test_absent_item_before_separator_has_error(#[case] code: &str) { - let (nodes, errors) = crate::parse_recovery(code); - - assert!(errors.has_errors(), "{code}"); - assert!(nodes.iter().any(|node| node.has_error()), "{code}: {nodes:?}"); - let commas: usize = nodes - .iter() - .flat_map(|node| node.children()) - .filter(|child| matches!(child.token.as_ref().map(|t| &t.kind), Some(TokenKind::Comma))) - .count(); - assert_eq!(commas, code.matches(',').count(), "{code}: separators must be kept"); - } - - #[rstest] - #[case::unclosed_call("foo(1,", Position { line: 1, column: 7 })] - #[case::unclosed_call_next_line("foo(1,\n", Position { line: 1, column: 7 })] - #[case::cursor_on_next_line("foo(1,\n ", Position { line: 2, column: 2 })] - fn test_unclosed_call_range_covers_cursor(#[case] code: &str, #[case] cursor: Position) { - let (nodes, _) = crate::parse_recovery(code); - let call = nodes - .iter() - .find(|node| matches!(node.kind, NodeKind::Call { .. })) - .unwrap_or_else(|| panic!("{code}: {nodes:?}")); - - assert!(call.node_range().contains(&cursor), "{code}: {:?}", call.node_range()); - } - - #[rstest] - #[case::junk_between_items("foo(1 2, 3)", 3)] - #[case::invalid_item("foo(1, ยง, 3) | bar", 3)] - #[case::stray_closer("foo(1) ) bar(2)", 1)] - fn test_junk_is_kept_in_error_node(#[case] code: &str, #[case] valid_literals: usize) { - let (nodes, errors) = crate::parse_recovery(code); - - fn count(node: &Shared, error_nodes: &mut usize, literals: &mut usize) { - match node.kind { - NodeKind::Error { .. } => *error_nodes += 1, - NodeKind::Literal => *literals += 1, - _ => {} - } - node.children().for_each(|child| count(child, error_nodes, literals)); - } - - let (mut error_nodes, mut literals) = (0, 0); - nodes - .iter() - .for_each(|node| count(node, &mut error_nodes, &mut literals)); - - assert!(errors.has_errors(), "{code}"); - assert!(error_nodes >= 1, "{code}"); - assert!(literals >= valid_literals.min(2), "{code}: {literals}"); - } - - #[rstest] - #[case::after_def("def f(): 1; let y = f()")] - #[case::let_after_let("let x = 1 let y = 2")] - fn test_statements_need_no_separator(#[case] code: &str) { - let (_, errors) = crate::parse_recovery(code); - - assert!(!errors.has_errors(), "{code}: {errors}"); - } - - proptest::proptest! { - #[test] - fn prop_parse_recovery_never_panics_on_ascii(code in "[ -~\\n\\t]{0,120}") { - let _ = crate::parse_recovery(&code); - } - - #[test] - fn prop_parse_recovery_never_panics_on_token_soup( - tokens in proptest::collection::vec(proptest::sample::select(TOKEN_SOUP), 0..40) - ) { - let _ = crate::parse_recovery(&tokens.join(" ")); - } - - /// Parsing is lossless: every non-EOF token reappears in the CST, in order. - #[test] - fn prop_cst_keeps_every_token( - stmts in proptest::collection::vec(proptest::sample::select(STATEMENTS), 1..8), - seps in proptest::collection::vec(proptest::sample::select(SEPARATORS), 8), - ) { - let mut code = String::new(); - for (i, stmt) in stmts.iter().enumerate() { - if i > 0 { - code.push_str(seps[i]); - } - code.push_str(stmt); - } - let (nodes, errors) = crate::parse_recovery(&code); - proptest::prop_assert!(!errors.has_errors(), "{}: {}", code, errors); - - let (kept, expected) = kept_and_lexed_token_counts(&code, &nodes); - proptest::prop_assert_eq!(kept, expected, "{}", code); - } - - /// Recovery is lossless even for broken input: no token is dropped. - #[test] - fn prop_cst_keeps_every_token_on_token_soup( - tokens in proptest::collection::vec(proptest::sample::select(TOKEN_SOUP), 0..40) - ) { - let code = tokens.join(" "); - let (nodes, _) = crate::parse_recovery(&code); - // A program with no statements (e.g. only a comment) has no node to hang trivia on. - proptest::prop_assume!(!nodes.is_empty()); - let (kept, expected) = kept_and_lexed_token_counts(&code, &nodes); - proptest::prop_assert_eq!(kept, expected, "{}", code); - } - } - - /// Number of distinct token ranges found in `nodes`, and the number of tokens the lexer produced. - fn kept_and_lexed_token_counts(code: &str, nodes: &[Shared]) -> (usize, usize) { - fn collect(node: &Shared, out: &mut Vec) { - for trivia in node.leading_trivia.iter().chain(&node.trailing_trivia) { - if !trivia.is_new_line() { - out.push(trivia.range()); - } - } - if let Some(token) = node.token.as_ref().filter(|t| !matches!(t.kind, TokenKind::Eof)) { - out.push(token.range); - } - node.children().for_each(|child| collect(child, out)); - } - - let mut ranges = Vec::new(); - nodes.iter().for_each(|node| collect(node, &mut ranges)); - ranges.sort(); - ranges.dedup(); - - let expected = crate::Lexer::new(crate::lexer::Options { - ignore_errors: true, - include_spaces: true, - }) - .tokenize(code, crate::Module::TOP_LEVEL_MODULE_ID) - .unwrap() - .iter() - .filter(|t| !matches!(t.kind, TokenKind::Eof | TokenKind::NewLine)) - .count(); - - (ranges.len(), expected) - } - - /// Renders `node` fully parenthesized with the source symbol of each operator. - fn shape(node: &Node) -> String { - let text = |node: &Node| node.token.as_ref().map(|t| t.to_string()).unwrap_or_default(); - match &node.kind { - NodeKind::BinaryOp { lhs, rhs, .. } | NodeKind::Assign { lhs, rhs, .. } => { - format!("({} {} {})", shape(lhs), text(node), shape(rhs)) - } - NodeKind::UnaryOp { operand, .. } => format!("({} {})", text(node), shape(operand)), - NodeKind::As { expr, name } => format!("({} as {})", shape(expr), text(name)), - NodeKind::Group { expr, .. } => format!("<{}>", shape(expr)), - _ => text(node), - } - } - - fn parse_shape(code: &str) -> (String, bool) { - let (nodes, errors) = crate::parse_recovery(code); - let expressions: Vec<_> = nodes.iter().filter(|n| !matches!(n.kind, NodeKind::Eof)).collect(); - assert_eq!(expressions.len(), 1, "expected one expression for {code:?}"); - (shape(expressions[0]), errors.has_errors()) - } - - /// For each argument of the top-level call, the statement count of its body if it is a lambda, - /// and whether parsing reported errors. - fn lambda_arg_body_lens(code: &str) -> (Vec>, bool) { - let (nodes, errors) = crate::parse_recovery(code); - let expressions: Vec<_> = nodes.iter().filter(|n| !matches!(n.kind, NodeKind::Eof)).collect(); - let [call] = expressions.as_slice() else { - panic!("expected one expression for {code:?}"); - }; - let NodeKind::Call { args } = &call.kind else { - panic!("expected a call for {code:?}"); - }; - let lens = args - .iter() - .filter(|arg| !matches!(arg.kind, NodeKind::Token)) - .map(|arg| match &arg.kind { - NodeKind::Fn { program, .. } => Some( - program - .iter() - .filter(|node| !matches!(node.kind, NodeKind::Token | NodeKind::End)) - .count(), - ), - _ => None, - }) - .collect(); - (lens, errors.has_errors()) - } - - #[rstest] - #[case::only_arg("f(fn(x): x + 1)", vec![Some(1)])] - #[case::arrow("f(->(x): x + 1)", vec![Some(1)])] - #[case::last_arg("f(a, fn(x): x + 1)", vec![None, Some(1)])] - #[case::ends_at_comma("f(fn(x): x, 1)", vec![Some(1), None])] - #[case::lambda_between_args("f(a, fn(x): x, b)", vec![None, Some(1), None])] - #[case::two_lambdas("f(fn(x): x + 1, fn(x): x * 2)", vec![Some(1), Some(1)])] - #[case::pipe_stays_in_body("f(fn(x): x | g(.h))", vec![Some(2)])] - #[case::pipe_before_comma("f(fn(x): x | g(.h), 1)", vec![Some(2), None])] - #[case::call_with_commas_in_body("f(fn(x): g(x, 1, 2), 3)", vec![Some(1), None])] - #[case::array_in_body("f(fn(x): [x, 1], 2)", vec![Some(1), None])] - #[case::paren_in_body("f(fn(x): (x + 1) * 2, 3)", vec![Some(1), None])] - #[case::if_else_in_body("f(fn(x): if (x): 1 else: 2, 3)", vec![Some(1), None])] - #[case::do_end_in_body("f(fn(x): do 1 | 2 end, 3)", vec![Some(1), None])] - #[case::foreach_in_body("f(fn(x): foreach (i, x): i end, 3)", vec![Some(1), None])] - #[case::match_in_body("f(fn(x): match (x): | 1: 2 | _: 3 end, 4)", vec![Some(1), None])] - #[case::nested_call_lambda_then_comma("f(fn(x): g(x, fn(y): y + 1), 2)", vec![Some(1), None])] - #[case::multiline_body("f(fn(x):\n let y = x + 1\n | y * 2\n, 3)", vec![Some(2), None])] - #[case::lambda_in_param_default("f(fn(x = g(fn(y): y)): x, 1)", vec![Some(1), None])] - #[case::semicolon_form("f(fn(x): x + 1;)", vec![Some(1)])] - #[case::semicolon_form_then_arg("f(fn(x): x; , 1)", vec![Some(1), None])] - fn test_lambda_in_call_args_may_omit_terminator(#[case] code: &str, #[case] expected: Vec>) { - assert_eq!(lambda_arg_body_lens(code), (expected, false)); - } - - #[rstest] - #[case::only_arg("f(fn: self + 1)", vec![Some(1)])] - #[case::ends_at_comma("f(fn: self, 1)", vec![Some(1), None])] - #[case::pipe_before_comma("f(fn: g | h, 1)", vec![Some(2), None])] - #[case::call_with_commas_in_body("f(fn: g(1, 2), 3)", vec![Some(1), None])] - #[case::nested("f(fn: g(fn: self + 1), 2)", vec![Some(1), None])] - #[case::semicolon_form("f(fn: self + 1;)", vec![Some(1)])] - #[case::spaced_colon("f(fn : self)", vec![Some(1)])] - fn test_lambda_without_params(#[case] code: &str, #[case] expected: Vec>) { - assert_eq!(lambda_arg_body_lens(code), (expected, false)); - } - - #[test] - fn test_lambda_without_params_has_no_param_nodes() { - let (nodes, errors) = crate::parse_recovery("f(fn: self)"); - assert!(!errors.has_errors()); - let NodeKind::Call { args } = &nodes[0].kind else { - panic!("expected a call"); - }; - let Some(NodeKind::Fn { params, .. }) = args - .iter() - .map(|arg| &arg.kind) - .find(|k| matches!(k, NodeKind::Fn { .. })) - else { - panic!("expected a lambda"); - }; - assert!(params.is_empty()); - } - - #[rstest] - #[case::arrow_has_no_paramless_form("f(->: self)")] - #[case::empty_body("f(fn: )")] - #[case::unclosed_call("f(fn: self")] - fn test_lambda_without_params_reports_malformed_input(#[case] code: &str) { - let (_, errors) = crate::parse_recovery(code); - assert!(errors.has_errors(), "{code:?} should report an error"); - } - - #[rstest] - #[case::empty_body("f(fn(x): )")] - #[case::empty_body_then_comma("f(fn(x):, 1)")] - #[case::dangling_operator("f(fn(x): x +)")] - #[case::unclosed_call("f(fn(x): x + 1")] - #[case::stray_close_paren("f(fn(x): x + 1))")] - #[case::let_value("let g = fn(x): x + 1)")] - fn test_lambda_reports_malformed_input(#[case] code: &str) { - let (_, errors) = crate::parse_recovery(code); - assert!(errors.has_errors(), "{code:?} should report an error"); - } - - #[rstest] - #[case::paren_group("(fn(x): x + 1)(2)")] - #[case::array_element("[fn(x): x, 1]")] - #[case::implicit_array_element("[fn: self, 1]")] - #[case::dict_value("{\"a\": fn(x): x, \"b\": 1}")] - #[case::lambda_body_tail("f(fn(x): fn(y): x + y)")] - #[case::after_pipe("f(1 | fn(x): x + 1, 2)")] - fn test_lambda_ends_at_the_closer_of_its_enclosing_construct(#[case] code: &str) { - let (_, errors) = crate::parse_recovery(code); - assert!(!errors.has_errors(), "{code:?} should not report an error"); - } - - #[rstest] - #[case::do_block("do 1")] - #[case::def("def f(): 1")] - #[case::def_do("def f() do 1")] - #[case::fn_params("fn(x): x")] - #[case::fn_no_params("fn: 1")] - #[case::if_body("if (true): 1")] - #[case::after_pipe("1 | fn(x): x")] - fn test_top_level_body_may_end_at_eof(#[case] code: &str) { - let (_, errors) = crate::parse_recovery(code); - assert!(!errors.has_errors(), "{code:?}: {errors:?}"); - } - - #[rstest] - #[case::call("f(do 1", "`)`")] - #[case::array("[do 1", "`]`")] - #[case::group("(do 1", "`)`")] - fn test_body_at_eof_leaves_missing_closer_to_enclosing_construct(#[case] code: &str, #[case] expected: &str) { - let (_, errors) = crate::parse_recovery(code); - let errors = errors.to_vec(); - assert_eq!(errors.len(), 1, "{code:?}: {errors:?}"); - assert!( - matches!(&errors[0], ParseError::Missing { expected: e, .. } if *e == expected), - "{code:?}: {errors:?}" - ); - } - - #[rstest] - #[case::add("a + b", "(a + b)")] - #[case::sub("a - b", "(a - b)")] - #[case::mul("a * b", "(a * b)")] - #[case::div("a / b", "(a / b)")] - #[case::modulo("a % b", "(a % b)")] - #[case::eq("a == b", "(a == b)")] - #[case::ne("a != b", "(a != b)")] - #[case::lt("a < b", "(a < b)")] - #[case::lte("a <= b", "(a <= b)")] - #[case::gt("a > b", "(a > b)")] - #[case::gte("a >= b", "(a >= b)")] - #[case::and("a && b", "(a && b)")] - #[case::or("a || b", "(a || b)")] - #[case::left_shift("a << b", "(a << b)")] - #[case::right_shift("a >> b", "(a >> b)")] - #[case::convert("a @ b", "(a @ b)")] - #[case::range("a .. b", "(a .. b)")] - #[case::coalesce("a ?? b", "(a ?? b)")] - #[case::regex_match("a =~ b", "(a =~ b)")] - #[case::not_regex_match("a !~ b", "(a !~ b)")] - fn test_every_binary_operator_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), (expected.to_string(), false)); - } - - #[rstest] - #[case::and_over_or("a || b && c", "(a || (b && c))")] - #[case::or_after_and("a && b || c", "((a && b) || c)")] - #[case::comparison_over_and("a && b == c", "(a && (b == c))")] - #[case::and_after_comparison("a == b && c", "((a == b) && c)")] - #[case::additive_over_comparison("a == b + c", "(a == (b + c))")] - #[case::comparison_after_additive("a + b == c", "((a + b) == c)")] - #[case::multiplicative_over_additive("a + b * c", "(a + (b * c))")] - #[case::additive_after_multiplicative("a * b + c", "((a * b) + c)")] - #[case::range_over_multiplicative("a * b .. c", "(a * (b .. c))")] - #[case::multiplicative_after_range("a .. b * c", "((a .. b) * c)")] - #[case::coalesce_over_multiplicative("a * b ?? c", "(a * (b ?? c))")] - #[case::multiplicative_after_coalesce("a ?? b * c", "((a ?? b) * c)")] - #[case::convert_with_multiplicative("a + b @ c", "(a + (b @ c))")] - #[case::convert_is_left_associative_with_mul("a * b @ c", "((a * b) @ c)")] - #[case::shift_with_additive("a << b + c", "((a << b) + c)")] - #[case::additive_with_shift("a + b >> c", "((a + b) >> c)")] - #[case::regex_match_with_comparison("a =~ b == c", "((a =~ b) == c)")] - #[case::regex_over_and("a !~ b && c", "((a !~ b) && c)")] - #[case::three_levels("a || b && c == d + e * f .. g", "(a || (b && (c == (d + (e * (f .. g))))))")] - #[case::three_levels_reversed("a .. b * c + d == e && f || g", "((((((a .. b) * c) + d) == e) && f) || g)")] - fn test_binary_op_precedence_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), (expected.to_string(), false)); - } - - #[rstest] - #[case::or("a || b || c", "((a || b) || c)")] - #[case::and("a && b && c", "((a && b) && c)")] - #[case::comparison("a == b == c", "((a == b) == c)")] - #[case::mixed_comparison("a < b != c", "((a < b) != c)")] - #[case::sub("a - b - c", "((a - b) - c)")] - #[case::add_then_sub("a + b - c", "((a + b) - c)")] - #[case::shifts("a << b >> c", "((a << b) >> c)")] - #[case::div("a / b / c", "((a / b) / c)")] - #[case::mul_div_mod("a * b / c % d", "(((a * b) / c) % d)")] - #[case::range("a .. b .. c", "((a .. b) .. c)")] - #[case::coalesce("a ?? b ?? c", "((a ?? b) ?? c)")] - #[case::range_then_coalesce("a .. b ?? c", "((a .. b) ?? c)")] - #[case::coalesce_then_range("a ?? b .. c", "((a ?? b) .. c)")] - #[case::convert("a @ b @ c", "((a @ b) @ c)")] - fn test_binary_op_associativity_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), (expected.to_string(), false)); - } - - #[rstest] - #[case::parens_override_precedence("(a + b) * c", "(<(a + b)> * c)")] - #[case::parens_on_the_right("a * (b + c)", "(a * <(b + c)>)")] - #[case::parens_override_associativity("a - (b - c)", "(a - <(b - c)>)")] - #[case::neg_binds_tighter_than_mul("-a * b", "((- a) * b)")] - #[case::not_binds_tighter_than_eq("!a == b", "((! a) == b)")] - #[case::not_binds_tighter_than_and("!a && !b", "((! a) && (! b))")] - #[case::unary_on_the_right("a - -b", "(a - (- b))")] - #[case::not_on_the_right("a * !b", "(a * (! b))")] - #[case::as_binds_the_whole_expression("a + b * c as x", "((a + (b * c)) as x)")] - #[case::as_after_logic("a && b as x", "((a && b) as x)")] - fn test_operand_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), (expected.to_string(), false)); - } - - #[rstest] - #[case::assign("x = a", "(x = a)")] - #[case::assign_is_lowest("x = a || b && c == d + e * f", "(x = (a || (b && (c == (d + (e * f))))))")] - #[case::assign_takes_the_whole_expression("x = a + b * c", "(x = (a + (b * c)))")] - #[case::plus_assign("x += a * b", "(x += (a * b))")] - #[case::minus_assign("x -= a * b", "(x -= (a * b))")] - #[case::mul_assign("x *= a + b", "(x *= (a + b))")] - #[case::div_assign("x /= a + b", "(x /= (a + b))")] - #[case::mod_assign("x %= a + b", "(x %= (a + b))")] - #[case::floor_div_assign("x //= a + b", "(x //= (a + b))")] - fn test_assignment_shape(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), (expected.to_string(), false)); - } - - #[rstest] - #[case::trivia_between("a +\n b # c\n * d", "(a + (b * d))")] - #[case::trivia_before_operator("a\n + b", "(a + b)")] - #[case::trivia_in_assignment("x =\n a || b", "(x = (a || b))")] - fn test_binary_op_shape_with_trivia(#[case] code: &str, #[case] expected: &str) { - assert_eq!(parse_shape(code), (expected.to_string(), false)); - } - - #[rstest] - #[case::missing_right_operand("a +")] - #[case::missing_right_operand_after_assign("a =")] - #[case::missing_right_operand_after_logic("a &&")] - #[case::as_without_name("a as")] - #[case::as_with_non_identifier("a as 1")] - fn test_binary_op_errors(#[case] code: &str) { - let (nodes, errors) = crate::parse_recovery(code); - assert!(errors.has_errors(), "expected an error for {code:?}, got {nodes:?}"); - } -} +mod tests; diff --git a/crates/mq-lang/src/cst/parser/tests.rs b/crates/mq-lang/src/cst/parser/tests.rs new file mode 100644 index 000000000..5b82bfe5e --- /dev/null +++ b/crates/mq-lang/src/cst/parser/tests.rs @@ -0,0 +1,8563 @@ +use std::vec; + +use crate::{lexer::token::StringSegment, range::Range}; + +use super::*; +use crate::cst::node::BinaryOp; +use rstest::rstest; +use smallvec::smallvec; + +fn error_node(kinds: Vec) -> Shared { + Shared::new(Node { + kind: NodeKind::Error { + children: kinds + .into_iter() + .map(|kind| { + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(kind))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) + }) + .collect(), + }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) +} + +fn missing_node(expected: &'static str) -> Shared { + Shared::new(Node { + kind: NodeKind::Missing { + expected, + position: Default::default(), + }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) +} + +fn eof_node() -> Shared { + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) +} + +fn token(token_kind: TokenKind) -> Token { + Token { + range: Range::default(), + kind: token_kind, + module_id: 1.into(), + } +} + +#[rstest] +#[case::def( + vec![ + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Comment("test comment".into()))), + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("test comment2".into()))), + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment2".into()))))], + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1)))), + Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment".into()))))], + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_without_colon1( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: None, program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_without_colon2( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("baz".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::NumberLiteral(42.into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: None, program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_without_colon_with_args( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("func".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("func".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: None, program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::let_( + vec![ + Shared::new(token(TokenKind::Whitespace(4))), + Shared::new(token(TokenKind::Let)), + Shared::new(token(TokenKind::Whitespace(4))), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(42.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Let { lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), eq_token: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Let))), + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::unexpected_token( + vec![ + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Comment("test comment".into()))), + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("test comment2".into()))), + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment2".into()))))], + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1)))), + Trivia::Comment(Shared::new(token(TokenKind::Comment("test comment".into())))) ], + trailing_trivia: TriviaList::new(), + }), + error_node(vec![TokenKind::Comma]), + eof_node(), + ], + ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Comma)))], 100) + ) + )] +#[case::unexpected_eof( + vec![ + Shared::new(token(TokenKind::Whitespace(4))), + Shared::new(token(TokenKind::Let)), + Shared::new(token(TokenKind::Whitespace(4))), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Error { + children: smallvec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Let))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + }, + token: None, + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], + trailing_trivia: TriviaList::new(), + }), + eof_node(), + ], + ErrorReporter::with_error(vec![ParseError::UnexpectedEOFDetected], 100) + ) + )] +#[case::if_( + vec![ + Shared::new(token(TokenKind::If)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch".into()))), + Shared::new(token(TokenKind::Else)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("else_branch".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::If { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), elifs: Elifs::new(), else_branch: Some(Shared::new(Node { + kind: NodeKind::Else { colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("else_branch".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Else))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::If))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::unless_( + vec![ + Shared::new(token(TokenKind::Unless)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Unless { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Unless))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::if_elif_else( + vec![ + Shared::new(token(TokenKind::If)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition1".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch1".into()))), + Shared::new(token(TokenKind::Elif)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition2".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch2".into()))), + Shared::new(token(TokenKind::Else)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("else_branch".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::If { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), elifs: vec![ +Shared::new(Node { + kind: NodeKind::Elif { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Elif))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), +].into(), else_branch: Some(Shared::new(Node { + kind: NodeKind::Else { colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("else_branch".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Else))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::If))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::if_only( + vec![ + Shared::new(token(TokenKind::If)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition1".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch1".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::If { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), elifs: Elifs::new(), else_branch: None }, + token: Some(Shared::new(token(TokenKind::If))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::if_elif_else( + vec![ + Shared::new(token(TokenKind::If)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition1".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch1".into()))), + Shared::new(token(TokenKind::Elif)), + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition2".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch2".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::If { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), elifs: vec![ +Shared::new(Node { + kind: NodeKind::Elif { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Elif))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), +].into(), else_branch: None }, + token: Some(Shared::new(token(TokenKind::If))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::multiple_expr_with_trivia( + vec![ + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("y".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::args_with_function( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::foreach( + vec![ + Shared::new(token(TokenKind::Foreach)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("item".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("collection".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("comment".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Foreach { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("item".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("collection".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("comment".into())))), Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Foreach))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::foreach_do_end( + vec![ + Shared::new(token(TokenKind::Foreach)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("item".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("collection".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Foreach { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("item".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("collection".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Do, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Foreach))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::foreach_value_is_function_call( + vec![ + Shared::new(token(TokenKind::Foreach)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("item".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("get_items".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("arg".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Foreach { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("item".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("arg".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("get_items".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Foreach))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::while_( + vec![ + Shared::new(token(TokenKind::While)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("comment".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::Whitespace(4))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::While { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("comment".into())))), Trivia::NewLine], + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(4))))], + }), +].into() }, + token: Some(Shared::new(token(TokenKind::While))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::while_do_end( + vec![ + Shared::new(token(TokenKind::While)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::While { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Do, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::While))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::until_( + vec![ + Shared::new(token(TokenKind::Until)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("condition".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("body".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Until { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("condition".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Until))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::loop_( + vec![ + Shared::new(token(TokenKind::Loop)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("loop comment".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::Whitespace(2))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Loop { colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("loop comment".into())))), Trivia::NewLine], + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Loop))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector1( + vec![ + Shared::new(token(TokenKind::Selector(".h1".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".h1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector2( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector3( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector4( + vec![ + Shared::new(token(TokenKind::Selector(".list".into()))), + Shared::new(token(TokenKind::Selector(".checked".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".checked".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".list".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::ident_attribute_access( + vec![ + Shared::new(token(TokenKind::Ident("c".into()))), + Shared::new(token(TokenKind::Selector(".lang".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Ident { attr: Some(Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".lang".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Ident("c".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::self_attribute_access( + vec![ + Shared::new(token(TokenKind::Self_)), + Shared::new(token(TokenKind::Selector(".name".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Self_ { attr: Some(Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".name".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Self_))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::standalone_attr_selector( + vec![ + Shared::new(token(TokenKind::Selector(".lang".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelfAttr, + token: Some(Shared::new(token(TokenKind::Selector(".lang".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::include( + vec![ + Shared::new(token(TokenKind::Include)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::StringLiteral("module".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Include { path: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("module".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Include))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::import( + vec![ + Shared::new(token(TokenKind::Import)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::StringLiteral("module".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Import { path: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("module".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), as_token: None, alias: None }, + token: Some(Shared::new(token(TokenKind::Import))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::import_as( + vec![ + Shared::new(token(TokenKind::Import)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::StringLiteral("module".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::As)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("m".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Import { path: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("module".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), as_token: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::As))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + })), alias: Some(Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("m".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Import))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::root_with_token_after_semicolon( + vec![ + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Ident("y".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Ident("y".into()))))], 100) + ) + )] +#[case::call_with_newlines( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("param comment".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("arg1".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("arg2".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("arg1".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Comment(Shared::new(token(TokenKind::Comment("param comment".into())))), Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("arg2".into())))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::interpolated_string( + vec![ + Shared::new(token(TokenKind::InterpolatedString(vec![StringSegment::Expr("val".into(), Range::default()), StringSegment::Text("hello".into(), Range::default())]))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::InterpolatedString, + token: Some(Shared::new(token(TokenKind::InterpolatedString(vec![StringSegment::Expr("val".into(), Range::default()), StringSegment::Text("hello".into(), Range::default())])))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::nodes( + vec![ + Shared::new(token(TokenKind::Nodes)), + Shared::new(token(TokenKind::Whitespace(1))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Nodes, + token: Some(Shared::new(token(TokenKind::Nodes))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::fn_with_program( + vec![ + Shared::new(token(TokenKind::Fn)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("y".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Fn))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::fn_with_parameter_and_program( + vec![ + Shared::new(token(TokenKind::Fn)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("param".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::SemiColon)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("param".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Fn))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + + ], + ErrorReporter::default() + ) + )] +#[case::fn_with_multiple_parameters( + vec![ + Shared::new(token(TokenKind::Fn)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("param1".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("param2".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("result".into()))), + Shared::new(token(TokenKind::SemiColon)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("param1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("param2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("result".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Fn))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::array_empty( + vec![ + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::array_with_single_element( + vec![ + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(42.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::array_with_multiple_elements( + vec![ + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::StringLiteral("hello".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("hello".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::array_with_trivia( + vec![ + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Comment("array element".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::NumberLiteral(42.into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Whitespace(1))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), + leading_trivia: smallvec![ + Trivia::NewLine, + Trivia::Comment(Shared::new(token(TokenKind::Comment("array element".into())))), + Trivia::NewLine + ], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), +].into() }, + token: None, + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::array_nested( + vec![ + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::array_with_spread( + vec![ + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::DotDotDot)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Spread { operand: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::DotDotDot))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::dict_empty( + vec![ + Shared::new(token(TokenKind::LBrace)), + Shared::new(token(TokenKind::RBrace)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Dict { entries: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::dict_with_single_pair( + vec![ + Shared::new(token(TokenKind::LBrace)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::RBrace)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Dict { entries: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::DictEntry { key: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), value: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::dict_with_multiple_pairs( + vec![ + Shared::new(token(TokenKind::LBrace)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::StringLiteral("foo".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RBrace)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Dict { entries: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::DictEntry { key: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), value: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::DictEntry { key: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), value: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::dict_with_spread( + vec![ + Shared::new(token(TokenKind::LBrace)), + Shared::new(token(TokenKind::DotDotDot)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::RBrace)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Dict { entries: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Spread { operand: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::DotDotDot))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBrace))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::eq_eq( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::EqEq)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Equal, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::EqEq))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::ne_eq( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::NeEq)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::NotEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::NeEq))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::plus( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Plus)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Plus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::coalesce( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Coalesce)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Coalesce, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Coalesce))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::lt( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Lt)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Lt, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Lt))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::lte( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Lte)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Lte, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Lte))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::gt( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Gt)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Gt, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Gt))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::gte( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Gte)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Gte, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Gte))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::range( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::DoubleDot)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::RangeOp, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::DoubleDot))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::minus( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Minus)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Minus, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Minus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::division( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Slash)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Division, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Slash))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::percent( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Percent)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Modulo, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Percent))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::multiplication( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Asterisk)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Multiplication, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Asterisk))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::left_shift( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::LeftShift)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::LeftShift, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::LeftShift))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::right_shift( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::RightShift)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::RightShift, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::RightShift))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::convert( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Convert)), + Shared::new(token(TokenKind::Ident("b".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Convert, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Convert))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::multiple_binary_ops( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Plus)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::Minus)), + Shared::new(token(TokenKind::Ident("c".into()))), + Shared::new(token(TokenKind::Asterisk)), + Shared::new(token(TokenKind::Ident("d".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Minus, lhs: Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Plus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Multiplication, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("c".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("d".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Asterisk))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Minus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::binary_ops_with_trivia( + vec![ + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Plus)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("y".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Minus)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("z".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Minus, lhs: Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }) }, + token: Some(Shared::new(token(TokenKind::Plus))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("z".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Minus))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::string_literal_with_escape_sequences( + vec![ + Shared::new(token(TokenKind::StringLiteral("\\x1b[2J\\x1b[H".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("\\x1b[2J\\x1b[H".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::symbol_with_ident( + vec![ + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("foo".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Symbol { colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), name: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::symbol_with_string( + vec![ + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("bar".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Symbol { colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), name: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::not_operation( + vec![ + Shared::new(token(TokenKind::Not)), + Shared::new(token(TokenKind::BoolLiteral(true))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::UnaryOp { op: UnaryOp::Not, operand: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Not))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::break_in_loop( + vec![ + Shared::new(token(TokenKind::While)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::BoolLiteral(true))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Break)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::While { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Break { colon: None, value: None }, + token: Some(Shared::new(token(TokenKind::Break))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::While))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::break_in_loop_with_value( + vec![ + Shared::new(token(TokenKind::While)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::BoolLiteral(true))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Break)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::BoolLiteral(false))), + Shared::new(token(TokenKind::SemiColon)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::While { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Break { colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), value: Some(Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::BoolLiteral(false)))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Break))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::While))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::continue_in_loop( + vec![ + Shared::new(token(TokenKind::While)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::BoolLiteral(true))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Continue)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::While { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::BoolLiteral(true)))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Continue, + token: Some(Shared::new(token(TokenKind::Continue))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::While))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::break_outside_loop( + vec![ + Shared::new(token(TokenKind::Break)), + ], + ( + vec![error_node(vec![TokenKind::Break])], + ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Break)))], 100) + ) + )] +#[case::continue_outside_loop( + vec![ + Shared::new(token(TokenKind::Continue)), + ], + ( + vec![error_node(vec![TokenKind::Continue])], + ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::Continue)))], 100) + ) + )] +#[case::bracket_access_with_number( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(5.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(5.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::bracket_access_with_string( + vec![ + Shared::new(token(TokenKind::Ident("dict".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::StringLiteral("key".into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("key".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("dict".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::bracket_access_error_missing_rbracket( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(5.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(5.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + missing_node("`]`"), + ].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + eof_node(), + ], + ErrorReporter::with_error(vec![ParseError::Missing { expected: "`]`", found: Shared::new(token(TokenKind::Eof)) }], 100) + ) + )] +#[case::call_with_not_ident_arg( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Not)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::UnaryOp { op: UnaryOp::Not, operand: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Not))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::slice_access_with_numbers( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::slice_access_with_variables( + vec![ + Shared::new(token(TokenKind::Ident("items".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::Ident("start".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("end".into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("start".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("end".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("items".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::group_expr_single_ident( + vec![ + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Group { lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), expr: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::group_expr_binary_op( + vec![ + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Plus)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Group { lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), expr: Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Plus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::group_expr_with_trivia( + vec![ + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Whitespace(1))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Group { lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), expr: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), rparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }) }, + token: None, + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::group_expr_missing_rparen( + vec![ + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Group { + lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + expr: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + rparen: missing_node("`)`"), + }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + eof_node(), + ], + ErrorReporter::with_error(vec![ParseError::Missing { expected: "`)`", found: Shared::new(token(TokenKind::Eof)) }], 100) + ) + )] +// Test group expr with index access: (x)[0] +#[case::group_expr_with_index_access( + vec![ + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(0.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Group { lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), expr: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +// Test group expr with dynamic call: (x)("test") +#[case::group_expr_with_dynamic_call( + vec![ + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::StringLiteral("test".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::CallDynamic { callee: Shared::new(Node { + kind: NodeKind::Group { lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), expr: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +// Test array literal with index access: [1,2][0] +#[case::array_literal_with_index_access( + vec![ + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(0.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Array { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::if_with_grouped_binary_op_condition( + vec![ + Shared::new(token(TokenKind::If)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Plus)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("then_branch".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::If { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Group { lparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), expr: Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Plus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rparen: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), then_branch: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("then_branch".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), elifs: Elifs::new(), else_branch: None }, + token: Some(Shared::new(token(TokenKind::If))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), + ], + ErrorReporter::default() + ) + )] +#[case::self_bracket_access_with_number( + vec![ + Shared::new(token(TokenKind::Self_)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Self_))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_index_access( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::negate( + vec![ + Shared::new(token(TokenKind::Minus)), + Shared::new(token(TokenKind::NumberLiteral(42.into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::UnaryOp { op: UnaryOp::Negate, operand: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Minus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::do_block_simple( + vec![ + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::Ident("test".into()))), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("test".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::do_block_with_whitespace( + vec![ + Shared::new(token(TokenKind::Whitespace(2))), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Tab(1))), + Shared::new(token(TokenKind::Ident("expression".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("expression".into())))), + leading_trivia: smallvec![Trivia::NewLine, Trivia::Tab(Shared::new(token(TokenKind::Tab(1))))], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(2))))], + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::do_block_with_comment( + vec![ + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::Comment("inside block".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Ident("value".into()))), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("value".into())))), + leading_trivia: smallvec![Trivia::Comment(Shared::new(token(TokenKind::Comment("inside block".into())))), Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::do_block_empty( + vec![ + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: smallvec![Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })] }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::with_error(vec![ParseError::UnexpectedToken(Shared::new(token(TokenKind::End)))], 100) + ) + )] +#[case::do_block_nested( + vec![ + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::Ident("inner".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("inner".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::try_catch( + vec![ + Shared::new(token(TokenKind::Try)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("try_body".into()))), + Shared::new(token(TokenKind::Catch)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("catch_body".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), body: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("try_body".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), catch: Some(Shared::new(Node { + kind: NodeKind::Catch { params: None, colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), body: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("catch_body".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Catch))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Try))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::try_without_catch( + vec![ + Shared::new(token(TokenKind::Try)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("try_body".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), body: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("try_body".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), catch: None }, + token: Some(Shared::new(token(TokenKind::Try))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_question_mark( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Question)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Question))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::match_simple( + vec![ + Shared::new(token(TokenKind::Match)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("one".into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("_".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("other".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Match { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), arms: vec![ +Shared::new(Node { + kind: NodeKind::MatchArm { pipe: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), pattern: Shared::new(Node { + kind: NodeKind::Pattern { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), if_token: None, guard_args: None, colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), body: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("one".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::MatchArm { pipe: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), pattern: Shared::new(Node { + kind: NodeKind::Pattern { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Ident("_".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), if_token: None, guard_args: None, colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), body: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("other".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), end_token: Some(Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Match))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::match_do_end( + vec![ + Shared::new(token(TokenKind::Match)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("one".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("_".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("other".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Match { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Do, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), arms: vec![ +Shared::new(Node { + kind: NodeKind::MatchArm { pipe: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), pattern: Shared::new(Node { + kind: NodeKind::Pattern { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), if_token: None, guard_args: None, colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), body: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("one".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::MatchArm { pipe: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), pattern: Shared::new(Node { + kind: NodeKind::Pattern { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Ident("_".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), if_token: None, guard_args: None, colon: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), body: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("other".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: None, + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: TriviaList::new(), + }), +].into(), end_token: Some(Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Match))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::index_access_with_function_call( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(0.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::CallDynamic { callee: Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::qualified_access_simple( + vec![ + Shared::new(token(TokenKind::Ident("mod1".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Ident("value".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::QualifiedAccess { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("value".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::qualified_access_multi_level( + vec![ + Shared::new(token(TokenKind::Ident("mod1".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Ident("mod2".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Ident("value".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::QualifiedAccess { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("mod2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("value".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::qualified_access_with_call( + vec![ + Shared::new(token(TokenKind::Ident("mod1".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Ident("func".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::StringLiteral("arg".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::QualifiedAccess { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("func".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("arg".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::qualified_access_multi_level_with_call( + vec![ + Shared::new(token(TokenKind::Ident("mod1".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Ident("mod2".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Ident("func".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::QualifiedAccess { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("mod2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("func".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::qualified_access::empty( + vec![ + Shared::new(token(TokenKind::Ident("mod1".into()))), + Shared::new(token(TokenKind::DoubleColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::QualifiedAccess { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::DoubleColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("mod1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::module_simple( + vec![ + Shared::new(token(TokenKind::Module)), + Shared::new(token(TokenKind::Ident("modname1".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Let)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("bar".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Module { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("modname1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Let { lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), eq_token: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Let))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Module))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::slice_access_with_start_only( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_existing_and_non_existing( + vec![ + Shared::new(token(TokenKind::Selector(".h".into()))), + Shared::new(token(TokenKind::Selector(".value".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".value".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_property( + vec![ + Shared::new(token(TokenKind::Selector(".\"notfound\"".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"notfound\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_property_chained( + vec![ + Shared::new(token(TokenKind::Selector(".\"a\"".into()))), + Shared::new(token(TokenKind::Selector(".\"b\"".into()))), + Shared::new(token(TokenKind::Selector(".\"c\"".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"a\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"b\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"c\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"a\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_property_chained_two( + vec![ + Shared::new(token(TokenKind::Selector(".\"foo\"".into()))), + Shared::new(token(TokenKind::Selector(".\"bar\"".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"foo\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"bar\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"foo\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_property_iterator( + // ."items"[] โ†’ Block([Selector(."items"), Selector(. + [[ + ]]))] + vec![ + Shared::new(token(TokenKind::Selector(".\"items\"".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(Token { + kind: TokenKind::Selector(".".into()), + range: Range::default(), + module_id: 1.into(), + })), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_property_iterator_with_index( + // ."items"[0] โ†’ Block([Selector(."items"), Selector(. + [[ + 0 + ]]))] + vec![ + Shared::new(token(TokenKind::Selector(".\"items\"".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(0.0.into()))), + Shared::new(token(TokenKind::RBracket)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(Token { + kind: TokenKind::Selector(".".into()), + range: Range::default(), + module_id: 1.into(), + })), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".\"items\"".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_selector_attribute( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Selector(".level".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".level".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[rstest] +#[case::selector_dot_followed_by_pipe( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::Pipe)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_dot_followed_by_semicolon( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::SemiColon)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_dot_followed_by_end( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::End)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_dot_followed_by_binary_op( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::Plus)), + Shared::new(token(TokenKind::Ident("x".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::Plus, lhs: Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Plus))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::let_followed_by_let_without_pipe( + vec![ + Shared::new(token(TokenKind::Let)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Let)), + Shared::new(token(TokenKind::Ident("y".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Let { lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), eq_token: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Let))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Let { lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), eq_token: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Let))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + eof_node(), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_do_block_argument( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_implicit_pipeline_arg( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("baz".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_implicit_pipeline_arg_multi_step( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("c".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("c".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_implicit_pipeline_first_of_multiple_args( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("c".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("c".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_implicit_pipeline_second_arg( + // foo(x, a | b): pipeline is the second argument + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_implicit_pipeline_multiline( + // foo(a\n| b): pipeline spanning two lines + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::NewLine)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: smallvec![Trivia::NewLine], + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_implicit_pipeline_call_exprs( + // foo(bar() | baz()): function calls inside pipeline + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Whitespace(1))), + Shared::new(token(TokenKind::Ident("baz".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Block { program: vec![ +Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: smallvec![Trivia::Whitespace(Shared::new(token(TokenKind::Whitespace(1))))], + }), +Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: None, + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_try_argument( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Try)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), body: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), catch: None }, + token: Some(Shared::new(token(TokenKind::Try))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::call_with_try_catch_argument( + vec![ + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Try)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("bar".into()))), + Shared::new(token(TokenKind::Catch)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("baz".into()))), + Shared::new(token(TokenKind::RParen)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Try { colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), body: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("bar".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), catch: Some(Shared::new(Node { + kind: NodeKind::Catch { params: None, colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), body: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("baz".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Catch))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Try))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::assign( + vec![ + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(10.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::Assign, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(10.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::plus_equal( + vec![ + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::PlusEqual)), + Shared::new(token(TokenKind::NumberLiteral(5.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::PlusEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(5.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::PlusEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::minus_equal( + vec![ + Shared::new(token(TokenKind::Ident("y".into()))), + Shared::new(token(TokenKind::MinusEqual)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::MinusEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::MinusEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::star_equal( + vec![ + Shared::new(token(TokenKind::Ident("z".into()))), + Shared::new(token(TokenKind::StarEqual)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::MultiplicationEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("z".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::StarEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::slash_equal( + vec![ + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::SlashEqual)), + Shared::new(token(TokenKind::NumberLiteral(4.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::DivisionEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(4.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::SlashEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::percent_equal( + vec![ + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::PercentEqual)), + Shared::new(token(TokenKind::NumberLiteral(7.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::ModuloEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(7.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::PercentEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::double_slash_equal( + vec![ + Shared::new(token(TokenKind::Ident("c".into()))), + Shared::new(token(TokenKind::DoubleSlashEqual)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::DoubleDivisionEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("c".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::DoubleSlashEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::pipe_equal_with_selector( + vec![ + Shared::new(token(TokenKind::Selector(".h1".into()))), + Shared::new(token(TokenKind::Selector(".value".into()))), + Shared::new(token(TokenKind::PipeEqual)), + Shared::new(token(TokenKind::StringLiteral("new_id".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::PipeEqual, lhs: Shared::new(Node { + kind: NodeKind::Selector { items: vec![ +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".value".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("new_id".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::PipeEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::pipe_equal_with_ident_and_attr( + vec![ + Shared::new(token(TokenKind::Ident("obj".into()))), + Shared::new(token(TokenKind::Selector(".value".into()))), + Shared::new(token(TokenKind::PipeEqual)), + Shared::new(token(TokenKind::StringLiteral("John".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::PipeEqual, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: Some(Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".value".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Ident("obj".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("John".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::PipeEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::pipe_equal_with_self( + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::PipeEqual)), + Shared::new(token(TokenKind::NumberLiteral(42.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::PipeEqual, lhs: Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(42.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::PipeEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::var_then_assign( + vec![ + Shared::new(token(TokenKind::Var)), + Shared::new(token(TokenKind::Ident("count".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(0.into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("count".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(10.into()))), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("count".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Var { lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("count".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), eq_token: Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Var))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::Assign, lhs: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("count".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(10.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("count".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::index_assign( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(0.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(10.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::Assign, lhs: Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(10.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::index_compound_assign( + vec![ + Shared::new(token(TokenKind::Ident("arr".into()))), + Shared::new(token(TokenKind::LBracket)), + Shared::new(token(TokenKind::NumberLiteral(0.into()))), + Shared::new(token(TokenKind::RBracket)), + Shared::new(token(TokenKind::PlusEqual)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Assign { op: BinaryOp::PlusEqual, lhs: Shared::new(Node { + kind: NodeKind::Call { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(0.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RBracket))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Ident("arr".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::PlusEqual))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_with_default_param( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("greet".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("name".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("greeting".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::StringLiteral("Hello".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("greeting".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("greet".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("name".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), default: Some(Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("Hello".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Ident("greeting".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("greeting".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::fn_with_multiple_default_params( + vec![ + Shared::new(token(TokenKind::Fn)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("b".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("c".into()))), + Shared::new(token(TokenKind::Equal)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("a".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), default: Some(Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Ident("b".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Equal))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), default: Some(Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })) }, + token: Some(Shared::new(token(TokenKind::Ident("c".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("a".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Fn))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::arrow_with_program( + vec![ + Shared::new(token(TokenKind::Arrow)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("x".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("y".into()))), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("x".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Arrow))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("y".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::arrow_with_parameter_and_program( + vec![ + Shared::new(token(TokenKind::Arrow)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("param".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("body".into()))), + Shared::new(token(TokenKind::SemiColon)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("param".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("body".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Arrow))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::arrow_with_multiple_parameters( + vec![ + Shared::new(token(TokenKind::Arrow)), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("param1".into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::Ident("param2".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::StringLiteral("result".into()))), + Shared::new(token(TokenKind::SemiColon)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Fn { params: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("param1".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Param { asterisk: None, eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("param2".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into(), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("result".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Arrow))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_with_variadic_param( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("f".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Asterisk)), + Shared::new(token(TokenKind::Ident("args".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("args".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("f".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: Some(vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +// Variadic param: Ident node with Asterisk token as single child + Shared::new(Node { + kind: NodeKind::Param { asterisk: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Asterisk))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), eq_token: None, default: None }, + token: Some(Shared::new(token(TokenKind::Ident("args".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into()), colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("args".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_without_params( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("f".into()))), + Shared::new(token(TokenKind::Colon)), + Shared::new(token(TokenKind::Ident("args".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("f".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: None, colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Colon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("args".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::def_without_params_with_do( + vec![ + Shared::new(token(TokenKind::Def)), + Shared::new(token(TokenKind::Ident("f".into()))), + Shared::new(token(TokenKind::Do)), + Shared::new(token(TokenKind::Ident("args".into()))), + Shared::new(token(TokenKind::SemiColon)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Def { name: Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("f".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), params: None, colon_or_do: Some(Shared::new(Node { + kind: NodeKind::Do, + token: Some(Shared::new(token(TokenKind::Do))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + })), program: vec![ +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("args".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::SemiColon))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Def))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_call_heading_single_arg( + vec![ + Shared::new(token(TokenKind::Selector(".h".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelectorCall { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_call_heading_multi_arg( + vec![ + Shared::new(token(TokenKind::Selector(".h".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::Comma)), + Shared::new(token(TokenKind::NumberLiteral(2.into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelectorCall { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Comma))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(2.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_call_code_lang( + vec![ + Shared::new(token(TokenKind::Selector(".code".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::StringLiteral("rust".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelectorCall { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::StringLiteral("rust".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".code".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_call_heading_range( + vec![ + Shared::new(token(TokenKind::Selector(".h".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::DoubleDot)), + Shared::new(token(TokenKind::NumberLiteral(3.into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelectorCall { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +// 1..3 is parsed as a BinaryOp(RangeOp) expression by parse_expr + Shared::new(Node { + kind: NodeKind::BinaryOp { op: BinaryOp::RangeOp, lhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), rhs: Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(3.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }) }, + token: Some(Shared::new(token(TokenKind::DoubleDot))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_call_ident_arg( + vec![ + Shared::new(token(TokenKind::Selector(".h".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::Ident("n".into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelectorCall { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("n".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::selector_call_with_attribute( + vec![ + Shared::new(token(TokenKind::Selector(".h".into()))), + Shared::new(token(TokenKind::LParen)), + Shared::new(token(TokenKind::NumberLiteral(1.into()))), + Shared::new(token(TokenKind::RParen)), + Shared::new(token(TokenKind::Selector(".depth".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::SelectorCall { args: vec![ +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::LParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Literal, + token: Some(Shared::new(token(TokenKind::NumberLiteral(1.into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::RParen))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +Shared::new(Node { + kind: NodeKind::Selector { items: ArgList::new() }, + token: Some(Shared::new(token(TokenKind::Selector(".depth".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), +].into() }, + token: Some(Shared::new(token(TokenKind::Selector(".h".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Eof, + token: Some(Shared::new(token(TokenKind::Eof))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + ], + ErrorReporter::default() + ) + )] +#[case::unmatched_end_at_root( + // `. end end eof`: the first `end` closes a root statement, the second `end` has no + // matching open block and should produce `UnmatchedEnd` pointing at the first `end`. + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + eof_node(), + ], + ErrorReporter::with_error(vec![ParseError::UnmatchedEnd(Shared::new(token(TokenKind::End)))], 100) + ) + )] +#[case::unmatched_end_at_root_pipe_follows( + // `. end end | ident eof`: mirrors the real-world `def foo: if (.): . end end | foo` + // scenario where the extra `end` should be flagged, not the pipe after it. + vec![ + Shared::new(token(TokenKind::Selector(".".into()))), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::End)), + Shared::new(token(TokenKind::Pipe)), + Shared::new(token(TokenKind::Ident("foo".into()))), + Shared::new(token(TokenKind::Eof)), + ], + ( + vec![ + Shared::new(Node { + kind: NodeKind::Self_ { attr: None }, + token: Some(Shared::new(token(TokenKind::Selector(".".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::End, + token: Some(Shared::new(token(TokenKind::End))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Token, + token: Some(Shared::new(token(TokenKind::Pipe))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + Shared::new(Node { + kind: NodeKind::Ident { attr: None }, + token: Some(Shared::new(token(TokenKind::Ident("foo".into())))), + leading_trivia: TriviaList::new(), + trailing_trivia: TriviaList::new(), + }), + eof_node(), + ], + ErrorReporter::with_error(vec![ParseError::UnmatchedEnd(Shared::new(token(TokenKind::End)))], 100) + ) + )] + +fn test_parse(#[case] input: Vec>, #[case] expected: (Vec>, ErrorReporter)) { + let (nodes, errors) = Parser::new(&input).parse(); + assert_eq!(errors, expected.1); + assert_eq!(nodes, expected.0); +} + +#[test] +fn test_module_block_can_follow_a_def() { + let (nodes, errors) = crate::parse_recovery("def a(): 1;\nmodule m:\n def b(): 2;\nend\n"); + assert!(!errors.has_errors()); + assert!(nodes.iter().any(|node| matches!(node.kind, NodeKind::Module { .. }))); +} + +#[test] +fn test_descendant_chain_has_no_synthetic_bridge_node() { + // No synthetic DoubleDot node: it would duplicate the next selector's source range. + let (nodes, errors) = crate::parse_recovery(".blockquote .code"); + assert!(!errors.has_errors()); + + let block = nodes + .iter() + .find(|node| matches!(node.kind, NodeKind::Block { .. })) + .expect("descendant chain should parse as a Block"); + let children = block.all_children(); + + assert_eq!( + children.len(), + 2, + "expected exactly the two real selectors, got {:#?}", + children + ); + assert!( + children + .iter() + .all(|child| !matches!(child.token.as_ref().map(|t| &t.kind), Some(TokenKind::DoubleDot))), + "no child should carry a synthetic DoubleDot token: {:#?}", + children + ); +} + +#[test] +fn test_unmatched_end_error_message() { + // Verify the error message text for `UnmatchedEnd`. + let err = ParseError::UnmatchedEnd(Shared::new(token(TokenKind::End))); + assert_eq!(err.to_string(), "Unexpected `end` keyword โ€” no open block to close"); +} + +#[test] +fn test_unmatched_end_error_ranges() { + // Verify that `error_ranges` correctly extracts the range from an `UnmatchedEnd` error. + let end_token = Shared::new(Token { + range: Range { + start: Position { line: 1, column: 20 }, + end: Position { line: 1, column: 23 }, + }, + kind: TokenKind::End, + module_id: 1.into(), + }); + let mut reporter = ErrorReporter::new(100); + reporter.report(ParseError::UnmatchedEnd(Shared::clone(&end_token))); + + let ranges = reporter.error_ranges("def foo: if (.): . end end"); + assert_eq!(ranges.len(), 1); + assert_eq!(ranges[0].1.start.line, 1); + assert_eq!(ranges[0].1.start.column, 20); + assert!(ranges[0].0.contains("end")); +} + +#[test] +fn test_error_reporter_error_ranges() { + let text = "def foo():\n bar()\n"; + + let mut reporter = ErrorReporter::new(100); + + let unexpected_token = Shared::new(Token { + range: Range { + start: Position { line: 1, column: 4 }, + end: Position { line: 1, column: 7 }, + }, + kind: TokenKind::Ident("foo".into()), + module_id: 1.into(), + }); + reporter.report(ParseError::UnexpectedToken(Shared::clone(&unexpected_token))); + + let insufficient_token = Shared::new(Token { + range: Range { + start: Position { line: 1, column: 4 }, + end: Position { line: 1, column: 7 }, + }, + kind: TokenKind::Ident("bar".into()), + module_id: 1.into(), + }); + reporter.report(ParseError::InsufficientTokens(Shared::clone(&insufficient_token))); + + reporter.report(ParseError::UnexpectedEOFDetected); + + let ranges = reporter.error_ranges(text); + + assert_eq!(ranges.len(), 3); + + assert_eq!(ranges[0].1.start.line, 1); + assert_eq!(ranges[0].1.start.column, 4); + assert_eq!(ranges[0].1.end.line, 1); + assert_eq!(ranges[0].1.end.column, 7); + + assert_eq!(ranges[1].1.start.line, 1); + assert_eq!(ranges[1].1.start.column, 4); + assert_eq!(ranges[1].1.end.line, 1); + assert_eq!(ranges[1].1.end.column, 7); + + assert_eq!(ranges[2].1.start.line, 2); + assert_eq!(ranges[2].1.start.column, 6); + assert_eq!(ranges[2].1.end.line, 2); + assert_eq!(ranges[2].1.end.column, 6); +} + +#[test] +fn test_error_reporter_diagnostics_carries_hints_and_ranges() { + let text = "def foo():\n bar()\n"; + + let mut reporter = ErrorReporter::new(100); + + let unexpected_token = Shared::new(Token { + range: Range { + start: Position { line: 1, column: 4 }, + end: Position { line: 1, column: 7 }, + }, + kind: TokenKind::Ident("foo".into()), + module_id: 1.into(), + }); + reporter.report(ParseError::UnexpectedToken(Shared::clone(&unexpected_token))); + reporter.report(ParseError::UnexpectedEOFDetected); + + let diagnostics = reporter.diagnostics(text); + + assert_eq!(diagnostics.len(), 2); + + assert_eq!( + diagnostics[0].message, + ParseError::UnexpectedToken(unexpected_token).to_string() + ); + assert_eq!(diagnostics[0].range.unwrap().start.column, 4); + assert_eq!(diagnostics[0].hints.len(), 1); + + assert_eq!(diagnostics[1].message, ParseError::UnexpectedEOFDetected.to_string()); + assert_eq!(diagnostics[1].hints.len(), 1); +} + +#[test] +fn test_error_reporter_diagnostics_unknown_selector_hint_matches_error_help() { + let mut reporter = ErrorReporter::new(100); + reporter.report(ParseError::UnknownSelector(crate::selector::UnknownSelector::new( + Token { + range: Range::default(), + kind: TokenKind::Selector(smol_str::SmolStr::new(".hedaing")), + module_id: 1.into(), + }, + ))); + + let diagnostics = reporter.diagnostics(""); + assert_eq!( + diagnostics[0].hints[0], + "Unknown selector `.hedaing`. A selector with a similar name exists: `.heading`." + ); +} + +#[test] +fn test_error_reporter_has_errors() { + let mut reporter = ErrorReporter::new(100); + assert!(!reporter.has_errors()); + + reporter.report(ParseError::UnexpectedEOFDetected); + assert!(reporter.has_errors()); +} + +#[test] +fn test_error_reporter_max_errors() { + let mut reporter = ErrorReporter::new(2); + + reporter.report(ParseError::UnexpectedToken(Shared::new(Token { + range: Range::default(), + kind: TokenKind::Ident("foo".into()), + module_id: 1.into(), + }))); + reporter.report(ParseError::UnexpectedToken(Shared::new(Token { + range: Range::default(), + kind: TokenKind::Ident("bar".into()), + module_id: 1.into(), + }))); + reporter.report(ParseError::UnexpectedEOFDetected); + + assert_eq!(reporter.errors.len(), 2); +} + +#[test] +fn test_error_reporter_unexpected_eof_detected() { + let mut reporter = ErrorReporter::new(2); + + reporter.report(ParseError::UnexpectedEOFDetected); + reporter.report(ParseError::UnexpectedEOFDetected); + + assert_eq!(reporter.errors.len(), 1); +} + +#[test] +fn test_error_reporter_display() { + let mut reporter = ErrorReporter::new(100); + + reporter.report(ParseError::UnexpectedEOFDetected); + let token = Shared::new(Token { + range: Range::default(), + kind: TokenKind::Ident("foo".into()), + module_id: 1.into(), + }); + reporter.report(ParseError::UnexpectedToken(token)); + + let display = format!("{}", reporter); + assert!(display.contains("Unexpected EOF detected")); + assert!(display.contains("Unexpected token")); +} + +#[rstest] +#[case::binary_op_without_rhs("1 +")] +#[case::as_without_name("1 as")] +#[case::as_with_non_ident("1 as 2")] +#[case::let_only("let")] +#[case::let_without_rhs("let x =")] +#[case::var_only("var")] +#[case::def_only("def")] +#[case::def_without_body("def f(x):")] +#[case::fn_only("fn")] +#[case::include_only("include")] +#[case::import_only("import")] +#[case::import_as_without_alias("import \"csv\" as")] +#[case::if_only("if")] +#[case::if_without_then("if (1):")] +#[case::elif_without_if("elif (1): 1")] +#[case::match_only("match")] +#[case::match_arm_without_body("match (1): | x:")] +#[case::foreach_open_paren("foreach (")] +#[case::try_only("try")] +#[case::selector_bracket_open(".[")] +#[case::dict_unclosed("{\"a\": ")] +#[case::dict_trailing_comma_unclosed("{\"a\": 1, ")] +#[case::array_unclosed("[1, ")] +#[case::call_unclosed("f(1, ")] +#[case::trailing_pipe("1 |")] +#[case::unmatched_end("end")] +fn test_truncated_input_reports_error(#[case] code: &str) { + let (_, errors) = crate::parse_recovery(code); + assert!(errors.has_errors(), "{code}"); +} + +#[rstest] +#[case::single_line("{\"a\": 1,}")] +#[case::multi_entry("{\"a\": 1, \"b\": [1],}")] +#[case::multi_line("{\n \"a\": 1,\n # c\n}")] +#[case::spread("{...x, \"a\": 1,}")] +fn test_dict_trailing_comma(#[case] code: &str) { + let (nodes, errors) = crate::parse_recovery(code); + assert!(!errors.has_errors(), "{code}: {errors:?}"); + assert!(matches!(nodes[0].kind, NodeKind::Dict { .. }), "{code}"); +} + +#[rstest] +#[case::string("\"abc\"[1]")] +#[case::string_slice("\"abc\"[1:2]")] +#[case::interpolated("s\"a${1}\"[0]")] +fn test_string_literal_index(#[case] code: &str) { + let (nodes, errors) = crate::parse_recovery(code); + assert!(!errors.has_errors(), "{code}: {errors:?}"); + assert!(matches!(nodes[0].kind, NodeKind::Call { .. }), "{code}"); +} + +#[rstest] +#[case::string_key("{\"a\": 1}[\"a\"]")] +#[case::symbol_key("{\"a\": 1}[:a]")] +#[case::chained("{\"a\": {\"b\": 2}}[\"a\"][\"b\"]")] +fn test_dict_literal_index(#[case] code: &str) { + let (nodes, errors) = crate::parse_recovery(code); + assert!(!errors.has_errors(), "{code}: {errors:?}"); + assert!(matches!(nodes[0].kind, NodeKind::Call { .. }), "{code}"); +} + +#[rstest] +#[case::dict_newline("{\"a\": 1}\n[\"a\"]")] +#[case::dict_comment("{\"a\": 1} # c\n[\"a\"]")] +#[case::array_newline("[1, 2]\n[0]")] +#[case::string_newline("\"abc\"\n[0]")] +#[case::ident_newline("x\n[0]")] +#[case::call_newline("foo(1)\n[0]")] +#[case::dynamic_call_newline("x\n(1)")] +fn test_postfix_across_newline(#[case] code: &str) { + let (nodes, errors) = crate::parse_recovery(code); + assert!(!errors.has_errors(), "{code}: {errors:?}"); + let exprs: Vec<_> = nodes.iter().filter(|n| !matches!(n.kind, NodeKind::Eof)).collect(); + assert_eq!(exprs.len(), 1, "{code}"); + assert!( + matches!(exprs[0].kind, NodeKind::Call { .. } | NodeKind::CallDynamic { .. }), + "{code}" + ); +} + +const TOKEN_SOUP: &[&str] = &[ + "def", + "fn", + "let", + "var", + "if", + "elif", + "else", + "unless", + "while", + "until", + "loop", + "foreach", + "match", + "try", + "catch", + "do", + "end", + "module", + "import", + "include", + "as", + "break", + "continue", + "yield", + "self", + "nodes", + "(", + ")", + "[", + "]", + "{", + "}", + ":", + ";", + ",", + "|", + "=", + "+", + "-", + "*", + "/", + "==", + "..", + ".", + ".h1", + ".[0]", + "x", + "f", + "1", + "\"s\"", + "s\"${x}\"", + ":sym", + "$ENV", + "#c\n", + "\n", + " ", +]; + +const STATEMENTS: &[&str] = &[ + "def f(x, y = 1):\n x + y;", + "let a = [1, 2]", + "if (a):\t1 elif (b): 2 else: 3", + "match (x): | [a, ..rest]: a | {k}: k | _: 0 end", + "foreach (v, a): v;", + "try: 1 catch(e): e", + ".h1.value", + ".[0][1]", + "{\"a\": 1, ...b}", + "fn(x): x;", + "s\"${a} b\"", + "import \"csv\" as c", + "module m: def g(): 1; end", + "while (a): break: 1;", + "loop do continue end", + "1 as n", + "x += 1", + "(a)(1)", +]; + +const SEPARATORS: &[&str] = &[" | ", "\n| ", " |\n ", "\t|\t", " | # comment\n", "\n\n| "]; + +/// Broken input still yields a tree: the well-formed parts survive, and the +/// damage is confined to `Error`/`Missing` nodes. +#[rstest] +#[case::unclosed_call("foo(1, 2,", "foo", "`)`")] +#[case::unclosed_group("(x", "(", "`)`")] +#[case::unclosed_array("[1, 2", "[", "`]`")] +#[case::unclosed_dict("{a: 1, b: 2", "{", "`}`")] +#[case::unclosed_bracket_access("arr[5", "arr", "`]`")] +fn test_unclosed_delimiter_becomes_missing_node(#[case] code: &str, #[case] kept: &str, #[case] expected: &str) { + let (nodes, errors) = crate::parse_recovery(code); + + assert!(errors.has_errors(), "{code}"); + assert!( + nodes + .iter() + .any(|node| node.to_string() == kept || node.children().any(|c| c.to_string() == kept)), + "{code}: {nodes:?}" + ); + fn has_missing(node: &Shared, expected: &str) -> bool { + matches!(&node.kind, NodeKind::Missing { expected: e, .. } if *e == expected) + || node.children().any(|child| has_missing(child, expected)) + } + assert!( + nodes.iter().any(|node| has_missing(node, expected)), + "{code}: {nodes:?}" + ); +} + +#[rstest] +#[case::leading("foo(, 1)")] +#[case::consecutive("foo(1,, 2)")] +#[case::trailing_pair("foo(1, ,)")] +#[case::only_comma("foo(,)")] +#[case::array_leading("[, 1]")] +fn test_absent_item_before_separator_has_error(#[case] code: &str) { + let (nodes, errors) = crate::parse_recovery(code); + + assert!(errors.has_errors(), "{code}"); + assert!(nodes.iter().any(|node| node.has_error()), "{code}: {nodes:?}"); + let commas: usize = nodes + .iter() + .flat_map(|node| node.children()) + .filter(|child| matches!(child.token.as_ref().map(|t| &t.kind), Some(TokenKind::Comma))) + .count(); + assert_eq!(commas, code.matches(',').count(), "{code}: separators must be kept"); +} + +#[rstest] +#[case::unclosed_call("foo(1,", Position { line: 1, column: 7 })] +#[case::unclosed_call_next_line("foo(1,\n", Position { line: 1, column: 7 })] +#[case::cursor_on_next_line("foo(1,\n ", Position { line: 2, column: 2 })] +fn test_unclosed_call_range_covers_cursor(#[case] code: &str, #[case] cursor: Position) { + let (nodes, _) = crate::parse_recovery(code); + let call = nodes + .iter() + .find(|node| matches!(node.kind, NodeKind::Call { .. })) + .unwrap_or_else(|| panic!("{code}: {nodes:?}")); + + assert!(call.node_range().contains(&cursor), "{code}: {:?}", call.node_range()); +} + +#[rstest] +#[case::junk_between_items("foo(1 2, 3)", 3)] +#[case::invalid_item("foo(1, ยง, 3) | bar", 3)] +#[case::stray_closer("foo(1) ) bar(2)", 1)] +fn test_junk_is_kept_in_error_node(#[case] code: &str, #[case] valid_literals: usize) { + let (nodes, errors) = crate::parse_recovery(code); + + fn count(node: &Shared, error_nodes: &mut usize, literals: &mut usize) { + match node.kind { + NodeKind::Error { .. } => *error_nodes += 1, + NodeKind::Literal => *literals += 1, + _ => {} + } + node.children().for_each(|child| count(child, error_nodes, literals)); + } + + let (mut error_nodes, mut literals) = (0, 0); + nodes + .iter() + .for_each(|node| count(node, &mut error_nodes, &mut literals)); + + assert!(errors.has_errors(), "{code}"); + assert!(error_nodes >= 1, "{code}"); + assert!(literals >= valid_literals.min(2), "{code}: {literals}"); +} + +#[rstest] +#[case::after_def("def f(): 1; let y = f()")] +#[case::let_after_let("let x = 1 let y = 2")] +fn test_statements_need_no_separator(#[case] code: &str) { + let (_, errors) = crate::parse_recovery(code); + + assert!(!errors.has_errors(), "{code}: {errors}"); +} + +proptest::proptest! { + #[test] + fn prop_parse_recovery_never_panics_on_ascii(code in "[ -~\\n\\t]{0,120}") { + let _ = crate::parse_recovery(&code); + } + + #[test] + fn prop_parse_recovery_never_panics_on_token_soup( + tokens in proptest::collection::vec(proptest::sample::select(TOKEN_SOUP), 0..40) + ) { + let _ = crate::parse_recovery(&tokens.join(" ")); + } + + /// Parsing is lossless: every non-EOF token reappears in the CST, in order. + #[test] + fn prop_cst_keeps_every_token( + stmts in proptest::collection::vec(proptest::sample::select(STATEMENTS), 1..8), + seps in proptest::collection::vec(proptest::sample::select(SEPARATORS), 8), + ) { + let mut code = String::new(); + for (i, stmt) in stmts.iter().enumerate() { + if i > 0 { + code.push_str(seps[i]); + } + code.push_str(stmt); + } + let (nodes, errors) = crate::parse_recovery(&code); + proptest::prop_assert!(!errors.has_errors(), "{}: {}", code, errors); + + let (kept, expected) = kept_and_lexed_token_counts(&code, &nodes); + proptest::prop_assert_eq!(kept, expected, "{}", code); + } + + /// Recovery is lossless even for broken input: no token is dropped. + #[test] + fn prop_cst_keeps_every_token_on_token_soup( + tokens in proptest::collection::vec(proptest::sample::select(TOKEN_SOUP), 0..40) + ) { + let code = tokens.join(" "); + let (nodes, _) = crate::parse_recovery(&code); + // A program with no statements (e.g. only a comment) has no node to hang trivia on. + proptest::prop_assume!(!nodes.is_empty()); + let (kept, expected) = kept_and_lexed_token_counts(&code, &nodes); + proptest::prop_assert_eq!(kept, expected, "{}", code); + } +} + +/// Number of distinct token ranges found in `nodes`, and the number of tokens the lexer produced. +fn kept_and_lexed_token_counts(code: &str, nodes: &[Shared]) -> (usize, usize) { + fn collect(node: &Shared, out: &mut Vec) { + for trivia in node.leading_trivia.iter().chain(&node.trailing_trivia) { + if !trivia.is_new_line() { + out.push(trivia.range()); + } + } + if let Some(token) = node.token.as_ref().filter(|t| !matches!(t.kind, TokenKind::Eof)) { + out.push(token.range); + } + node.children().for_each(|child| collect(child, out)); + } + + let mut ranges = Vec::new(); + nodes.iter().for_each(|node| collect(node, &mut ranges)); + ranges.sort(); + ranges.dedup(); + + let expected = crate::Lexer::new(crate::lexer::Options { + ignore_errors: true, + include_spaces: true, + }) + .tokenize(code, crate::Module::TOP_LEVEL_MODULE_ID) + .unwrap() + .iter() + .filter(|t| !matches!(t.kind, TokenKind::Eof | TokenKind::NewLine)) + .count(); + + (ranges.len(), expected) +} + +/// Renders `node` fully parenthesized with the source symbol of each operator. +fn shape(node: &Node) -> String { + let text = |node: &Node| node.token.as_ref().map(|t| t.to_string()).unwrap_or_default(); + match &node.kind { + NodeKind::BinaryOp { lhs, rhs, .. } | NodeKind::Assign { lhs, rhs, .. } => { + format!("({} {} {})", shape(lhs), text(node), shape(rhs)) + } + NodeKind::UnaryOp { operand, .. } => format!("({} {})", text(node), shape(operand)), + NodeKind::As { expr, name } => format!("({} as {})", shape(expr), text(name)), + NodeKind::Group { expr, .. } => format!("<{}>", shape(expr)), + _ => text(node), + } +} + +fn parse_shape(code: &str) -> (String, bool) { + let (nodes, errors) = crate::parse_recovery(code); + let expressions: Vec<_> = nodes.iter().filter(|n| !matches!(n.kind, NodeKind::Eof)).collect(); + assert_eq!(expressions.len(), 1, "expected one expression for {code:?}"); + (shape(expressions[0]), errors.has_errors()) +} + +/// For each argument of the top-level call, the statement count of its body if it is a lambda, +/// and whether parsing reported errors. +fn lambda_arg_body_lens(code: &str) -> (Vec>, bool) { + let (nodes, errors) = crate::parse_recovery(code); + let expressions: Vec<_> = nodes.iter().filter(|n| !matches!(n.kind, NodeKind::Eof)).collect(); + let [call] = expressions.as_slice() else { + panic!("expected one expression for {code:?}"); + }; + let NodeKind::Call { args } = &call.kind else { + panic!("expected a call for {code:?}"); + }; + let lens = args + .iter() + .filter(|arg| !matches!(arg.kind, NodeKind::Token)) + .map(|arg| match &arg.kind { + NodeKind::Fn { program, .. } => Some( + program + .iter() + .filter(|node| !matches!(node.kind, NodeKind::Token | NodeKind::End)) + .count(), + ), + _ => None, + }) + .collect(); + (lens, errors.has_errors()) +} + +#[rstest] +#[case::only_arg("f(fn(x): x + 1)", vec![Some(1)])] +#[case::arrow("f(->(x): x + 1)", vec![Some(1)])] +#[case::last_arg("f(a, fn(x): x + 1)", vec![None, Some(1)])] +#[case::ends_at_comma("f(fn(x): x, 1)", vec![Some(1), None])] +#[case::lambda_between_args("f(a, fn(x): x, b)", vec![None, Some(1), None])] +#[case::two_lambdas("f(fn(x): x + 1, fn(x): x * 2)", vec![Some(1), Some(1)])] +#[case::pipe_stays_in_body("f(fn(x): x | g(.h))", vec![Some(2)])] +#[case::pipe_before_comma("f(fn(x): x | g(.h), 1)", vec![Some(2), None])] +#[case::call_with_commas_in_body("f(fn(x): g(x, 1, 2), 3)", vec![Some(1), None])] +#[case::array_in_body("f(fn(x): [x, 1], 2)", vec![Some(1), None])] +#[case::paren_in_body("f(fn(x): (x + 1) * 2, 3)", vec![Some(1), None])] +#[case::if_else_in_body("f(fn(x): if (x): 1 else: 2, 3)", vec![Some(1), None])] +#[case::do_end_in_body("f(fn(x): do 1 | 2 end, 3)", vec![Some(1), None])] +#[case::foreach_in_body("f(fn(x): foreach (i, x): i end, 3)", vec![Some(1), None])] +#[case::match_in_body("f(fn(x): match (x): | 1: 2 | _: 3 end, 4)", vec![Some(1), None])] +#[case::nested_call_lambda_then_comma("f(fn(x): g(x, fn(y): y + 1), 2)", vec![Some(1), None])] +#[case::multiline_body("f(fn(x):\n let y = x + 1\n | y * 2\n, 3)", vec![Some(2), None])] +#[case::lambda_in_param_default("f(fn(x = g(fn(y): y)): x, 1)", vec![Some(1), None])] +#[case::semicolon_form("f(fn(x): x + 1;)", vec![Some(1)])] +#[case::semicolon_form_then_arg("f(fn(x): x; , 1)", vec![Some(1), None])] +fn test_lambda_in_call_args_may_omit_terminator(#[case] code: &str, #[case] expected: Vec>) { + assert_eq!(lambda_arg_body_lens(code), (expected, false)); +} + +#[rstest] +#[case::only_arg("f(fn: self + 1)", vec![Some(1)])] +#[case::ends_at_comma("f(fn: self, 1)", vec![Some(1), None])] +#[case::pipe_before_comma("f(fn: g | h, 1)", vec![Some(2), None])] +#[case::call_with_commas_in_body("f(fn: g(1, 2), 3)", vec![Some(1), None])] +#[case::nested("f(fn: g(fn: self + 1), 2)", vec![Some(1), None])] +#[case::semicolon_form("f(fn: self + 1;)", vec![Some(1)])] +#[case::spaced_colon("f(fn : self)", vec![Some(1)])] +fn test_lambda_without_params(#[case] code: &str, #[case] expected: Vec>) { + assert_eq!(lambda_arg_body_lens(code), (expected, false)); +} + +#[test] +fn test_lambda_without_params_has_no_param_nodes() { + let (nodes, errors) = crate::parse_recovery("f(fn: self)"); + assert!(!errors.has_errors()); + let NodeKind::Call { args } = &nodes[0].kind else { + panic!("expected a call"); + }; + let Some(NodeKind::Fn { params, .. }) = args + .iter() + .map(|arg| &arg.kind) + .find(|k| matches!(k, NodeKind::Fn { .. })) + else { + panic!("expected a lambda"); + }; + assert!(params.is_empty()); +} + +#[rstest] +#[case::arrow_has_no_paramless_form("f(->: self)")] +#[case::empty_body("f(fn: )")] +#[case::unclosed_call("f(fn: self")] +fn test_lambda_without_params_reports_malformed_input(#[case] code: &str) { + let (_, errors) = crate::parse_recovery(code); + assert!(errors.has_errors(), "{code:?} should report an error"); +} + +#[rstest] +#[case::empty_body("f(fn(x): )")] +#[case::empty_body_then_comma("f(fn(x):, 1)")] +#[case::dangling_operator("f(fn(x): x +)")] +#[case::unclosed_call("f(fn(x): x + 1")] +#[case::stray_close_paren("f(fn(x): x + 1))")] +#[case::let_value("let g = fn(x): x + 1)")] +fn test_lambda_reports_malformed_input(#[case] code: &str) { + let (_, errors) = crate::parse_recovery(code); + assert!(errors.has_errors(), "{code:?} should report an error"); +} + +#[rstest] +#[case::paren_group("(fn(x): x + 1)(2)")] +#[case::array_element("[fn(x): x, 1]")] +#[case::implicit_array_element("[fn: self, 1]")] +#[case::dict_value("{\"a\": fn(x): x, \"b\": 1}")] +#[case::lambda_body_tail("f(fn(x): fn(y): x + y)")] +#[case::after_pipe("f(1 | fn(x): x + 1, 2)")] +fn test_lambda_ends_at_the_closer_of_its_enclosing_construct(#[case] code: &str) { + let (_, errors) = crate::parse_recovery(code); + assert!(!errors.has_errors(), "{code:?} should not report an error"); +} + +#[rstest] +#[case::do_block("do 1")] +#[case::def("def f(): 1")] +#[case::def_do("def f() do 1")] +#[case::fn_params("fn(x): x")] +#[case::fn_no_params("fn: 1")] +#[case::if_body("if (true): 1")] +#[case::after_pipe("1 | fn(x): x")] +fn test_top_level_body_may_end_at_eof(#[case] code: &str) { + let (_, errors) = crate::parse_recovery(code); + assert!(!errors.has_errors(), "{code:?}: {errors:?}"); +} + +#[rstest] +#[case::call("f(do 1", "`)`")] +#[case::array("[do 1", "`]`")] +#[case::group("(do 1", "`)`")] +fn test_body_at_eof_leaves_missing_closer_to_enclosing_construct(#[case] code: &str, #[case] expected: &str) { + let (_, errors) = crate::parse_recovery(code); + let errors = errors.to_vec(); + assert_eq!(errors.len(), 1, "{code:?}: {errors:?}"); + assert!( + matches!(&errors[0], ParseError::Missing { expected: e, .. } if *e == expected), + "{code:?}: {errors:?}" + ); +} + +#[rstest] +#[case::add("a + b", "(a + b)")] +#[case::sub("a - b", "(a - b)")] +#[case::mul("a * b", "(a * b)")] +#[case::div("a / b", "(a / b)")] +#[case::modulo("a % b", "(a % b)")] +#[case::eq("a == b", "(a == b)")] +#[case::ne("a != b", "(a != b)")] +#[case::lt("a < b", "(a < b)")] +#[case::lte("a <= b", "(a <= b)")] +#[case::gt("a > b", "(a > b)")] +#[case::gte("a >= b", "(a >= b)")] +#[case::and("a && b", "(a && b)")] +#[case::or("a || b", "(a || b)")] +#[case::left_shift("a << b", "(a << b)")] +#[case::right_shift("a >> b", "(a >> b)")] +#[case::convert("a @ b", "(a @ b)")] +#[case::range("a .. b", "(a .. b)")] +#[case::coalesce("a ?? b", "(a ?? b)")] +#[case::regex_match("a =~ b", "(a =~ b)")] +#[case::not_regex_match("a !~ b", "(a !~ b)")] +fn test_every_binary_operator_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), (expected.to_string(), false)); +} + +#[rstest] +#[case::and_over_or("a || b && c", "(a || (b && c))")] +#[case::or_after_and("a && b || c", "((a && b) || c)")] +#[case::comparison_over_and("a && b == c", "(a && (b == c))")] +#[case::and_after_comparison("a == b && c", "((a == b) && c)")] +#[case::additive_over_comparison("a == b + c", "(a == (b + c))")] +#[case::comparison_after_additive("a + b == c", "((a + b) == c)")] +#[case::multiplicative_over_additive("a + b * c", "(a + (b * c))")] +#[case::additive_after_multiplicative("a * b + c", "((a * b) + c)")] +#[case::range_over_multiplicative("a * b .. c", "(a * (b .. c))")] +#[case::multiplicative_after_range("a .. b * c", "((a .. b) * c)")] +#[case::coalesce_over_multiplicative("a * b ?? c", "(a * (b ?? c))")] +#[case::multiplicative_after_coalesce("a ?? b * c", "((a ?? b) * c)")] +#[case::convert_with_multiplicative("a + b @ c", "(a + (b @ c))")] +#[case::convert_is_left_associative_with_mul("a * b @ c", "((a * b) @ c)")] +#[case::shift_with_additive("a << b + c", "((a << b) + c)")] +#[case::additive_with_shift("a + b >> c", "((a + b) >> c)")] +#[case::regex_match_with_comparison("a =~ b == c", "((a =~ b) == c)")] +#[case::regex_over_and("a !~ b && c", "((a !~ b) && c)")] +#[case::three_levels("a || b && c == d + e * f .. g", "(a || (b && (c == (d + (e * (f .. g))))))")] +#[case::three_levels_reversed("a .. b * c + d == e && f || g", "((((((a .. b) * c) + d) == e) && f) || g)")] +fn test_binary_op_precedence_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), (expected.to_string(), false)); +} + +#[rstest] +#[case::or("a || b || c", "((a || b) || c)")] +#[case::and("a && b && c", "((a && b) && c)")] +#[case::comparison("a == b == c", "((a == b) == c)")] +#[case::mixed_comparison("a < b != c", "((a < b) != c)")] +#[case::sub("a - b - c", "((a - b) - c)")] +#[case::add_then_sub("a + b - c", "((a + b) - c)")] +#[case::shifts("a << b >> c", "((a << b) >> c)")] +#[case::div("a / b / c", "((a / b) / c)")] +#[case::mul_div_mod("a * b / c % d", "(((a * b) / c) % d)")] +#[case::range("a .. b .. c", "((a .. b) .. c)")] +#[case::coalesce("a ?? b ?? c", "((a ?? b) ?? c)")] +#[case::range_then_coalesce("a .. b ?? c", "((a .. b) ?? c)")] +#[case::coalesce_then_range("a ?? b .. c", "((a ?? b) .. c)")] +#[case::convert("a @ b @ c", "((a @ b) @ c)")] +fn test_binary_op_associativity_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), (expected.to_string(), false)); +} + +#[rstest] +#[case::parens_override_precedence("(a + b) * c", "(<(a + b)> * c)")] +#[case::parens_on_the_right("a * (b + c)", "(a * <(b + c)>)")] +#[case::parens_override_associativity("a - (b - c)", "(a - <(b - c)>)")] +#[case::neg_binds_tighter_than_mul("-a * b", "((- a) * b)")] +#[case::not_binds_tighter_than_eq("!a == b", "((! a) == b)")] +#[case::not_binds_tighter_than_and("!a && !b", "((! a) && (! b))")] +#[case::unary_on_the_right("a - -b", "(a - (- b))")] +#[case::not_on_the_right("a * !b", "(a * (! b))")] +#[case::as_binds_the_whole_expression("a + b * c as x", "((a + (b * c)) as x)")] +#[case::as_after_logic("a && b as x", "((a && b) as x)")] +fn test_operand_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), (expected.to_string(), false)); +} + +#[rstest] +#[case::assign("x = a", "(x = a)")] +#[case::assign_is_lowest("x = a || b && c == d + e * f", "(x = (a || (b && (c == (d + (e * f))))))")] +#[case::assign_takes_the_whole_expression("x = a + b * c", "(x = (a + (b * c)))")] +#[case::plus_assign("x += a * b", "(x += (a * b))")] +#[case::minus_assign("x -= a * b", "(x -= (a * b))")] +#[case::mul_assign("x *= a + b", "(x *= (a + b))")] +#[case::div_assign("x /= a + b", "(x /= (a + b))")] +#[case::mod_assign("x %= a + b", "(x %= (a + b))")] +#[case::floor_div_assign("x //= a + b", "(x //= (a + b))")] +fn test_assignment_shape(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), (expected.to_string(), false)); +} + +#[rstest] +#[case::trivia_between("a +\n b # c\n * d", "(a + (b * d))")] +#[case::trivia_before_operator("a\n + b", "(a + b)")] +#[case::trivia_in_assignment("x =\n a || b", "(x = (a || b))")] +fn test_binary_op_shape_with_trivia(#[case] code: &str, #[case] expected: &str) { + assert_eq!(parse_shape(code), (expected.to_string(), false)); +} + +#[rstest] +#[case::missing_right_operand("a +")] +#[case::missing_right_operand_after_assign("a =")] +#[case::missing_right_operand_after_logic("a &&")] +#[case::as_without_name("a as")] +#[case::as_with_non_identifier("a as 1")] +fn test_binary_op_errors(#[case] code: &str) { + let (nodes, errors) = crate::parse_recovery(code); + assert!(errors.has_errors(), "expected an error for {code:?}, got {nodes:?}"); +} diff --git a/crates/mq-lang/src/engine.rs b/crates/mq-lang/src/engine.rs index 727598872..82fea4429 100644 --- a/crates/mq-lang/src/engine.rs +++ b/crates/mq-lang/src/engine.rs @@ -55,7 +55,7 @@ pub enum DefineValueError { /// ``` #[derive(Debug, Clone)] pub struct Engine> { - /// VM state โ€” see [`tarn::VmState`]. + /// VM state: see [`tarn::VmState`]. pub(crate) vm: tarn::VmState, pub(crate) token_arena: Shared>>, pub(crate) vm_module_prelude: Vec, @@ -94,7 +94,7 @@ impl Default for Engine> { } // `new` is pinned to the default `IO` (rather than generic over any `IO: Default`) because Rust -// doesn't use a generic parameter's default as an inference fallback โ€” callers like +// doesn't use a generic parameter's default as an inference fallback; callers like // `Engine::new(resolver)` have no other way to pin `IO`. Mirrors the `HashMap::new`/ // `HashMap::with_hasher` split. To use a different `IO`, annotate the binding's type, e.g. // `let engine: Engine = Engine::new(resolver);` (this only fixes the *value* passed at @@ -138,7 +138,7 @@ impl Engine { impl Engine { /// Like the [`SandboxedIo`]-pinned [`Engine::new`], but generic over `IO` and - /// takes the [`Io`] value up front โ€” for hosts that need to select the `Io` *type* at + /// 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). Useful for e.g. a /// test runner that wants an in-memory mock `Io` installed from the start. /// @@ -181,7 +181,7 @@ impl Engine { } /// Sets the [`Io`] this engine uses for file, environment-variable, and network - /// access โ€” both for builtins (`read_file`, `write_file`, `http`, ...) and for + /// access: both for builtins (`read_file`, `write_file`, `http`, ...) and for /// local module resolution (`include`/`import`). Defaults to an all-denied /// [`SandboxedIo`](crate::SandboxedIo) wrapping [`NativeIo`](crate::NativeIo), /// so a host must opt in explicitly. @@ -251,10 +251,10 @@ impl Engine { /// - A raw form taking the already-evaluated call arguments as a slice and returning a /// single [`RuntimeValue`]: `|args: &[RuntimeValue]| -> HostFnResult { .. }`. /// - A typed form taking up to eight plain Rust arguments (any combination of `i64`, `f64`, - /// `String`, `bool`, `RuntimeValue`, `Vec`, or `Option` for a [`ValueAdapter`] `T`), + /// `String`, `bool`, `RuntimeValue`, `Vec`, or `Option` for a [`ValueAdapter`](crate::ValueAdapter) `T`), /// returning `Result` for an `R: ValueAdapter`. Argument and return /// values are converted to/from `RuntimeValue` automatically; a wrong argument count or - /// type is reported as a [`HostFunctionError`] rather than panicking. + /// type is reported as a [`HostFunctionError`](crate::HostFunctionError) rather than panicking. /// /// Errors and panics raised by the function are caught at the call boundary and surfaced as /// a normal mq runtime error rather than aborting evaluation or unwinding through the @@ -263,7 +263,7 @@ impl Engine { /// /// A registered function only fills in a name that would otherwise be undefined: a `def` of /// the same name always takes precedence, and so does *any* built-in function name, whether - /// or not [`Self::load_builtin_module`] was called โ€” name resolution itself falls back to + /// or not [`Self::load_builtin_module`] was called: name resolution itself falls back to /// the built-in table before host functions are ever consulted. Host functions therefore /// extend the set of callable names rather than override existing ones; pick a name that /// doesn't collide with a builtin. diff --git a/crates/mq-lang/src/engine/compiled_program.rs b/crates/mq-lang/src/engine/compiled_program.rs index 946057157..f3ccd8323 100644 --- a/crates/mq-lang/src/engine/compiled_program.rs +++ b/crates/mq-lang/src/engine/compiled_program.rs @@ -4,7 +4,7 @@ use crate::SharedCell; #[cfg(any(not(feature = "debugger"), feature = "mqc"))] use crate::{Shared, tarn}; -/// A compiled mq program bundled with its original source, returned by [`Engine::compile`]. +/// A compiled mq program bundled with its original source, returned by `Engine::compile`. #[derive(Debug, Clone)] pub struct CompiledProgram { pub(crate) source: String, diff --git a/crates/mq-lang/src/engine/tests.rs b/crates/mq-lang/src/engine/tests.rs index f2814cffe..18e9d51e6 100644 --- a/crates/mq-lang/src/engine/tests.rs +++ b/crates/mq-lang/src/engine/tests.rs @@ -1106,7 +1106,7 @@ fn test_builtin_cache_sequential_engines_consistent( } /// Compiling and evaluating a builtin function call on a second engine (cache path) must -/// produce the correct result โ€” verifying that token_ids in the compiled program are valid +/// produce the correct result: verifying that token_ids in the compiled program are valid /// when the builtin tokens were injected from cache rather than freshly parsed. #[rstest] #[case("add(1, 2)", vec!["".to_string().into()], vec![3.into()])] @@ -1173,7 +1173,7 @@ fn test_builtin_cache_runtime_error_token_location_correct(#[case] query: &str, } /// Two sequential engines (one possibly fresh-parse, one cache) must produce identical -/// error locations โ€” confirming that token_id indices are not shifted by cache replay. +/// error locations: confirming that token_id indices are not shifted by cache replay. #[rstest] #[case("undefined_fn()")] #[case("1 | undefined_fn()")] @@ -1362,7 +1362,7 @@ fn test_eval_debug_expression_vm_paused_frame_binding_shadows_soft_builtin() { fn test_eval_compiled_vm_error_is_a_real_miette_diagnostic() { use crate::RuntimeValue; - // `eval_compiled_vm` used to return a bare `Result<_, String>` โ€” this checks it now + // `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. @@ -1636,7 +1636,7 @@ fn test_eval_compiled_vm_recompiles_after_search_paths_change_on_same_engine() { &[RuntimeValue::String(Shared::new("first".to_string()))] ); - // Same CompiledProgram, same engine โ€” only search paths changed. + // Same CompiledProgram, same engine; only search paths changed. engine.set_search_paths(vec![second_dir.path().to_owned()]); assert_eq!( engine @@ -1841,7 +1841,7 @@ fn test_module_var_initializer_runs_once_per_eval_for_import() { }); // `import` always addresses its vars through the qualified (aliased) compile path, - // unlike `include`'s plain named locals โ€” a separate code path this fix must also cover. + // unlike `include`'s plain named locals: a separate code path this fix must also cover. engine.import_module("side_effect_counter_import").unwrap(); let baseline = call_count.load(Ordering::SeqCst); @@ -2505,7 +2505,7 @@ fn test_cached_vm_does_not_reuse_an_environment_after_its_engine_drops() { fn test_eval_compiled_vm_resolves_a_local_file_import() { use crate::RuntimeValue; - // Mirrors `test_eval_import_as_alias`, but through `eval_compiled_vm` โ€” this only works + // 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). @@ -2567,7 +2567,7 @@ fn test_eval_compiled_vm_notifies_debugger_of_uncaught_error() { .lock() .unwrap() .iter() - // "Division by zero" โ€” `RuntimeError::ZeroDivision`'s wording, the same the + // "Division by zero": `RuntimeError::ZeroDivision`'s wording, the same the // tree-walker uses; `notify_error` converts through it rather than using // `VmError`'s own (not user-facing) `Display`. See `notify_error`'s doc comment. .any(|message| message.contains("Division by zero")) diff --git a/crates/mq-lang/src/error.rs b/crates/mq-lang/src/error.rs index be0bce658..dba9db4e7 100644 --- a/crates/mq-lang/src/error.rs +++ b/crates/mq-lang/src/error.rs @@ -298,9 +298,6 @@ impl Diagnostic for Error { InnerError::Runtime(RuntimeError::DateTimeFormatError(_, _)) => Some(Cow::Borrowed( "Invalid date/time format. Please check your format string.", )), - InnerError::Runtime(RuntimeError::IndexOutOfBounds(_, _)) => Some(Cow::Borrowed( - "Index out of bounds. Check your array or string indices.", - )), InnerError::Runtime(RuntimeError::InvalidDefinition(_, name)) => Some(Cow::Owned(format!( "'{name}' exists but is not a function, so it cannot be called." ))), @@ -321,9 +318,6 @@ impl Diagnostic for Error { InnerError::Runtime(RuntimeError::InvalidRegularExpression(_, _)) => Some(Cow::Borrowed( "Invalid regular expression. Please check your regex syntax.", )), - InnerError::Runtime(RuntimeError::InternalError(_)) => Some(Cow::Borrowed( - "An internal error occurred. Please report this if it persists.", - )), InnerError::Runtime(RuntimeError::Runtime(_, _)) => { Some(Cow::Borrowed("A runtime error occurred during evaluation.")) } @@ -340,12 +334,6 @@ impl Diagnostic for Error { InnerError::Runtime(RuntimeError::ModuleLoadError(_)) => { Some(Cow::Borrowed("Failed to load module. Check module paths and names.")) } - InnerError::Runtime(RuntimeError::UnexpectedBreak(_)) => { - Some(Cow::Borrowed("'break' can only be used inside a loop.")) - } - InnerError::Runtime(RuntimeError::UnexpectedContinue(_)) => { - Some(Cow::Borrowed("'continue' can only be used inside a loop.")) - } InnerError::Runtime(RuntimeError::EnvNotFound(_, env)) => Some(Cow::Owned(format!( "Environment variable '{env}' not found. Did you forget to set it?" ))), @@ -616,14 +604,6 @@ mod test { }, "".to_string(), Box::new([]))), "source code" )] - #[case::eval_index_out_of_bounds( - InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: ArenaId::new(0), - }, 1.into())), - "source code" - )] #[case::eval_invalid_definition( InnerError::Runtime(RuntimeError::InvalidDefinition(Token { range: Range::default(), @@ -656,15 +636,7 @@ mod test { }, "".to_string())), "source code" )] - #[case::eval_internal_error( - InnerError::Runtime(RuntimeError::InternalError(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: ArenaId::new(0), - })), - "source code" - )] - #[case::eval_internal_error( + #[case::eval_runtime_error( InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), kind: TokenKind::Eof, @@ -840,13 +812,6 @@ mod test { module_id: ArenaId::new(0), }, "fmt".to_string())) )] - #[case::eval_index_out_of_bounds( - InnerError::Runtime(RuntimeError::IndexOutOfBounds(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: ArenaId::new(0), - }, 1.into())) - )] #[case::eval_invalid_definition( InnerError::Runtime(RuntimeError::InvalidDefinition(Token { range: Range::default(), @@ -886,13 +851,6 @@ mod test { module_id: ArenaId::new(0), }, "bad".to_string())) )] - #[case::eval_internal_error( - InnerError::Runtime(RuntimeError::InternalError(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: ArenaId::new(0), - })) - )] #[case::eval_runtime_error( InnerError::Runtime(RuntimeError::Runtime(Token { range: Range::default(), @@ -907,16 +865,6 @@ mod test { module_id: ArenaId::new(0), })) )] - #[case::eval_unexpected_break(InnerError::Runtime(RuntimeError::UnexpectedBreak(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: ArenaId::new(0), - })))] - #[case::eval_unexpected_continue(InnerError::Runtime(RuntimeError::UnexpectedContinue(Token { - range: Range::default(), - kind: TokenKind::Eof, - module_id: ArenaId::new(0), - })))] #[case::eval_env_not_found( InnerError::Runtime(RuntimeError::EnvNotFound(Token { range: Range::default(), diff --git a/crates/mq-lang/src/error/runtime.rs b/crates/mq-lang/src/error/runtime.rs index 6dfb08223..f50ffde51 100644 --- a/crates/mq-lang/src/error/runtime.rs +++ b/crates/mq-lang/src/error/runtime.rs @@ -3,7 +3,7 @@ use std::time::Duration; use smol_str::SmolStr; use thiserror::Error; -use crate::{Token, number::Number}; +use crate::Token; use super::module::error::ModuleError; @@ -33,8 +33,6 @@ pub enum RuntimeError { UndefinedReference(ErrorToken, FunctionName, Box<[FunctionName]>), #[error("Unable to format date time, {1}")] DateTimeFormatError(ErrorToken, String), - #[error("Index out of bounds {1}")] - IndexOutOfBounds(ErrorToken, Number), #[error("Invalid definition for \"{1}\"")] InvalidDefinition(ErrorToken, String), #[error("Maximum recursion depth exceeded ({0})")] @@ -59,18 +57,12 @@ pub enum RuntimeError { }, #[error("Invalid regular expression \"{1}\"")] InvalidRegularExpression(ErrorToken, String), - #[error("Internal error")] - InternalError(ErrorToken), #[error("Failed to load module \"{0}\"")] ModuleLoadError(#[from] ModuleError), #[error("Runtime error: {1}")] Runtime(ErrorToken, String), #[error("Division by zero")] ZeroDivision(ErrorToken), - #[error("Unexpected break outside of loop")] - UnexpectedBreak(ErrorToken), - #[error("Unexpected continue outside of loop")] - UnexpectedContinue(ErrorToken), #[error("Environment variable `{1}` not found")] EnvNotFound(Token, SmolStr), #[error("Cannot assign to immutable variable \"{1}\"")] @@ -96,7 +88,6 @@ impl RuntimeError { RuntimeError::NotDefined(token, _, _) => Some(token), RuntimeError::UndefinedReference(token, _, _) => Some(token), RuntimeError::DateTimeFormatError(token, _) => Some(token), - RuntimeError::IndexOutOfBounds(token, _) => Some(token), RuntimeError::InvalidDefinition(token, _) => Some(token), RuntimeError::RecursionError(_) => None, RuntimeError::Timeout(_) => None, @@ -104,12 +95,9 @@ impl RuntimeError { RuntimeError::InvalidTypes { token, .. } => Some(token), RuntimeError::InvalidNumberOfArguments { token, .. } => Some(token), RuntimeError::InvalidRegularExpression(token, _) => Some(token), - RuntimeError::InternalError(token) => Some(token), RuntimeError::ModuleLoadError(err) => err.token(), RuntimeError::Runtime(token, _) => Some(token), RuntimeError::ZeroDivision(token) => Some(token), - RuntimeError::UnexpectedBreak(token) => Some(token), - RuntimeError::UnexpectedContinue(token) => Some(token), RuntimeError::EnvNotFound(token, _) => Some(token), RuntimeError::AssignToImmutable(token, _) => Some(token), RuntimeError::InvalidConvert(token, _) => Some(token), @@ -139,18 +127,14 @@ mod tests { #[case(RuntimeError::NotDefined(eof_token(), "f".to_string(), Box::new([])), true)] #[case(RuntimeError::UndefinedReference(eof_token(), "r".to_string(), Box::new([])), true)] #[case(RuntimeError::DateTimeFormatError(eof_token(), "err".to_string()), true)] - #[case(RuntimeError::IndexOutOfBounds(eof_token(), Number::from(1.0)), true)] #[case(RuntimeError::InvalidDefinition(eof_token(), "d".to_string()), true)] #[case(RuntimeError::RecursionError(10), false)] #[case(RuntimeError::Timeout(Duration::from_secs(1)), false)] #[case(RuntimeError::InvalidTypes { token: eof_token(), name: "f".to_string(), args: vec![] }, true)] #[case(RuntimeError::InvalidNumberOfArguments { token: eof_token(), name: "f".to_string(), expected: 1, actual: 0 }, true)] #[case(RuntimeError::InvalidRegularExpression(eof_token(), "pat".to_string()), true)] - #[case(RuntimeError::InternalError(eof_token()), true)] #[case(RuntimeError::Runtime(eof_token(), "err".to_string()), true)] #[case(RuntimeError::ZeroDivision(eof_token()), true)] - #[case(RuntimeError::UnexpectedBreak(eof_token()), true)] - #[case(RuntimeError::UnexpectedContinue(eof_token()), true)] #[case(RuntimeError::EnvNotFound(eof_token(), "VAR".into()), true)] #[case(RuntimeError::AssignToImmutable(eof_token(), "x".to_string()), true)] #[case(RuntimeError::InvalidConvert(eof_token(), "msg".to_string()), true)] diff --git a/crates/mq-lang/src/io.rs b/crates/mq-lang/src/io.rs index ec5e25fd5..ae450b6ff 100644 --- a/crates/mq-lang/src/io.rs +++ b/crates/mq-lang/src/io.rs @@ -1,6 +1,6 @@ -//! Abstracts every environment-dependent operation the mq engine needs โ€” file +//! Abstracts every environment-dependent operation the mq engine needs (file //! read/write, environment-variable access, network fetch, and external -//! process execution โ€” behind a single trait, so the core evaluator, parser, +//! process execution) behind a single trait, so the core evaluator, parser, //! and module resolvers never call `std::fs`/`std::env`/a network //! client/`std::process::Command` directly. Concrete implementations are //! injected per [`Engine`](crate::Engine) instance. @@ -13,7 +13,7 @@ //! //! `LocalFsModuleResolver` (local module resolution) routes through this //! trait; `HttpModuleResolver`/`UreqFetcher` (HTTP module imports) do not yet -//! โ€” that unification, including relocating `UreqFetcher`'s on-disk +//! that unification, including relocating `UreqFetcher`'s on-disk //! cache/lockfile logic into `NativeIo`, is left for a follow-up. mod error; @@ -29,7 +29,7 @@ pub use sandboxed::{EnvAccess, NetAccess, PathAccess, SandboxedIo}; /// `pub` here (rather than gated by the `mock-io` feature) would still not leak `MemIo` /// externally, since the `io` module itself is private to the crate (see `mod io;` in -/// `lib.rs`) โ€” but it's gated anyway so the "unused" build (neither `test` nor `mock-io`) +/// `lib.rs`), but it's gated anyway so the "unused" build (neither `test` nor `mock-io`) /// doesn't warn. The crate-root re-export in `lib.rs` is what actually governs external /// visibility, since [`MemIo`] and the `mock_fetch` builtin are opt-in, testing-focused surface. #[cfg(any(test, feature = "mock-io"))] @@ -67,7 +67,7 @@ pub struct HttpRequestSpec { } /// Kind of filesystem entry reported by [`Io::metadata`]. Reflects the entry itself -/// (via a symlink-unaware stat), not what a symlink points to โ€” a symlink is always +/// (via a symlink-unaware stat), not what a symlink points to; a symlink is always /// reported as `Symlink`, never followed to its target's kind. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FileKind { @@ -91,7 +91,7 @@ impl FileKind { } /// Metadata for the entry at a path, as returned by [`Io::metadata`], backing the -/// `file_info` builtin. Never follows a symlink to inspect its target โ€” see [`FileKind`]. +/// `file_info` builtin. Never follows a symlink to inspect its target, see [`FileKind`]. #[derive(Debug, Clone, Copy)] pub struct FileMetadata { pub kind: FileKind, @@ -147,7 +147,7 @@ pub trait Io: std::fmt::Debug + IoSyncBound + 'static { fn env_vars(&self) -> Result, IoError>; /// Callers are responsible for URL/domain policy (HTTPS-only, allowlisting, - /// etc.) before calling this โ€” `fetch` is a minimal transport primitive. + /// etc.) before calling this: `fetch` is a minimal transport primitive. fn fetch(&self, url: &str) -> Result; /// General-purpose HTTP request (arbitrary method/body/headers), backing @@ -194,8 +194,8 @@ pub trait Io: std::fmt::Debug + IoSyncBound + 'static { /// The current working directory, if known. fn current_dir(&self) -> Option; - /// Runs `command` with `args` as a child process โ€” never through a shell, so shell - /// metacharacters in `args` are never interpreted โ€” and returns its captured stdout as a + /// Runs `command` with `args` as a child process, never through a shell, so shell + /// metacharacters in `args` are never interpreted, and returns its captured stdout as a /// string. A non-zero exit status is reported as an error that includes the process's /// stderr. Callers are responsible for policy (which commands are permitted) before calling /// this, same as [`fetch`](Self::fetch)'s URL/domain policy. diff --git a/crates/mq-lang/src/io/error.rs b/crates/mq-lang/src/io/error.rs index 1ebe2df80..7e7ebb403 100644 --- a/crates/mq-lang/src/io/error.rs +++ b/crates/mq-lang/src/io/error.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use thiserror::Error; -/// Error returned by [`Io`](super::Io) operations. Internal plumbing type โ€” +/// Error returned by [`Io`](super::Io) operations. Internal plumbing type: /// call sites are expected to convert it into whatever domain error type they /// already use (e.g. `ModuleError` for module resolution, the builtin /// evaluator's own error type) rather than surface it to `miette` directly. diff --git a/crates/mq-lang/src/io/mem.rs b/crates/mq-lang/src/io/mem.rs index 0468b9124..7b0b96a34 100644 --- a/crates/mq-lang/src/io/mem.rs +++ b/crates/mq-lang/src/io/mem.rs @@ -7,7 +7,7 @@ use std::sync::{ atomic::{AtomicUsize, Ordering}, }; -/// In-memory [`Io`] โ€” file writes, environment variables, and fetch/HTTP responses all +/// In-memory [`Io`]: file writes, environment variables, and fetch/HTTP responses all /// live in-process instead of touching the real filesystem/network. Useful both for unit /// tests that only need to assert *what was asked for*, and for embeddings (e.g. `mq-test`) /// that want a hermetic `Io` a query can read from and, via [`Self::set_fetch_response`], diff --git a/crates/mq-lang/src/io/native.rs b/crates/mq-lang/src/io/native.rs index 99e32ce90..26664f21c 100644 --- a/crates/mq-lang/src/io/native.rs +++ b/crates/mq-lang/src/io/native.rs @@ -9,7 +9,7 @@ use std::path::{Path, PathBuf}; /// [`SandboxedIo`](super::SandboxedIo) to gate read/write/net permissions. /// /// `fetch` does not yet implement the on-disk caching/lockfile behavior that -/// `UreqFetcher` (the module-import HTTP fetcher) has today โ€” that migration +/// `UreqFetcher` (the module-import HTTP fetcher) has today; that migration /// is follow-up work. This is a plain, uncached HTTPS GET through the same /// SSRF-hardened agent used elsewhere in this crate. #[derive(Debug, Clone)] @@ -440,7 +440,7 @@ mod tests { let result = io.metadata(&path); // A root-run process (e.g. inside a container) ignores permission bits, so only - // assert when the OS actually enforced them here โ€” otherwise the test would flake. + // assert when the OS actually enforced them here; otherwise the test would flake. let permissions_enforced = std::fs::metadata(&path).is_err(); // Restore permissions so the tempdir can be cleaned up. diff --git a/crates/mq-lang/src/io/sandboxed.rs b/crates/mq-lang/src/io/sandboxed.rs index e8d90fa40..7f2fdf939 100644 --- a/crates/mq-lang/src/io/sandboxed.rs +++ b/crates/mq-lang/src/io/sandboxed.rs @@ -16,7 +16,7 @@ pub use run_access::RunAccess; /// read/write/net/run/env permission grants, enforced by every operation. All /// five default to fully denied (fail safe), and may additionally be /// restricted to an allowlist via [`PathAccess::AllowedPaths`]/[`NetAccess::AllowedDomains`]/ -/// [`RunAccess::AllowedCommands`]/[`EnvAccess::AllowedNames`]. +/// `RunAccess::AllowedCommands`/[`EnvAccess::AllowedNames`]. /// /// Every operation enforces its own permission independently of any external /// pre-check, so a caller cannot accidentally bypass sandboxing by forgetting @@ -74,8 +74,8 @@ impl SandboxedIo { self } - /// Gates [`Io::execute`], the primitive backing the `system()` builtin โ€” external process - /// execution. See [`Self::allow_read`] for the accepted forms; `Vec`/ + /// Gates [`Io::execute`], the primitive backing the `system()` builtin (external process + /// execution). See [`Self::allow_read`] for the accepted forms; `Vec`/ /// `Option>` restrict by command name. pub fn allow_run(mut self, allow: impl Into) -> Self { self.allow_run = allow.into(); @@ -564,7 +564,7 @@ mod tests { } /// `is_read_allowed`/`is_write_allowed`/`is_env_allowed` report whether *any* access was - /// granted, including a restricted allowlist โ€” they don't distinguish full vs. restricted + /// granted, including a restricted allowlist: they don't distinguish full vs. restricted /// access. #[rstest] #[case::nothing_granted(false, false, false, false, false, (false, false, false, false, false))] diff --git a/crates/mq-lang/src/module.rs b/crates/mq-lang/src/module.rs index 454d9a3ec..899668399 100644 --- a/crates/mq-lang/src/module.rs +++ b/crates/mq-lang/src/module.rs @@ -1029,7 +1029,7 @@ mod tests { let mut loader1 = ModuleLoader::new(DefaultModuleResolver::default()); loader1.load_builtin(pristine_token_arena).unwrap(); - // Second pristine load โ€” this is the cache-hit path. + // Second pristine load: this is the cache-hit path. let arena2 = Shared::new(SharedCell::new(crate::arena::Arena::new(2048))); token_alloc( &arena2, diff --git a/crates/mq-lang/src/module/resolver.rs b/crates/mq-lang/src/module/resolver.rs index 728ba1e7f..5d8d466c9 100644 --- a/crates/mq-lang/src/module/resolver.rs +++ b/crates/mq-lang/src/module/resolver.rs @@ -119,7 +119,7 @@ impl ModuleResolver for DefaultModuleResolver { impl DefaultModuleResolver { /// Creates a new resolver with the given filesystem search paths, backed by - /// [`NativeIo`](crate::io::NativeIo) for local module resolution โ€” i.e. full, + /// [`NativeIo`](crate::io::NativeIo) for local module resolution, i.e. full, /// ungated disk access, matching this resolver's historical behavior. /// /// An empty `paths` slice falls back to the built-in default search directories. @@ -137,7 +137,7 @@ impl DefaultModuleResolver { } /// Creates a new resolver whose local filesystem module resolution goes through `io` - /// instead of a bare [`NativeIo`](crate::io::NativeIo) โ€” e.g. a + /// instead of a bare [`NativeIo`](crate::io::NativeIo), e.g. a /// [`SandboxedIo`](crate::io::SandboxedIo) so `include`/`import` respect the same /// read permission as the `read_file`/`write_file`/`http` builtins. HTTP module /// imports are unaffected (still `NativeIo`-only, if the `http-import-ureq` feature @@ -211,7 +211,7 @@ impl DefaultModuleResolver { } /// When `true`, a URL with no existing `mq.lock` entry is a hard error instead of being - /// recorded as a new entry. See [`http_resolver::UreqFetcher::set_lockfile_frozen`]. + /// recorded as a new entry. See `http_resolver::UreqFetcher::set_lockfile_frozen`. #[cfg(feature = "http-import-ureq")] pub fn set_lockfile_frozen(&mut self, frozen: bool) { self.http_resolver.set_lockfile_frozen(frozen); diff --git a/crates/mq-lang/src/module/resolver/http_import.rs b/crates/mq-lang/src/module/resolver/http_import.rs index 4d6efdbf7..a5d6e3c26 100644 --- a/crates/mq-lang/src/module/resolver/http_import.rs +++ b/crates/mq-lang/src/module/resolver/http_import.rs @@ -254,7 +254,7 @@ mod tests { } #[rstest] - // Single component after github.com/ โ€” not enough to form owner/repo + // Single component after github.com/, not enough to form owner/repo #[case("github.com/owner")] fn test_github_to_raw_url_returns_none(#[case] input: &str) { assert!(github_to_raw_url(input).is_none()); diff --git a/crates/mq-lang/src/module/resolver/http_resolver.rs b/crates/mq-lang/src/module/resolver/http_resolver.rs index 7b0129597..f73f700f8 100644 --- a/crates/mq-lang/src/module/resolver/http_resolver.rs +++ b/crates/mq-lang/src/module/resolver/http_resolver.rs @@ -11,7 +11,7 @@ use std::{borrow::Cow, path::PathBuf}; /// Pluggable HTTP fetch-and-cache backend for [`HttpModuleResolver`]. /// /// Implementations are responsible for both fetching remote content and managing -/// any associated caching (disk, memory, etc.). The URL passed to [`fetch`] has +/// any associated caching (disk, memory, etc.). The URL passed to `fetch` has /// already been normalized and domain-checked by [`HttpModuleResolver`]. pub trait HttpFetcher: Clone + Default { /// Fetch the content at `url`, using any internal cache. @@ -162,9 +162,9 @@ struct CachedModule { /// /// Fetched modules are stored under `{system_cache_dir}/mq/` in one of two subdirectories: /// -/// - `versioned/` โ€” URLs resolved to a specific tag (e.g. `@v0.1.0`); never cleared by +/// - `versioned/`: URLs resolved to a specific tag (e.g. `@v0.1.0`); never cleared by /// [`UreqFetcher::clear_cache`]. -/// - `mutable/` โ€” URLs resolved to `HEAD`, `main`, `master`, or any non-GitHub HTTP URL; +/// - `mutable/`: URLs resolved to `HEAD`, `main`, `master`, or any non-GitHub HTTP URL; /// cleared by [`UreqFetcher::clear_cache`]. /// /// Each cached module is accompanied by a `.mq.sha256` sidecar for tamper detection. @@ -520,7 +520,7 @@ impl HttpModuleResolver { } /// When `true`, a URL with no existing `mq.lock` entry is a hard error instead of being - /// recorded as a new entry. See [`UreqFetcher::set_lockfile_frozen`]. + /// recorded as a new entry. See `UreqFetcher::set_lockfile_frozen`. pub fn set_lockfile_frozen(&mut self, frozen: bool) { self.fetcher.set_lockfile_frozen(frozen); } diff --git a/crates/mq-lang/src/module/resolver/ssrf.rs b/crates/mq-lang/src/module/resolver/ssrf.rs index 36d38201e..f0ce22ebb 100644 --- a/crates/mq-lang/src/module/resolver/ssrf.rs +++ b/crates/mq-lang/src/module/resolver/ssrf.rs @@ -2,9 +2,9 @@ //! crate: HTTP module imports (`http_resolver.rs`) and the `http` builtin //! (`eval/builtin/http.rs`). //! -//! [`is_global_ip`] has no I/O dependencies. [`SsrfSafeResolver`]/[`ssrf_safe_agent`] additionally +//! [`is_global_ip`] has no I/O dependencies. `SsrfSafeResolver`/`ssrf_safe_agent` additionally //! require a concrete `ureq` transport, so they're gated behind `http-import-ureq` (HTTP module -//! imports) or `http` (the `http` builtin) โ€” either one pulls in `ureq`. +//! imports) or `http` (the `http` builtin): either one pulls in `ureq`. /// Returns `true` if `url` uses the `https://` scheme. pub fn is_https(url: &str) -> bool { diff --git a/crates/mq-lang/src/runtime/builtin.rs b/crates/mq-lang/src/runtime/builtin.rs index d52a22951..1eb2fc608 100644 --- a/crates/mq-lang/src/runtime/builtin.rs +++ b/crates/mq-lang/src/runtime/builtin.rs @@ -5423,101 +5423,102 @@ fn load_gitignore(io: &dyn Io, dir: &std::path::Path) -> Option