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 | [](https://crates.io/crates/mq-crawler) |
| [`mq-macros`](crates/mq-macros) | Procedural macros for builtin function registration | [](https://crates.io/crates/mq-macros) |
| [`mq-test`](crates/mq-test) | Test runner for mq | [](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 | [](https://www.npmjs.com/package/mq-web) |
| [`mq-nodejs`](packages/mq-nodejs) | Node.js bindings | [](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
-
-
- "#,
- );
- 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
+
+
+ "#,
+ );
+ 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,
- gitignore_stack: &mut Vec,
+struct DirWalk<'a> {
+ io: &'a dyn Io,
respect_gitignore: bool,
follow_symlink_dirs: bool,
- visit_file: &mut dyn FnMut(&std::path::Path, &std::path::Path),
-) -> Result<(), Error> {
- // Also rejects a symlinked root, not just a symlinked subdirectory found during recursion.
- if !follow_symlink_dirs && matches!(io.metadata(dir).map(|m| m.kind), Ok(FileKind::Symlink)) {
- return Ok(());
- }
-
- let canonical = io.canonicalize(dir);
- if !ancestors.insert(canonical.clone()) {
- return Ok(());
- }
+ ancestors: FxHashSet,
+ gitignore_stack: Vec,
+}
- let pushed_gitignore = respect_gitignore && load_gitignore(io, dir).map(|gi| gitignore_stack.push(gi)).is_some();
+#[cfg(feature = "file-io")]
+impl<'a> DirWalk<'a> {
+ fn new(io: &'a dyn Io, respect_gitignore: bool, follow_symlink_dirs: bool) -> Self {
+ Self {
+ io,
+ respect_gitignore,
+ follow_symlink_dirs,
+ ancestors: FxHashSet::default(),
+ gitignore_stack: Vec::new(),
+ }
+ }
+
+ fn walk(
+ &mut self,
+ dir: &std::path::Path,
+ rel_dir: &std::path::Path,
+ visit_file: &mut dyn FnMut(&std::path::Path, &std::path::Path),
+ ) -> Result<(), Error> {
+ // Also rejects a symlinked root, not just a symlinked subdirectory found during recursion.
+ if !self.follow_symlink_dirs && matches!(self.io.metadata(dir).map(|m| m.kind), Ok(FileKind::Symlink)) {
+ return Ok(());
+ }
+
+ let canonical = self.io.canonicalize(dir);
+ if !self.ancestors.insert(canonical.clone()) {
+ return Ok(());
+ }
+
+ let pushed_gitignore = self.respect_gitignore
+ && load_gitignore(self.io, dir)
+ .map(|gi| self.gitignore_stack.push(gi))
+ .is_some();
+
+ let entries = self
+ .io
+ .read_dir(dir)
+ .map_err(|e| Error::Runtime(format!("Failed to read directory {}: {}", dir.display(), e)))?;
+
+ for (path, is_dir) in entries {
+ // Keep as `OsStr`: a non-UTF-8 filename is valid on Unix and must still be walked.
+ let Some(file_name) = path.file_name() else {
+ continue;
+ };
- let entries = io
- .read_dir(dir)
- .map_err(|e| Error::Runtime(format!("Failed to read directory {}: {}", dir.display(), e)))?;
+ if self.respect_gitignore {
+ let is_hidden = file_name.to_string_lossy().starts_with('.');
+ if is_hidden || is_gitignored(&self.gitignore_stack, &path, is_dir) {
+ continue;
+ }
+ }
- for (path, is_dir) in entries {
- // Keep as `OsStr`: a non-UTF-8 filename is valid on Unix and must still be walked.
- let Some(file_name) = path.file_name() else {
- continue;
- };
+ let rel_path = if rel_dir.as_os_str().is_empty() {
+ std::path::PathBuf::from(file_name)
+ } else {
+ rel_dir.join(file_name)
+ };
- if respect_gitignore {
- let is_hidden = file_name.to_string_lossy().starts_with('.');
- if is_hidden || is_gitignored(gitignore_stack, &path, is_dir) {
- continue;
+ if is_dir {
+ self.walk(&path, &rel_path, visit_file)?;
+ } else if self.io.exists(&path).unwrap_or(false) {
+ visit_file(&path, &rel_path);
}
}
- let rel_path = if rel_dir.as_os_str().is_empty() {
- std::path::PathBuf::from(file_name)
- } else {
- rel_dir.join(file_name)
- };
-
- if is_dir {
- walk_dir(
- io,
- &path,
- &rel_path,
- ancestors,
- gitignore_stack,
- respect_gitignore,
- follow_symlink_dirs,
- visit_file,
- )?;
- } else if io.exists(&path).unwrap_or(false) {
- visit_file(&path, &rel_path);
+ if pushed_gitignore {
+ self.gitignore_stack.pop();
}
+ self.ancestors.remove(&canonical);
+ Ok(())
}
-
- if pushed_gitignore {
- gitignore_stack.pop();
- }
- ancestors.remove(&canonical);
- Ok(())
}
#[cfg(feature = "file-io")]
fn collection_impl_inner(dir: &str, respect_gitignore: bool) -> Result {
let io = io_context::current();
- let mut ancestors = FxHashSet::default();
- let mut gitignore_stack = Vec::new();
let mut paths = Vec::new();
- walk_dir(
- io.as_ref(),
+ DirWalk::new(io.as_ref(), respect_gitignore, true).walk(
std::path::Path::new(dir),
std::path::Path::new(""),
- &mut ancestors,
- &mut gitignore_stack,
- respect_gitignore,
- true, // `collection` has always followed symlinked directories.
&mut |path, _rel| {
if path
.extension()
@@ -5581,18 +5582,11 @@ fn walk_files_impl_inner(
};
let io = io_context::current();
- let mut ancestors = FxHashSet::default();
- let mut gitignore_stack = Vec::new();
let mut out = Vec::new();
- walk_dir(
- io.as_ref(),
+ DirWalk::new(io.as_ref(), respect_gitignore, follow_symlinks).walk(
std::path::Path::new(root),
std::path::Path::new(""),
- &mut ancestors,
- &mut gitignore_stack,
- respect_gitignore,
- follow_symlinks,
&mut |_path, rel| {
let rel_str = rel.to_string_lossy().replace('\\', "/");
if glob_pattern.matches_with(&rel_str, match_options) {
@@ -6670,5408 +6664,4 @@ fn repeat(value: &mut RuntimeValue, n: usize) -> Result {
}
#[cfg(test)]
-mod tests {
- use crate::DictMap;
-
- use mq_markdown::Node;
- use rstest::rstest;
- use smol_str::SmolStr;
-
- use crate::io::MemIo;
- #[cfg(any(feature = "file-io", feature = "http", feature = "process-io"))]
- use crate::io::NativeIo;
- use crate::io::SandboxedIo;
-
- use super::*;
-
- #[rstest]
- #[case("type", vec![RuntimeValue::String(Shared::new("test".into()))].into(), Ok(RuntimeValue::String(Shared::new("string".into()))))]
- #[case("len", vec![RuntimeValue::String(Shared::new("test".into()))].into(), Ok(RuntimeValue::Number(4.into())))]
- // German sharp s: full case folding expands ร -> ss, unlike downcase.
- #[case("casefold", vec![RuntimeValue::String(Shared::new("Straรe".into()))].into(), Ok(RuntimeValue::String(Shared::new("strasse".into()))))]
- // Turkish dotted capital ฤฐ: default (non-Turkish) fold is i + combining dot above, not plain "i".
- #[case("casefold", vec![RuntimeValue::String(Shared::new("ฤฐstanbul".into()))].into(), Ok(RuntimeValue::String(Shared::new("i\u{0307}stanbul".into()))))]
- // Turkish dotless small ฤฑ is left untouched by default (non-Turkish) folding.
- #[case("casefold", vec![RuntimeValue::String(Shared::new("ฤฑ".into()))].into(), Ok(RuntimeValue::String(Shared::new("ฤฑ".into()))))]
- #[case("unicode_normalize", vec![RuntimeValue::String(Shared::new("e\u{0301}".into())), RuntimeValue::String(Shared::new("nfc".into()))].into(), Ok(RuntimeValue::String(Shared::new("\u{00e9}".into()))))]
- #[case("unicode_normalize", vec![RuntimeValue::String(Shared::new("\u{00e9}".into())), RuntimeValue::String(Shared::new("nfd".into()))].into(), Ok(RuntimeValue::String(Shared::new("e\u{0301}".into()))))]
- // NFKC of the compatibility-only ellipsis (โฆ) decomposes it to three ASCII dots.
- #[case("unicode_normalize", vec![RuntimeValue::String(Shared::new("\u{2026}".into())), RuntimeValue::String(Shared::new("NFKC".into()))].into(), Ok(RuntimeValue::String(Shared::new("...".into()))))]
- // Combining acute accent stays attached to its base character as a single grapheme.
- #[case("graphemes", vec![RuntimeValue::String(Shared::new("e\u{0301}f".into()))].into(), Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("e\u{0301}".into())), RuntimeValue::String(Shared::new("f".into()))]))))]
- // Emoji ZWJ sequence (family) is one extended grapheme cluster despite five code points.
- #[case("graphemes", vec![RuntimeValue::String(Shared::new("๐จ\u{200d}๐ฉ\u{200d}๐ง".into()))].into(), Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("๐จ\u{200d}๐ฉ\u{200d}๐ง".into()))]))))]
- #[case("grapheme_len", vec![RuntimeValue::String(Shared::new("e\u{0301}f".into()))].into(), Ok(RuntimeValue::Number(2.into())))]
- #[case("grapheme_len", vec![RuntimeValue::String(Shared::new("๐จ\u{200d}๐ฉ\u{200d}๐ง".into()))].into(), Ok(RuntimeValue::Number(1.into())))]
- #[case("token_count", vec![RuntimeValue::String(Shared::new("Hello, world!".into())), RuntimeValue::String(Shared::new("gpt-4".into()))].into(), Ok(RuntimeValue::Number(4.into())))]
- #[case("token_count", vec![RuntimeValue::String(Shared::new("".into())), RuntimeValue::String(Shared::new("gpt-4".into()))].into(), Ok(RuntimeValue::Number(0.into())))]
- #[case("token_count", vec![RuntimeValue::String(Shared::new("Hello, world!".into()))].into(), Ok(RuntimeValue::Number(4.into())))]
- #[case("token_count", vec![RuntimeValue::String(Shared::new("".into()))].into(), Ok(RuntimeValue::Number(0.into())))]
- #[case(
- "token_compress",
- vec![
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::Markdown(Shared::new(Node::from("hi".to_string())), None)])),
- RuntimeValue::Number(1000.into()),
- ].into(),
- Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Markdown(Shared::new(Node::from("hi".to_string())), None)])))
- )]
- #[case(
- "token_compress",
- vec![RuntimeValue::None, RuntimeValue::Number(100.into())].into(),
- Ok(RuntimeValue::Array(Shared::new(vec![])))
- )]
- #[case(
- "token_compress",
- vec![
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::Markdown(Shared::new(Node::from("hi".to_string())), None)])),
- RuntimeValue::Number(1000.into()),
- RuntimeValue::String(Shared::new("gpt-4".into())),
- ].into(),
- Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Markdown(Shared::new(Node::from("hi".to_string())), None)])))
- )]
- #[case(
- "token_compress",
- vec![RuntimeValue::None, RuntimeValue::Number(100.into()), RuntimeValue::String(Shared::new("gpt-4".into()))].into(),
- Ok(RuntimeValue::Array(Shared::new(vec![])))
- )]
- #[case("abs", vec![RuntimeValue::Number((-10).into())].into(), Ok(RuntimeValue::Number(10.into())))]
- #[case("ceil", vec![RuntimeValue::Number(3.2.into())].into(), Ok(RuntimeValue::Number(4.0.into())))]
- #[case("floor", vec![RuntimeValue::Number(3.8.into())].into(), Ok(RuntimeValue::Number(3.0.into())))]
- #[case("round", vec![RuntimeValue::Number(3.5.into())].into(), Ok(RuntimeValue::Number(4.0.into())))]
- #[case("sin", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("cos", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(1.0.into())))]
- #[case("tan", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("log", vec![RuntimeValue::Number(1.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("asin", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("acos", vec![RuntimeValue::Number(1.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("atan", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("sinh", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("cosh", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(1.0.into())))]
- #[case("tanh", vec![RuntimeValue::Number(0.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("log2", vec![RuntimeValue::Number(8.0.into())].into(), Ok(RuntimeValue::Number(3.0.into())))]
- #[case("cbrt", vec![RuntimeValue::Number(27.0.into())].into(), Ok(RuntimeValue::Number(3.0.into())))]
- #[case("atan2", vec![RuntimeValue::Number(0.0.into()), RuntimeValue::Number(1.0.into())].into(), Ok(RuntimeValue::Number(0.0.into())))]
- #[case("hypot", vec![RuntimeValue::Number(3.0.into()), RuntimeValue::Number(4.0.into())].into(), Ok(RuntimeValue::Number(5.0.into())))]
- #[case("add", vec![RuntimeValue::Number(3.0.into()), RuntimeValue::Number(2.0.into())].into(), Ok(RuntimeValue::Number(5.0.into())))]
- #[case("sub", vec![RuntimeValue::Number(5.0.into()), RuntimeValue::Number(3.0.into())].into(), Ok(RuntimeValue::Number(2.0.into())))]
- #[case("mul", vec![RuntimeValue::Number(4.0.into()), RuntimeValue::Number(2.0.into())].into(), Ok(RuntimeValue::Number(8.0.into())))]
- #[case("div", vec![RuntimeValue::Number(8.0.into()), RuntimeValue::Number(2.0.into())].into(), Ok(RuntimeValue::Number(4.0.into())))]
- #[case("has", vec![DictMap::from_iter([(Ident::new("a"), RuntimeValue::Number(1.into())), (Ident::new("b"), RuntimeValue::Number(2.into()))]).into(), RuntimeValue::String(Shared::new("a".into()))].into(), Ok(RuntimeValue::Boolean(true)))]
- #[case("has", vec![DictMap::from_iter([(Ident::new("a"), RuntimeValue::Number(1.into())), (Ident::new("b"), RuntimeValue::Number(2.into()))]).into(), RuntimeValue::String(Shared::new("c".into()))].into(), Ok(RuntimeValue::Boolean(false)))]
- #[case("has", vec![DictMap::from_iter([(Ident::new("a"), RuntimeValue::None)]).into(), RuntimeValue::String(Shared::new("a".into()))].into(), Ok(RuntimeValue::Boolean(true)))]
- #[case("has", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into())])), RuntimeValue::Number(1.into())].into(), Ok(RuntimeValue::Boolean(true)))]
- #[case("has", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into())])), RuntimeValue::Number(5.into())].into(), Ok(RuntimeValue::Boolean(false)))]
- #[case("has", vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into()), RuntimeValue::Number(3.into())])), RuntimeValue::Number((-1).into())].into(), Ok(RuntimeValue::Boolean(false)))]
- #[case("has", vec![RuntimeValue::String(Shared::new("not a container".into())), RuntimeValue::String(Shared::new("key".into()))].into(), Ok(RuntimeValue::Boolean(false)))]
- fn test_eval_builtin(#[case] func_name: &str, #[case] args: Args, #[case] expected: Result) {
- let ident = Ident::new(func_name);
- assert_eq!(
- eval_builtin(&RuntimeValue::None, &ident, args, &VmEnv::default()),
- expected
- );
- }
-
- #[rstest]
- #[case("div", vec![RuntimeValue::Number(1.0.into()), RuntimeValue::Number(0.0.into())].into(), Error::ZeroDivision)]
- #[case("unknown_func", vec![RuntimeValue::Number(1.0.into())].into(), Error::NotDefined("unknown_func".to_string(), vec![]))]
- #[case("add", vec![].into(), Error::InvalidNumberOfArguments("add".to_string(), 2, 0))]
- #[case("add", vec![RuntimeValue::Boolean(true), RuntimeValue::Number(1.0.into())].into(),
- Error::InvalidTypes("add".to_string(), vec![RuntimeValue::Boolean(true), RuntimeValue::Number(1.0.into())]))]
- #[case("unicode_normalize", vec![RuntimeValue::String(Shared::new("abc".into())), RuntimeValue::String(Shared::new("bogus".into()))].into(),
- Error::Runtime("unicode_normalize: invalid normalization form `bogus`, expected one of \"nfc\", \"nfd\", \"nfkc\", \"nfkd\"".to_string()))]
- fn test_eval_builtin_errors(#[case] func_name: &str, #[case] args: Args, #[case] expected_error: Error) {
- let ident = Ident::new(func_name);
- let result = eval_builtin(&RuntimeValue::None, &ident, args, &VmEnv::default());
- assert!(result.is_err());
- assert_eq!(result.unwrap_err(), expected_error);
- }
-
- #[rstest]
- #[case("halt", 1)]
- #[case("convert", 2)]
- #[case("date_add", 3)]
- fn test_builtin_func_returns_arity_error_when_called_directly(#[case] func_name: &str, #[case] expected: u8) {
- let ident = Ident::new(func_name);
- let func = get_builtin_functions(&ident).expect("builtin is registered").func;
- let args: Args = vec![].into();
- let result = func(&ident, &RuntimeValue::None, args, &VmEnv::default());
- assert_eq!(
- result.unwrap_err(),
- Error::InvalidNumberOfArguments(func_name.to_string(), expected, 0)
- );
- }
-
- #[test]
- fn test_gmtime_epoch() {
- // Unix epoch (0) โ 1970-01-01T00:00:00 UTC (Thursday)
- // format: [year, mon(0-11), mday, hour, min, sec, wday(0=Sun), yday(0-365)]
- let ident = Ident::new("gmtime");
- let args = vec![RuntimeValue::Number(0.into())];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap();
- assert_eq!(
- result,
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(1970.into()), // year
- RuntimeValue::Number(0.into()), // mon (Jan=0)
- RuntimeValue::Number(1.into()), // mday
- RuntimeValue::Number(0.into()), // hour
- RuntimeValue::Number(0.into()), // min
- RuntimeValue::Number(0.into()), // sec
- RuntimeValue::Number(4.into()), // wday (Thu=4)
- RuntimeValue::Number(0.into()), // yday
- ]))
- );
- }
-
- #[test]
- fn test_gmtime_known_date() {
- // 2024-01-01T00:00:00 UTC = 1704067200 seconds
- let ident = Ident::new("gmtime");
- let args = vec![RuntimeValue::Number(1704067200_i64.into())];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap();
- assert_eq!(
- result,
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(2024.into()), // year
- RuntimeValue::Number(0.into()), // mon (Jan=0)
- RuntimeValue::Number(1.into()), // mday
- RuntimeValue::Number(0.into()), // hour
- RuntimeValue::Number(0.into()), // min
- RuntimeValue::Number(0.into()), // sec
- RuntimeValue::Number(1.into()), // wday (Mon=1)
- RuntimeValue::Number(0.into()), // yday
- ]))
- );
- }
-
- #[rstest]
- #[case(0, 0)]
- #[case(1704067200_i64, 1704067200_i64)]
- #[case(1718454645_i64, 1718454645_i64)]
- fn test_mktime_roundtrip(#[case] secs: i64, #[case] expected: i64) {
- let env = VmEnv::default();
- let gmtime_ident = Ident::new("gmtime");
- let mktime_ident = Ident::new("mktime");
-
- let arr = eval_builtin(
- &RuntimeValue::None,
- &gmtime_ident,
- vec![RuntimeValue::Number(secs.into())].into(),
- &env,
- )
- .unwrap();
- let result = eval_builtin(&RuntimeValue::None, &mktime_ident, vec![arr].into(), &env).unwrap();
- assert_eq!(result, RuntimeValue::Number(expected.into()));
- }
-
- fn call_uuid_fn(name: &str) -> String {
- let env = VmEnv::default();
- match eval_builtin(&RuntimeValue::None, &Ident::new(name), vec![].into(), &env).unwrap() {
- RuntimeValue::String(s) => s.to_string(),
- other => panic!("{name} should return a string, got {other:?}"),
- }
- }
-
- fn assert_uuid_shape(uuid: &str, expected_version: char) {
- let parts: Vec<&str> = uuid.split('-').collect();
- assert_eq!(parts.len(), 5, "uuid should have 5 hyphen-separated groups");
- assert_eq!(
- [
- parts[0].len(),
- parts[1].len(),
- parts[2].len(),
- parts[3].len(),
- parts[4].len()
- ],
- [8, 4, 4, 4, 12]
- );
- assert!(uuid.chars().all(|c| c == '-' || c.is_ascii_hexdigit()));
- assert_eq!(parts[2].chars().next().unwrap(), expected_version, "version nibble");
- assert!(
- matches!(parts[3].chars().next().unwrap(), '8' | '9' | 'a' | 'b'),
- "variant nibble should be 10xxxxxx"
- );
- }
-
- #[test]
- fn test_uuid_is_version_4() {
- assert_uuid_shape(&call_uuid_fn("uuid"), '4');
- }
-
- #[test]
- fn test_uuid_v4_is_version_4() {
- assert_uuid_shape(&call_uuid_fn("uuid_v4"), '4');
- }
-
- #[test]
- fn test_uuid_v7_is_version_7() {
- assert_uuid_shape(&call_uuid_fn("uuid_v7"), '7');
- }
-
- #[test]
- fn test_uuid_calls_are_unique() {
- let values: std::collections::HashSet = (0..200).map(|_| call_uuid_fn("uuid")).collect();
- assert_eq!(values.len(), 200, "uuid() should not repeat across calls");
- }
-
- #[test]
- fn test_rand_is_in_unit_range() {
- let env = VmEnv::default();
- for _ in 0..200 {
- match eval_builtin(&RuntimeValue::None, &Ident::new("rand"), vec![].into(), &env).unwrap() {
- RuntimeValue::Number(n) => assert!((0.0..1.0).contains(&n.value()), "rand() out of [0, 1)"),
- other => panic!("rand() should return a number, got {other:?}"),
- }
- }
- }
-
- #[rstest]
- #[case(1, 10)]
- #[case(-5, 5)]
- #[case(7, 7)]
- fn test_rand_int_within_bounds(#[case] min: i64, #[case] max: i64) {
- let env = VmEnv::default();
- for _ in 0..200 {
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("rand_int"),
- vec![RuntimeValue::Number(min.into()), RuntimeValue::Number(max.into())].into(),
- &env,
- )
- .unwrap();
- match result {
- RuntimeValue::Number(n) => {
- let v = n.to_int();
- assert!((min..=max).contains(&v), "rand_int({min}, {max}) produced {v}");
- }
- other => panic!("rand_int should return a number, got {other:?}"),
- }
- }
- }
-
- #[test]
- fn test_rand_seeded_is_deterministic_and_in_unit_range() {
- let env = VmEnv::default();
- let call = |seed: i64| match eval_builtin(
- &RuntimeValue::None,
- &Ident::new("rand"),
- vec![RuntimeValue::Number(seed.into())].into(),
- &env,
- )
- .unwrap()
- {
- RuntimeValue::Number(n) => n.value(),
- other => panic!("rand(seed) should return a number, got {other:?}"),
- };
- let v = call(42);
- assert!((0.0..1.0).contains(&v), "rand(42) out of [0, 1)");
- assert_eq!(v, call(42), "rand(seed) should be deterministic for a fixed seed");
- }
-
- #[test]
- fn test_rand_int_seeded_is_deterministic_and_within_bounds() {
- let env = VmEnv::default();
- let call = |seed: i64| match eval_builtin(
- &RuntimeValue::None,
- &Ident::new("rand_int"),
- vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(10.into()),
- RuntimeValue::Number(seed.into()),
- ]
- .into(),
- &env,
- )
- .unwrap()
- {
- RuntimeValue::Number(n) => n.to_int(),
- other => panic!("rand_int(min, max, seed) should return a number, got {other:?}"),
- };
- let v = call(7);
- assert!((1..=10).contains(&v), "rand_int(1, 10, 7) produced {v}");
- assert_eq!(
- v,
- call(7),
- "rand_int(min, max, seed) should be deterministic for a fixed seed"
- );
- }
-
- #[test]
- fn test_rand_int_invalid_range_errors() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("rand_int"),
- vec![RuntimeValue::Number(10.into()), RuntimeValue::Number(1.into())].into(),
- &env,
- );
- assert!(result.is_err(), "rand_int(10, 1) should error since min > max");
- }
-
- #[test]
- fn test_random_string_uses_only_charset_chars_and_requested_length() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("random_string"),
- vec![
- RuntimeValue::Number(12.into()),
- RuntimeValue::String(Shared::new("abc".into())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- match result {
- RuntimeValue::String(s) => {
- assert_eq!(s.chars().count(), 12);
- assert!(s.chars().all(|c| "abc".contains(c)));
- }
- other => panic!("random_string should return a string, got {other:?}"),
- }
- }
-
- #[test]
- fn test_random_string_zero_length_is_empty() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("random_string"),
- vec![
- RuntimeValue::Number(0.into()),
- RuntimeValue::String(Shared::new("abc".into())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new("".into())));
- }
-
- #[test]
- fn test_random_string_rejects_lengths_above_the_allocation_limit() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("random_string"),
- vec![
- RuntimeValue::Number((MAX_RANGE_SIZE as i64 + 1).into()),
- RuntimeValue::String(Shared::new("abc".into())),
- ]
- .into(),
- &env,
- );
- assert!(matches!(result, Err(Error::Runtime(message)) if message.contains("random_string length")));
- }
-
- #[test]
- fn test_random_string_empty_charset_errors() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("random_string"),
- vec![
- RuntimeValue::Number(5.into()),
- RuntimeValue::String(Shared::new("".into())),
- ]
- .into(),
- &env,
- );
- assert!(
- result.is_err(),
- "random_string(5, \"\") should error since charset is empty"
- );
- }
-
- #[test]
- fn test_random_string_seeded_is_deterministic() {
- let env = VmEnv::default();
- let call = |seed: i64| match eval_builtin(
- &RuntimeValue::None,
- &Ident::new("random_string"),
- vec![
- RuntimeValue::Number(16.into()),
- RuntimeValue::String(Shared::new("abcdefghijklmnopqrstuvwxyz0123456789".into())),
- RuntimeValue::Number(seed.into()),
- ]
- .into(),
- &env,
- )
- .unwrap()
- {
- RuntimeValue::String(s) => s.to_string(),
- other => panic!("random_string should return a string, got {other:?}"),
- };
- assert_eq!(
- call(7),
- call(7),
- "random_string(len, charset, seed) should be deterministic for a fixed seed"
- );
- }
-
- #[test]
- fn test_random_string_calls_are_unique() {
- let env = VmEnv::default();
- let values: std::collections::HashSet = (0..200)
- .map(|_| {
- match eval_builtin(
- &RuntimeValue::None,
- &Ident::new("random_string"),
- vec![
- RuntimeValue::Number(16.into()),
- RuntimeValue::String(Shared::new("abcdefghijklmnopqrstuvwxyz0123456789".into())),
- ]
- .into(),
- &env,
- )
- .unwrap()
- {
- RuntimeValue::String(s) => s.to_string(),
- other => panic!("random_string should return a string, got {other:?}"),
- }
- })
- .collect();
- assert_eq!(
- values.len(),
- 200,
- "random_string(16, ...) should not repeat across calls"
- );
- }
-
- #[test]
- fn test_shuffle_preserves_elements() {
- let env = VmEnv::default();
- let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("shuffle"),
- vec![RuntimeValue::Array(Shared::new(input.clone()))].into(),
- &env,
- )
- .unwrap();
- match result {
- RuntimeValue::Array(shuffled) => {
- assert_eq!(shuffled.len(), input.len());
- let mut sorted_input = input.clone();
- let mut sorted_shuffled = (*shuffled).clone();
- sorted_input.sort_by(|a, b| a.partial_cmp(b).unwrap());
- sorted_shuffled.sort_by(|a, b| a.partial_cmp(b).unwrap());
- assert_eq!(sorted_input, sorted_shuffled);
- }
- other => panic!("shuffle should return an array, got {other:?}"),
- }
- }
-
- #[test]
- fn test_shuffle_seeded_is_deterministic() {
- let env = VmEnv::default();
- let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect();
- let call = |seed: i64| match eval_builtin(
- &RuntimeValue::None,
- &Ident::new("shuffle"),
- vec![
- RuntimeValue::Array(Shared::new(input.clone())),
- RuntimeValue::Number(seed.into()),
- ]
- .into(),
- &env,
- )
- .unwrap()
- {
- RuntimeValue::Array(shuffled) => (*shuffled).clone(),
- other => panic!("shuffle should return an array, got {other:?}"),
- };
- assert_eq!(
- call(3),
- call(3),
- "shuffle(arr, seed) should be deterministic for a fixed seed"
- );
- }
-
- #[test]
- fn test_sample_returns_subset_without_duplicates() {
- let env = VmEnv::default();
- let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("sample"),
- vec![
- RuntimeValue::Array(Shared::new(input.clone())),
- RuntimeValue::Number(4.into()),
- ]
- .into(),
- &env,
- )
- .unwrap();
- match result {
- RuntimeValue::Array(sampled) => {
- assert_eq!(sampled.len(), 4);
- for v in sampled.iter() {
- assert!(input.contains(v));
- }
- let mut sorted = (*sampled).clone();
- sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
- sorted.dedup();
- assert_eq!(sorted.len(), 4, "sample should not contain duplicates");
- }
- other => panic!("sample should return an array, got {other:?}"),
- }
- }
-
- #[test]
- fn test_sample_seeded_is_deterministic() {
- let env = VmEnv::default();
- let input: Vec = (1..=10).map(|n| RuntimeValue::Number(n.into())).collect();
- let call = |seed: i64| match eval_builtin(
- &RuntimeValue::None,
- &Ident::new("sample"),
- vec![
- RuntimeValue::Array(Shared::new(input.clone())),
- RuntimeValue::Number(4.into()),
- RuntimeValue::Number(seed.into()),
- ]
- .into(),
- &env,
- )
- .unwrap()
- {
- RuntimeValue::Array(sampled) => (*sampled).clone(),
- other => panic!("sample should return an array, got {other:?}"),
- };
- assert_eq!(
- call(11),
- call(11),
- "sample(arr, n, seed) should be deterministic for a fixed seed"
- );
- }
-
- #[test]
- fn test_sample_n_exceeds_length_errors() {
- let env = VmEnv::default();
- let input: Vec = (1..=3).map(|n| RuntimeValue::Number(n.into())).collect();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("sample"),
- vec![RuntimeValue::Array(Shared::new(input)), RuntimeValue::Number(10.into())].into(),
- &env,
- );
- assert!(
- result.is_err(),
- "sample(arr, n) should error when n exceeds the array length"
- );
- }
-
- #[rstest]
- #[case(1704067200_i64, "%Y-%m-%d", "2024-01-01")]
- #[case(0_i64, "%Y-%m-%dT%H:%M:%S", "1970-01-01T00:00:00")]
- #[case(1704067200_i64, "%Y", "2024")]
- fn test_strftime(#[case] ts: i64, #[case] fmt: &str, #[case] expected: &str) {
- let ident = Ident::new("strftime");
- let args = vec![
- RuntimeValue::Number(ts.into()),
- RuntimeValue::String(Shared::new(fmt.into())),
- ];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.into())));
- }
-
- #[rstest]
- #[case("2024-01-01", "%Y-%m-%d", 1704067200_i64)]
- #[case("1970-01-01T00:00:00", "%Y-%m-%dT%H:%M:%S", 0_i64)]
- #[case("01/02/2024", "%m/%d/%Y", 1704153600_i64)]
- fn test_strptime(#[case] date_str: &str, #[case] fmt: &str, #[case] expected: i64) {
- let ident = Ident::new("strptime");
- let args = vec![
- RuntimeValue::String(Shared::new(date_str.into())),
- RuntimeValue::String(Shared::new(fmt.into())),
- ];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default()).unwrap();
- assert_eq!(result, RuntimeValue::Number(expected.into()));
- }
-
- #[test]
- fn test_strptime_invalid_format() {
- let ident = Ident::new("strptime");
- let args = vec![
- RuntimeValue::String(Shared::new("not-a-date".into())),
- RuntimeValue::String(Shared::new("%Y-%m-%d".into())),
- ];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- fn gmtime_array(secs: i64) -> RuntimeValue {
- let env = VmEnv::default();
- eval_builtin(
- &RuntimeValue::None,
- &Ident::new("gmtime"),
- vec![RuntimeValue::Number(secs.into())].into(),
- &env,
- )
- .unwrap()
- }
-
- // date_add: simple durations
- #[rstest]
- #[case(1704067200_i64, 60, "seconds", 1704067260_i64)]
- #[case(1704067200_i64, 5, "minutes", 1704067500_i64)]
- #[case(1704067200_i64, 2, "hours", 1704074400_i64)]
- #[case(1704067200_i64, 1, "days", 1704153600_i64)]
- #[case(1704067200_i64, -1, "days", 1703980800_i64)]
- #[case(1704067200_i64, 1, "weeks", 1704672000_i64)]
- fn test_date_add_duration(#[case] base: i64, #[case] n: i64, #[case] unit: &str, #[case] expected_secs: i64) {
- let env = VmEnv::default();
- let arr = gmtime_array(base);
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_add"),
- vec![
- arr,
- RuntimeValue::Number(n.into()),
- RuntimeValue::String(Shared::new(unit.into())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- // convert result array back to timestamp via mktime and compare
- let ts = eval_builtin(&RuntimeValue::None, &Ident::new("mktime"), vec![result].into(), &env).unwrap();
- assert_eq!(ts, RuntimeValue::Number(expected_secs.into()));
- }
-
- // date_add: calendar-aware month/year arithmetic
- #[test]
- fn test_date_add_months_end_of_month() {
- // 2024-01-31 + 1 month = 2024-02-29 (leap year)
- let env = VmEnv::default();
- let arr = gmtime_array(1706659200); // 2024-01-31T00:00:00Z
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_add"),
- vec![
- arr,
- RuntimeValue::Number(1.into()),
- RuntimeValue::String(Shared::new("months".into())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- // 2024-02-29T00:00:00Z = 1709164800
- let ts = eval_builtin(&RuntimeValue::None, &Ident::new("mktime"), vec![result].into(), &env).unwrap();
- assert_eq!(ts, RuntimeValue::Number(1709164800_i64.into()));
- }
-
- #[test]
- fn test_date_add_years() {
- // 2024-02-29 + 1 year = 2025-02-28 (non-leap year clamps)
- let env = VmEnv::default();
- let arr = gmtime_array(1709164800); // 2024-02-29T00:00:00Z
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_add"),
- vec![
- arr,
- RuntimeValue::Number(1.into()),
- RuntimeValue::String(Shared::new("years".into())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- // 2025-02-28T00:00:00Z = 1740700800
- let ts = eval_builtin(&RuntimeValue::None, &Ident::new("mktime"), vec![result].into(), &env).unwrap();
- assert_eq!(ts, RuntimeValue::Number(1740700800_i64.into()));
- }
-
- #[test]
- fn test_date_add_invalid_unit() {
- let env = VmEnv::default();
- let arr = gmtime_array(0);
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_add"),
- vec![
- arr,
- RuntimeValue::Number(1.into()),
- RuntimeValue::String(Shared::new("centuries".into())),
- ]
- .into(),
- &env,
- );
- assert!(matches!(result, Err(Error::Runtime(_))));
- }
-
- // date_diff: difference in various units
- #[rstest]
- #[case(1704067200_i64, 1704153600_i64, "seconds", 86400_i64)]
- #[case(1704067200_i64, 1704153600_i64, "minutes", 1440_i64)]
- #[case(1704067200_i64, 1704153600_i64, "hours", 24_i64)]
- #[case(1704067200_i64, 1704153600_i64, "days", 1_i64)]
- #[case(1704067200_i64, 1704672000_i64, "weeks", 1_i64)]
- #[case(1704153600_i64, 1704067200_i64, "seconds", -86400_i64)]
- fn test_date_diff(#[case] base1: i64, #[case] base2: i64, #[case] unit: &str, #[case] expected: i64) {
- let env = VmEnv::default();
- let arr1 = gmtime_array(base1);
- let arr2 = gmtime_array(base2);
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_diff"),
- vec![arr1, arr2, RuntimeValue::String(Shared::new(unit.into()))].into(),
- &env,
- )
- .unwrap();
- assert_eq!(result, RuntimeValue::Number(expected.into()));
- }
-
- #[test]
- fn test_date_diff_invalid_unit() {
- let env = VmEnv::default();
- let arr = gmtime_array(0);
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_diff"),
- vec![arr.clone(), arr, RuntimeValue::String(Shared::new("months".into()))].into(),
- &env,
- );
- assert!(matches!(result, Err(Error::Runtime(_))));
- }
-
- #[test]
- fn test_gmtime_invalid_type() {
- let ident = Ident::new("gmtime");
- let args = vec![RuntimeValue::String(Shared::new("not a number".into()))];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default());
- assert!(matches!(result, Err(Error::InvalidTypes(_, _))));
- }
-
- #[test]
- fn test_mktime_invalid_input() {
- let ident = Ident::new("mktime");
- let args = vec![RuntimeValue::String(Shared::new("not an array".into()))];
- let result = eval_builtin(&RuntimeValue::None, &ident, args.into(), &VmEnv::default());
- assert!(matches!(result, Err(Error::InvalidTypes(_, _))));
- }
-
- #[test]
- fn test_date_add_malformed_array_error_prefix() {
- let env = VmEnv::default();
- let bad_arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("x".into())); 8]));
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_add"),
- vec![
- bad_arr,
- RuntimeValue::Number(1.into()),
- RuntimeValue::String(Shared::new("days".into())),
- ]
- .into(),
- &env,
- );
- match result {
- Err(Error::Runtime(msg)) => assert!(msg.starts_with("date_add:"), "expected date_add prefix, got: {msg}"),
- other => panic!("expected Runtime error, got: {other:?}"),
- }
- }
-
- #[test]
- fn test_date_diff_malformed_array_error_prefix() {
- let env = VmEnv::default();
- let bad_arr = RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("x".into())); 8]));
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_diff"),
- vec![
- bad_arr.clone(),
- bad_arr,
- RuntimeValue::String(Shared::new("days".into())),
- ]
- .into(),
- &env,
- );
- match result {
- Err(Error::Runtime(msg)) => assert!(msg.starts_with("date_diff:"), "expected date_diff prefix, got: {msg}"),
- other => panic!("expected Runtime error, got: {other:?}"),
- }
- }
-
- // date_relative: base is 2024-01-15T00:00:00Z (Monday), 1705276800
- #[rstest]
- #[case("now", 1705276800_i64)]
- #[case("today", 1705276800_i64)]
- #[case("yesterday", 1705190400_i64)]
- #[case("tomorrow", 1705363200_i64)]
- #[case("3 days ago", 1705017600_i64)]
- #[case("in 2 weeks", 1706486400_i64)]
- #[case("next monday", 1705881600_i64)]
- #[case("last friday", 1705017600_i64)]
- fn test_date_relative(#[case] input: &str, #[case] expected_secs: i64) {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_relative"),
- vec![
- RuntimeValue::Number(1705276800_i64.into()),
- RuntimeValue::String(Shared::new(input.into())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- assert_eq!(result, RuntimeValue::Number(expected_secs.into()));
- }
-
- #[test]
- fn test_date_relative_invalid_expression() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_relative"),
- vec![
- RuntimeValue::Number(1705276800_i64.into()),
- RuntimeValue::String(Shared::new("not a relative date".into())),
- ]
- .into(),
- &env,
- );
- match result {
- Err(Error::Runtime(msg)) => assert!(
- msg.starts_with("date_relative:"),
- "expected date_relative prefix, got: {msg}"
- ),
- other => panic!("expected Runtime error, got: {other:?}"),
- }
- }
-
- #[test]
- fn test_date_relative_invalid_types() {
- let env = VmEnv::default();
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("date_relative"),
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())].into(),
- &env,
- );
- assert!(matches!(result, Err(Error::InvalidTypes(_, _))));
- }
-
- #[test]
- fn test_implicit_first_arg() {
- let ident = Ident::new("starts_with");
- let first_arg = RuntimeValue::String(Shared::new("hello world".into()));
- let args = vec![RuntimeValue::String(Shared::new("hello".into()))];
-
- let result = eval_builtin(&first_arg, &ident, args.into(), &VmEnv::default());
- assert_eq!(result, Ok(RuntimeValue::Boolean(true)));
- }
-
- #[rstest]
- #[case::code(
- Node::Code(mq_markdown::Code { value: "test".into(), lang: Some("rust".into()), fence: true, meta: None, position: None }),
- Selector::Code,
- true
- )]
- #[case::inline_code(
- Node::CodeInline(mq_markdown::CodeInline { value: "test".into(), position: None }),
- Selector::InlineCode,
- true
- )]
- #[case::inline_math(
- Node::MathInline(mq_markdown::MathInline { value: "test".into(), position: None }),
- Selector::InlineMath,
- true
- )]
- #[case::strong(
- Node::Strong(mq_markdown::Strong { values: vec!["test".to_string().into()], position: None }),
- Selector::Strong,
- true
- )]
- #[case::emphasis(
- Node::Emphasis(mq_markdown::Emphasis{ values: vec!["test".to_string().into()], position: None }),
- Selector::Emphasis,
- true
- )]
- #[case::delete(
- Node::Delete(mq_markdown::Delete{ values: vec!["test".to_string().into()], position: None }),
- Selector::Delete,
- true
- )]
- #[case::link(
- Node::Link(mq_markdown::Link { url: mq_markdown::Url::new("https://example.com".into()), values: Vec::new(), title: None, position: None }),
- Selector::Link,
- true
- )]
- #[case::heading_matching_depth(
- Node::Heading(mq_markdown::Heading { depth: mq_markdown::HeadingDepth::H2, values: vec!["test".to_string().into()], position: None }),
- Selector::Heading(Some(2)),
- true
- )]
- #[case::heading_wrong_depth(
- Node::Heading(mq_markdown::Heading { depth: mq_markdown::HeadingDepth::H2, values: vec!["test".to_string().into()], position: None }),
- Selector::Heading(Some(3)),
- false
- )]
- #[case::table_cell_with_matching_row_col(
- Node::TableCell(mq_markdown::TableCell { row: 1, column: 2, values: vec!["test".to_string().into()], position: None }),
- Selector::Table(Some(1), Some(2)),
- true
- )]
- #[case::table_cell_with_wrong_row(
- Node::TableCell(mq_markdown::TableCell { row: 1, column: 2, values: vec!["test".to_string().into()], position: None }),
- Selector::Table(Some(2), Some(2)),
- false
- )]
- #[case::table_cell_with_only_row(
- Node::TableCell(mq_markdown::TableCell { row: 1, column: 2, values: vec!["test".to_string().into()], position: None }),
- Selector::Table(Some(1), None),
- true
- )]
- #[case::table_header_with_no_row_col(
- Node::TableAlign(mq_markdown::TableAlign { align: vec![], position: None }),
- Selector::Table(None, None),
- true
- )]
- #[case::table_header_with_only_row(
- Node::TableAlign(mq_markdown::TableAlign { align: vec![], position: None }),
- Selector::Table(Some(2), None),
- false
- )]
- #[case::table_header_with_only_col(
- Node::TableAlign(mq_markdown::TableAlign { align: vec![], position: None }),
- Selector::Table(None, Some(3)),
- false
- )]
- #[case::table_header_with_row_col(
- Node::TableAlign(mq_markdown::TableAlign { align: vec![], position: None }),
- Selector::Table(Some(1), Some(1)),
- false
- )]
- #[case::list_with_matching_index_checked(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(true), position: None }),
- Selector::List(Some(1), Some(true)),
- true
- )]
- #[case::list_with_wrong_index(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(true), position: None }),
- Selector::List(Some(2), Some(true)),
- false
- )]
- #[case::list_without_index(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(true), position: None }),
- Selector::List(None, None),
- true
- )]
- #[case::task_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(true), position: None }),
- Selector::Task,
- true
- )]
- #[case::task_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(false), position: None }),
- Selector::Task,
- true
- )]
- #[case::task_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: None, position: None }),
- Selector::Task,
- false
- )]
- #[case::todo_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(false), position: None }),
- Selector::Todo,
- true
- )]
- #[case::todo_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(true), position: None }),
- Selector::Todo,
- false
- )]
- #[case::todo_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: None, position: None }),
- Selector::Todo,
- false
- )]
- #[case::done_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(true), position: None }),
- Selector::Done,
- true
- )]
- #[case::done_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: Some(false), position: None }),
- Selector::Done,
- false
- )]
- #[case::done_list(
- Node::List(mq_markdown::List { marker: None, start: None, spread: false, values: vec!["test".to_string().into()], ordered: false, index: 1, level: 1, checked: None, position: None }),
- Selector::Done,
- false
- )]
- #[case::text(
- Node::Text(mq_markdown::Text { value: "test".into(), position: None }),
- Selector::Text,
- true
- )]
- #[case::html(
- Node::Html(mq_markdown::Html { value: "test
".into(), position: None }),
- Selector::Html,
- true
- )]
- #[case::yaml(
- Node::Yaml(mq_markdown::Yaml { value: "test".into(), position: None }),
- Selector::Yaml,
- true
- )]
- #[case::toml(
- Node::Toml(mq_markdown::Toml { value: "test".into(), position: None }),
- Selector::Toml,
- true
- )]
- #[case::break_(
- Node::Break(mq_markdown::Break{position: None}),
- Selector::Break,
- true
- )]
- #[case::image(
- Node::Image(mq_markdown::Image { alt: "".to_string(), url: "".to_string(), title: None, position: None }),
- Selector::Image,
- true
- )]
- #[case::image_ref(
- Node::ImageRef(mq_markdown::ImageRef{ alt: "".to_string(), ident: "".to_string(), label: None, position: None }),
- Selector::ImageRef,
- true
- )]
- #[case::footnote(
- Node::Footnote(mq_markdown::Footnote{ident: "".to_string(), values: vec!["test".to_string().into()], position: None}),
- Selector::Footnote,
- true
- )]
- #[case::footnote_ref(
- Node::FootnoteRef(mq_markdown::FootnoteRef{ident: "".to_string(), label: None, position: None}),
- Selector::FootnoteRef,
- true
- )]
- #[case::math(
- Node::Math(mq_markdown::Math { value: "E=mc^2".into(), position: None }),
- Selector::Math,
- true
- )]
- #[case::horizontal_rule(
- Node::HorizontalRule(mq_markdown::HorizontalRule{ marker: None, position: None }),
- Selector::HorizontalRule,
- true
- )]
- #[case::blockquote(
- Node::Blockquote(mq_markdown::Blockquote{ values: vec!["test".to_string().into()], position: None }),
- Selector::Blockquote,
- true
- )]
- #[case::definition(
- Node::Definition(mq_markdown::Definition { ident: "id".to_string(), url: mq_markdown::Url::new("url".into()), label: None, title: None, position: None }),
- Selector::Definition,
- true
- )]
- #[case::mdx_jsx_flow_element(
- Node::MdxJsxFlowElement(mq_markdown::MdxJsxFlowElement { name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxFlowElement,
- true
- )]
- #[case::mdx_flow_expression(
- Node::MdxFlowExpression(mq_markdown::MdxFlowExpression{ value: "value".into(), position: None }),
- Selector::MdxFlowExpression,
- true
- )]
- #[case::mdx_text_expression(
- Node::MdxTextExpression(mq_markdown::MdxTextExpression{ value: "value".into(), position: None }),
- Selector::MdxTextExpression,
- true
- )]
- #[case::mdx_js_esm(
- Node::MdxJsEsm(mq_markdown::MdxJsEsm{ value: "value".into(), position: None }),
- Selector::MdxJsEsm,
- true
- )]
- fn test_eval_selector(#[case] node: Node, #[case] selector: Selector, #[case] expected: bool) {
- assert_eq!(!eval_selector(&node, &selector).is_none(), expected);
- }
-
- #[test]
- fn test_eval_recursive_selector_with_children() {
- let node = Node::Heading(mq_markdown::Heading {
- values: vec![
- Node::Text(mq_markdown::Text {
- value: "hello".into(),
- position: None,
- }),
- Node::Link(mq_markdown::Link {
- url: mq_markdown::Url::new("url".into()),
- title: None,
- values: Vec::new(),
- position: None,
- }),
- ],
- position: None,
- depth: mq_markdown::HeadingDepth::H1,
- });
- let result = eval_selector(&node, &Selector::Recursive);
- assert_eq!(
- result,
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Markdown(
- Shared::new(Node::Text(mq_markdown::Text {
- value: "hello".into(),
- position: None,
- })),
- None
- ),
- RuntimeValue::Markdown(
- Shared::new(Node::Link(mq_markdown::Link {
- url: mq_markdown::Url::new("url".into()),
- title: None,
- values: Vec::new(),
- position: None,
- })),
- None
- ),
- ]))
- );
- }
-
- #[test]
- fn test_eval_recursive_selector_leaf_node() {
- let node = Node::Text(mq_markdown::Text {
- value: "leaf".into(),
- position: None,
- });
- let result = eval_selector(&node, &Selector::Recursive);
- assert_eq!(result, RuntimeValue::Array(Shared::new(vec![])));
- }
-
- #[test]
- fn test_eval_recursive_selector_nested() {
- let inner_text = Node::Text(mq_markdown::Text {
- value: "nested".into(),
- position: None,
- });
- let heading = Node::Heading(mq_markdown::Heading {
- values: vec![inner_text.clone()],
- position: None,
- depth: mq_markdown::HeadingDepth::H2,
- });
- let node = Node::Blockquote(mq_markdown::Blockquote {
- values: vec![heading.clone()],
- position: None,
- });
- let result = eval_selector(&node, &Selector::Recursive);
- assert_eq!(
- result,
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::new_markdown(inner_text),
- RuntimeValue::new_markdown(heading),
- ]))
- );
- }
-
- #[rstest]
- #[case(ParamNum::None, 0, true)]
- #[case(ParamNum::None, 1, false)]
- #[case(ParamNum::Fixed(2), 2, true)]
- #[case(ParamNum::Fixed(2), 1, false)]
- #[case(ParamNum::Fixed(2), 3, false)]
- #[case(ParamNum::Range(1, 3), 1, true)]
- #[case(ParamNum::Range(1, 3), 2, true)]
- #[case(ParamNum::Range(1, 3), 3, true)]
- #[case(ParamNum::Range(1, 3), 0, false)]
- #[case(ParamNum::Range(1, 3), 4, false)]
- fn test_param_num_is_valid(#[case] param_num: ParamNum, #[case] num_args: u8, #[case] expected: bool) {
- assert_eq!(param_num.is_valid(num_args), expected);
- }
-
- #[rstest]
- #[case(ParamNum::None, 0)]
- #[case(ParamNum::Fixed(2), 2)]
- #[case(ParamNum::Range(1, 3), 1)]
- fn test_param_num_to_num(#[case] param_num: ParamNum, #[case] expected: u8) {
- assert_eq!(param_num.to_num(), expected);
- }
-
- #[rstest]
- #[case(ParamNum::None, 0, false)]
- #[case(ParamNum::Fixed(2), 1, true)]
- #[case(ParamNum::Fixed(2), 0, false)]
- #[case(ParamNum::Range(1, 3), 0, true)]
- #[case(ParamNum::Range(1, 3), 1, false)]
- fn test_param_num_is_missing_one_params(#[case] param_num: ParamNum, #[case] num_args: u8, #[case] expected: bool) {
- assert_eq!(param_num.is_missing_one_params(num_args), expected);
- }
-
- // Tests for Dict functions
- #[rstest]
- #[case(
- DictMap::from_iter([("a", RuntimeValue::Number(1.0.into())), ("b", RuntimeValue::Number(2.0.into()))]),
- DictMap::from_iter([("c", RuntimeValue::Number(3.0.into()))]),
- DictMap::from_iter([("a", RuntimeValue::Number(1.0.into())), ("b", RuntimeValue::Number(2.0.into())), ("c", RuntimeValue::Number(3.0.into()))]),
- )]
- #[case(
- DictMap::from_iter([("a", RuntimeValue::Number(1.0.into()))]),
- DictMap::from_iter([("a", RuntimeValue::Number(99.0.into())), ("b", RuntimeValue::Number(2.0.into()))]),
- DictMap::from_iter([("a", RuntimeValue::Number(99.0.into())), ("b", RuntimeValue::Number(2.0.into()))]),
- )]
- #[case(
- DictMap::default(),
- DictMap::from_iter([("x", RuntimeValue::String(Shared::new("hello".into())))]),
- DictMap::from_iter([("x", RuntimeValue::String(Shared::new("hello".into())))]),
- )]
- #[case(
- DictMap::from_iter([("x", RuntimeValue::String(Shared::new("hello".into())))]),
- DictMap::default(),
- DictMap::from_iter([("x", RuntimeValue::String(Shared::new("hello".into())))]),
- )]
- fn test_eval_builtin_add_dict(#[case] d1: DictMap, #[case] d2: DictMap, #[case] expected: DictMap) {
- let ident = Ident::new("add");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Dict(Shared::new(d1)), RuntimeValue::Dict(Shared::new(d2))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Dict(Shared::new(expected))));
- }
-
- #[test]
- fn test_eval_builtin_new_dict() {
- let ident = Ident::new("dict");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![].into(), &VmEnv::default());
- assert!(result.is_ok());
- let map_val = result.unwrap();
- match map_val {
- RuntimeValue::Dict(map) => {
- assert_eq!(map.len(), 0);
- }
- _ => panic!("Expected Dict, got {:?}", map_val),
- }
-
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("key".into())),
- RuntimeValue::String(Shared::new("value".into())),
- ]))]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(
- result,
- Ok(RuntimeValue::Dict(Shared::new(DictMap::from_iter([(
- "key",
- RuntimeValue::String(Shared::new("value".into()))
- )]))))
- );
- }
-
- #[test]
- fn test_eval_builtin_set_dict() {
- let ident_set = Ident::new("set");
- let initial_map = RuntimeValue::new_dict();
-
- let args1 = vec![
- initial_map.clone(),
- RuntimeValue::String(Shared::new("name".into())),
- RuntimeValue::String(Shared::new("Jules".into())),
- ];
- let result1 = eval_builtin(&RuntimeValue::None, &ident_set, args1.into(), &VmEnv::default());
- assert!(result1.is_ok());
- let map_val1 = result1.unwrap();
- match &map_val1 {
- RuntimeValue::Dict(map) => {
- assert_eq!(map.len(), 1);
- assert_eq!(
- map.get(&Ident::new("name")),
- Some(&RuntimeValue::String(Shared::new("Jules".into())))
- );
- }
- _ => panic!("Expected Dict, got {:?}", map_val1),
- }
-
- let args2 = vec![
- map_val1.clone(),
- RuntimeValue::String(Shared::new("age".into())),
- RuntimeValue::Number(30.into()),
- ];
- let result2 = eval_builtin(&RuntimeValue::None, &ident_set, args2.into(), &VmEnv::default());
- assert!(result2.is_ok());
- let map_val2 = result2.unwrap();
- match &map_val2 {
- RuntimeValue::Dict(map) => {
- assert_eq!(map.len(), 2);
- assert_eq!(
- map.get(&Ident::new("name")),
- Some(&RuntimeValue::String(Shared::new("Jules".into())))
- );
- assert_eq!(map.get(&Ident::new("age")), Some(&RuntimeValue::Number(30.into())));
- }
- _ => panic!("Expected Dict, got {:?}", map_val2),
- }
-
- let args3 = vec![
- map_val2.clone(),
- RuntimeValue::String(Shared::new("name".into())),
- RuntimeValue::String(Shared::new("Vincent".into())),
- ];
- let result3 = eval_builtin(&RuntimeValue::None, &ident_set, args3.into(), &VmEnv::default());
- assert!(result3.is_ok());
- let map_val3 = result3.unwrap();
- match &map_val3 {
- RuntimeValue::Dict(map) => {
- assert_eq!(map.len(), 2);
- assert_eq!(
- map.get(&Ident::new("name")),
- Some(&RuntimeValue::String(Shared::new("Vincent".into())))
- );
- assert_eq!(map.get(&Ident::new("age")), Some(&RuntimeValue::Number(30.into())));
- }
- _ => panic!("Expected Dict, got {:?}", map_val3),
- }
-
- let mut nested_map_data = DictMap::default();
- nested_map_data.insert(Ident::new("level"), RuntimeValue::Number(2.into()));
- let nested_map: RuntimeValue = nested_map_data.into();
- let args4 = vec![
- map_val3.clone(),
- RuntimeValue::String(Shared::new("nested".into())),
- nested_map.clone(),
- ];
- let result4 = eval_builtin(&RuntimeValue::None, &ident_set, args4.into(), &VmEnv::default());
- assert!(result4.is_ok());
- match result4.unwrap() {
- RuntimeValue::Dict(map) => {
- assert_eq!(map.len(), 3);
- assert_eq!(map.get(&Ident::new("nested")), Some(&nested_map));
- }
- _ => panic!("Expected Dict"),
- }
-
- let args_err1 = vec![
- RuntimeValue::String(Shared::new("not_a_map".into())),
- RuntimeValue::String(Shared::new("key".into())),
- RuntimeValue::String(Shared::new("value".into())),
- ];
- let result_err1 = eval_builtin(&RuntimeValue::None, &ident_set, args_err1.into(), &VmEnv::default());
- assert_eq!(
- result_err1,
- Err(Error::InvalidTypes(
- "set".to_string(),
- vec![
- RuntimeValue::String(Shared::new("not_a_map".into())),
- RuntimeValue::String(Shared::new("key".into())),
- RuntimeValue::String(Shared::new("value".into()))
- ]
- ))
- );
-
- let args_err2 = vec![
- initial_map.clone(),
- RuntimeValue::Number(123.into()),
- RuntimeValue::String(Shared::new("value".into())),
- ];
- let result_err2 = eval_builtin(&RuntimeValue::None, &ident_set, args_err2.into(), &VmEnv::default());
- assert_eq!(
- result_err2,
- Err(Error::InvalidTypes(
- "set".to_string(),
- vec![
- initial_map.clone(),
- RuntimeValue::Number(123.into()),
- RuntimeValue::String(Shared::new("value".into()))
- ]
- ))
- );
- }
-
- #[test]
- fn test_eval_builtin_get_map() {
- let ident_get = Ident::new("get");
- let mut map_data = DictMap::default();
- map_data.insert("name", RuntimeValue::String(Shared::new("Jules".into())));
- map_data.insert("age", RuntimeValue::Number(30.into()));
- let map_val: RuntimeValue = map_data.into();
-
- let args1 = vec![map_val.clone(), RuntimeValue::String(Shared::new("name".into()))];
- let result1 = eval_builtin(&RuntimeValue::None, &ident_get, args1.into(), &VmEnv::default());
- assert_eq!(result1, Ok(RuntimeValue::String(Shared::new("Jules".into()))));
-
- let args2 = vec![map_val.clone(), RuntimeValue::String(Shared::new("location".into()))];
- let result2 = eval_builtin(&RuntimeValue::None, &ident_get, args2.into(), &VmEnv::default());
- assert_eq!(result2, Ok(RuntimeValue::None));
-
- let args_err1 = vec![
- RuntimeValue::String(Shared::new("not_a_map".into())),
- RuntimeValue::String(Shared::new("key".into())),
- ];
- let result_err1 = eval_builtin(&RuntimeValue::None, &ident_get, args_err1.into(), &VmEnv::default());
- assert_eq!(
- result_err1,
- Err(Error::Runtime(
- "cannot index string with string (arrays, strings and markdown take a number, dicts take a string or symbol)".to_string()
- ))
- );
-
- let args_err2 = vec![map_val.clone(), RuntimeValue::Number(123.into())];
- let result_err2 = eval_builtin(&RuntimeValue::None, &ident_get, args_err2.into(), &VmEnv::default());
- assert_eq!(
- result_err2,
- Err(Error::Runtime(
- "cannot index dict with number (arrays, strings and markdown take a number, dicts take a string or symbol)".to_string()
- ))
- );
- }
-
- #[test]
- fn test_eval_builtin_get_markdown_index_beyond_selector_range() {
- let ident_get = Ident::new("get");
- let values: Vec = (0..300)
- .map(|i| {
- Node::Text(mq_markdown::Text {
- value: format!("child{i}"),
- position: None,
- })
- })
- .collect();
- let parent = RuntimeValue::Markdown(
- Shared::new(Node::Strong(mq_markdown::Strong { values, position: None })),
- None,
- );
-
- // Selects a child past the old u8-backed Selector's 254 cap; the u32-backed
- // Selector has no trouble representing it.
- let in_range = eval_builtin(
- &RuntimeValue::None,
- &ident_get,
- vec![parent.clone(), RuntimeValue::Number(255.into())].into(),
- &VmEnv::default(),
- )
- .unwrap();
- assert_eq!(in_range.markdown_node().unwrap().value(), "child255");
-
- // Still degrades to None for an index that's actually out of range for the node
- // (only 300 children, so 300 itself is out of bounds), rather than erroring or
- // crashing.
- let out_of_range = eval_builtin(
- &RuntimeValue::None,
- &ident_get,
- vec![parent, RuntimeValue::Number(300.into())].into(),
- &VmEnv::default(),
- )
- .unwrap();
- assert_eq!(out_of_range.markdown_node(), None);
- }
-
- #[test]
- fn test_eval_builtin_keys_dict() {
- let ident_keys = Ident::new("keys");
- let empty_map = RuntimeValue::new_dict();
- let args1 = vec![empty_map.clone()];
- let result1 = eval_builtin(&RuntimeValue::None, &ident_keys, args1.into(), &VmEnv::default());
- assert_eq!(result1, Ok(RuntimeValue::Array(Shared::new(vec![]))));
-
- let mut map_data = DictMap::default();
- map_data.insert("name", RuntimeValue::String(Shared::new("Jules".into())));
- map_data.insert("age", RuntimeValue::Number(30.into()));
- let map_val: RuntimeValue = map_data.into();
- let args2 = vec![map_val.clone()];
- let result2 = eval_builtin(&RuntimeValue::None, &ident_keys, args2.into(), &VmEnv::default());
- assert!(result2.is_ok());
- match result2.unwrap() {
- RuntimeValue::Array(keys_array) => {
- assert_eq!(keys_array.len(), 2);
- let keys_str: Vec = Shared::unwrap_or_clone(keys_array)
- .into_iter()
- .map(|k| match k {
- RuntimeValue::String(s) => Shared::unwrap_or_clone(s),
- _ => panic!("Expected string key"),
- })
- .collect();
- assert_eq!(keys_str, vec!["name".to_string(), "age".to_string()]);
- }
- _ => panic!("Expected Array of keys"),
- }
-
- let args_err1 = vec![RuntimeValue::String(Shared::new("not_a_map".into()))];
- let result_err1 = eval_builtin(&RuntimeValue::None, &ident_keys, args_err1.into(), &VmEnv::default());
- assert_eq!(
- result_err1,
- Err(Error::InvalidTypes(
- "keys".to_string(),
- vec![RuntimeValue::String(Shared::new("not_a_map".into()))]
- ))
- );
-
- let args_err2 = vec![map_val.clone(), RuntimeValue::String(Shared::new("extra".into()))];
- let result_err2 = eval_builtin(&RuntimeValue::None, &ident_keys, args_err2.into(), &VmEnv::default());
- assert_eq!(
- result_err2,
- Err(Error::InvalidNumberOfArguments("keys".to_string(), 1, 2))
- );
- }
-
- #[test]
- fn test_eval_builtin_values_dict() {
- let ident_values = Ident::new("values");
- let empty_map = RuntimeValue::new_dict();
- let args1 = vec![empty_map.clone()];
- let result1 = eval_builtin(&RuntimeValue::None, &ident_values, args1.into(), &VmEnv::default());
- assert_eq!(result1, Ok(RuntimeValue::Array(Shared::new(vec![]))));
-
- let mut map_data = DictMap::default();
- map_data.insert("name", RuntimeValue::String(Shared::new("Jules".into())));
- map_data.insert("age", RuntimeValue::Number(30.into()));
- let map_val: RuntimeValue = map_data.into();
- let args2 = vec![map_val.clone()];
- let result2 = eval_builtin(&RuntimeValue::None, &ident_values, args2.into(), &VmEnv::default());
- assert!(result2.is_ok());
- match result2.unwrap() {
- RuntimeValue::Array(values_array) => {
- assert_eq!(values_array.len(), 2);
- assert!(values_array.contains(&RuntimeValue::String(Shared::new("Jules".into()))));
- assert!(values_array.contains(&RuntimeValue::Number(30.into())));
- }
- _ => panic!("Expected Array of values"),
- }
-
- let args_err1 = vec![RuntimeValue::String(Shared::new("not_a_map".into()))];
- let result_err1 = eval_builtin(&RuntimeValue::None, &ident_values, args_err1.into(), &VmEnv::default());
- assert_eq!(
- result_err1,
- Err(Error::InvalidTypes(
- "values".to_string(),
- vec![RuntimeValue::String(Shared::new("not_a_map".into()))]
- ))
- );
-
- let args_err2 = vec![map_val.clone(), RuntimeValue::String(Shared::new("extra".into()))];
- let result_err2 = eval_builtin(&RuntimeValue::None, &ident_values, args_err2.into(), &VmEnv::default());
- assert_eq!(
- result_err2,
- Err(Error::InvalidNumberOfArguments("values".to_string(), 1, 2))
- );
- }
-
- #[rstest]
- #[case::excessively_large_range(0, 2_000_000, 1)]
- #[case::negative_step_large_range(10_000_000, 0, -1)]
- #[case::just_over_limit(0, 1_000_000, 1)]
- fn test_range_size_limit_exceeds(#[case] start: isize, #[case] end: isize, #[case] step: isize) {
- let result = generate_numeric_range(start, end, step);
- assert!(result.is_err());
- if let Err(Error::Runtime(msg)) = result {
- assert!(msg.contains("exceeds maximum allowed size"));
- } else {
- panic!("Expected Runtime error");
- }
- }
-
- #[rstest]
- #[case::reasonable_range(0, 100, 1, 101)]
- #[case::exactly_at_limit(0, 999_999, 1, 1_000_000)]
- fn test_range_size_limit_success(
- #[case] start: isize,
- #[case] end: isize,
- #[case] step: isize,
- #[case] expected_len: usize,
- ) {
- let result = generate_numeric_range(start, end, step);
- assert!(result.is_ok());
- if let Ok(vec) = result {
- assert_eq!(vec.len(), expected_len);
- }
- }
-
- #[rstest]
- #[case::unicode_max_range('\u{0000}', '\u{10FFFF}', Some(1))]
- fn test_char_range_size_limit_exceeds(#[case] start: char, #[case] end: char, #[case] step: Option) {
- let result = generate_char_range(start, end, step);
- assert!(result.is_err());
- if let Err(Error::Runtime(msg)) = result {
- assert!(msg.contains("exceeds maximum allowed size"));
- } else {
- panic!("Expected Runtime error");
- }
- }
-
- #[rstest]
- #[case::reasonable_char_range('a', 'z', None, 26)]
- fn test_char_range_size_limit_success(
- #[case] start: char,
- #[case] end: char,
- #[case] step: Option,
- #[case] expected_len: usize,
- ) {
- let result = generate_char_range(start, end, step);
- assert!(result.is_ok());
- if let Ok(vec) = result {
- assert_eq!(vec.len(), expected_len);
- }
- }
-
- #[rstest]
- #[case::excessively_large_array_repeat(
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())],
- 600_000,
- "array repeat size"
- )]
- #[case::just_over_limit(
- vec![RuntimeValue::Number(1.into())],
- 1_000_001,
- "exceeds maximum allowed size"
- )]
- fn test_repeat_array_size_limit_exceeds(
- #[case] array: Vec,
- #[case] n: usize,
- #[case] expected_msg: &str,
- ) {
- let mut value = RuntimeValue::Array(Shared::new(array));
- let result = repeat(&mut value, n);
- assert!(result.is_err());
- if let Err(Error::Runtime(msg)) = result {
- assert!(msg.contains(expected_msg));
- } else {
- panic!("Expected Runtime error for array repeat");
- }
- }
-
- #[rstest]
- #[case::reasonable_array_repeat(
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())],
- 10,
- 20
- )]
- #[case::exactly_at_limit(
- vec![RuntimeValue::Number(1.into())],
- 1_000_000,
- 1_000_000
- )]
- fn test_repeat_array_size_limit_success(
- #[case] array: Vec,
- #[case] n: usize,
- #[case] expected_len: usize,
- ) {
- let mut value = RuntimeValue::Array(Shared::new(array));
- let result = repeat(&mut value, n);
- assert!(result.is_ok());
- if let Ok(RuntimeValue::Array(vec)) = result {
- assert_eq!(vec.len(), expected_len);
- } else {
- panic!("Expected successful array repeat");
- }
- }
-
- #[rstest]
- #[case::excessively_large_string_repeat("test", 300_000, "string repeat size")]
- fn test_repeat_string_size_limit_exceeds(#[case] string: &str, #[case] n: usize, #[case] expected_msg: &str) {
- let mut value = RuntimeValue::String(Shared::new(string.to_string()));
- let result = repeat(&mut value, n);
- assert!(result.is_err());
- if let Err(Error::Runtime(msg)) = result {
- assert!(msg.contains(expected_msg));
- } else {
- panic!("Expected Runtime error for string repeat");
- }
- }
-
- #[rstest]
- #[case::reasonable_string_repeat("test", 10, 40)]
- fn test_repeat_string_size_limit_success(#[case] string: &str, #[case] n: usize, #[case] expected_len: usize) {
- let mut value = RuntimeValue::String(Shared::new(string.to_string()));
- let result = repeat(&mut value, n);
- assert!(result.is_ok());
- if let Ok(RuntimeValue::String(s)) = result {
- assert_eq!(s.len(), expected_len);
- } else {
- panic!("Expected successful string repeat");
- }
- }
-
- #[rstest]
- #[case::set_array("set", vec![RuntimeValue::empty_array(), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into()), RuntimeValue::Number(1.into())])]
- #[case::insert_array("insert", vec![RuntimeValue::empty_array(), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into()), RuntimeValue::Number(1.into())])]
- #[case::insert_string("insert", vec![RuntimeValue::String(Shared::new("".into())), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into()), RuntimeValue::String(Shared::new("x".into()))])]
- #[case::del_array("del", vec![RuntimeValue::empty_array(), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into())])]
- #[case::del_string("del", vec![RuntimeValue::String(Shared::new("".into())), RuntimeValue::Number((MAX_RANGE_SIZE as i64).into())])]
- fn collection_mutators_reject_oversized_indices(#[case] name: &str, #[case] args: Vec) {
- let env = VmEnv::default();
- let result = eval_builtin(&RuntimeValue::None, &Ident::new(name), args.into(), &env);
- assert!(matches!(result, Err(Error::Runtime(message)) if message.contains("index")));
- }
-
- #[rstest]
- #[case::simple_no_header(
- "a,b,c\n1,2,3\n4,5,6",
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::String(Shared::new("b".to_string())),
- RuntimeValue::String(Shared::new("c".to_string())),
- ])),
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("1".to_string())),
- RuntimeValue::String(Shared::new("2".to_string())),
- RuntimeValue::String(Shared::new("3".to_string())),
- ])),
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("4".to_string())),
- RuntimeValue::String(Shared::new("5".to_string())),
- RuntimeValue::String(Shared::new("6".to_string())),
- ])),
- ])))
- )]
- #[case::single_row_no_header(
- "x,y",
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("x".to_string())),
- RuntimeValue::String(Shared::new("y".to_string())),
- ])),
- ])))
- )]
- #[case::empty_no_header(
- "",
- Ok(RuntimeValue::Array(Shared::new(vec![])))
- )]
- #[case::ragged_rows_no_header(
- "a,b,c\n1,2\n3,4,5,6",
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::String(Shared::new("b".to_string())),
- RuntimeValue::String(Shared::new("c".to_string())),
- ])),
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("1".to_string())),
- RuntimeValue::String(Shared::new("2".to_string())),
- ])),
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("3".to_string())),
- RuntimeValue::String(Shared::new("4".to_string())),
- RuntimeValue::String(Shared::new("5".to_string())),
- RuntimeValue::String(Shared::new("6".to_string())),
- ])),
- ])))
- )]
- fn test_csv_parse_no_header(#[case] csv: &str, #[case] expected: Result) {
- let ident = Ident::new("_csv_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(csv.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::simple_with_header(
- "name,age\nAlice,30\nBob,25",
- {
- let mut alice = DictMap::default();
- alice.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())));
- alice.insert(Ident::new("age"), RuntimeValue::String(Shared::new("30".to_string())));
- let mut bob = DictMap::default();
- bob.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Bob".to_string())));
- bob.insert(Ident::new("age"), RuntimeValue::String(Shared::new("25".to_string())));
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Dict(Shared::new(alice)),
- RuntimeValue::Dict(Shared::new(bob)),
- ])))
- }
- )]
- #[case::single_row_with_header(
- "id,value\n1,hello",
- {
- let mut row = DictMap::default();
- row.insert(Ident::new("id"), RuntimeValue::String(Shared::new("1".to_string())));
- row.insert(Ident::new("value"), RuntimeValue::String(Shared::new("hello".to_string())));
- Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row))])))
- }
- )]
- #[case::quoted_fields_with_header(
- "name,note\n\"Doe, Jane\",\"says \"\"hi\"\"\"",
- {
- let mut row = DictMap::default();
- row.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Doe, Jane".to_string())));
- row.insert(Ident::new("note"), RuntimeValue::String(Shared::new("says \"hi\"".to_string())));
- Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row))])))
- }
- )]
- #[case::ragged_rows_with_header(
- "a,b,c\n1,2\n3,4,5,6",
- {
- let mut short_row = DictMap::default();
- short_row.insert(Ident::new("a"), RuntimeValue::String(Shared::new("1".to_string())));
- short_row.insert(Ident::new("b"), RuntimeValue::String(Shared::new("2".to_string())));
- short_row.insert(Ident::new("c"), RuntimeValue::String(Shared::new("".to_string())));
- let mut long_row = DictMap::default();
- long_row.insert(Ident::new("a"), RuntimeValue::String(Shared::new("3".to_string())));
- long_row.insert(Ident::new("b"), RuntimeValue::String(Shared::new("4".to_string())));
- long_row.insert(Ident::new("c"), RuntimeValue::String(Shared::new("5".to_string())));
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Dict(Shared::new(short_row)),
- RuntimeValue::Dict(Shared::new(long_row)),
- ])))
- }
- )]
- fn test_csv_parse_with_header(#[case] csv: &str, #[case] expected: Result) {
- let ident = Ident::new("_csv_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::String(Shared::new(csv.to_string())),
- RuntimeValue::String(Shared::new(",".to_string())),
- RuntimeValue::Boolean(true),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::tsv_no_header(
- "a\tb\tc\n1\t2\t3",
- "\t",
- false,
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::String(Shared::new("b".to_string())),
- RuntimeValue::String(Shared::new("c".to_string())),
- ])),
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("1".to_string())),
- RuntimeValue::String(Shared::new("2".to_string())),
- RuntimeValue::String(Shared::new("3".to_string())),
- ])),
- ])))
- )]
- #[case::tsv_with_header(
- "name\tage\nAlice\t30",
- "\t",
- true,
- {
- let mut row = DictMap::default();
- row.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())));
- row.insert(Ident::new("age"), RuntimeValue::String(Shared::new("30".to_string())));
- Ok(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row))])))
- }
- )]
- fn test_csv_parse_custom_delimiter(
- #[case] csv: &str,
- #[case] delimiter: &str,
- #[case] has_header: bool,
- #[case] expected: Result,
- ) {
- let ident = Ident::new("_csv_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::String(Shared::new(csv.to_string())),
- RuntimeValue::String(Shared::new(delimiter.to_string())),
- RuntimeValue::Boolean(has_header),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::invalid_type_number(RuntimeValue::Number(42.into()))]
- #[case::invalid_type_bool(RuntimeValue::Boolean(false))]
- fn test_csv_parse_invalid_arg_type(#[case] invalid_arg: RuntimeValue) {
- let ident = Ident::new("_csv_parse");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![invalid_arg].into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::simple_object(
- r#"{"key": "value"}"#,
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("key"), RuntimeValue::String(Shared::new("value".to_string())));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::array(
- r#"[1, 2, 3]"#,
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- RuntimeValue::Number(3.into()),
- ])))
- )]
- #[case::nested(
- r#"{"a": [true, null], "b": {"c": 1.2}}"#,
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("a"), RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Boolean(true),
- RuntimeValue::NONE,
- ])));
- let mut inner = DictMap::default();
- inner.insert(Ident::new("c"), RuntimeValue::Number(1.2.into()));
- map.insert(Ident::new("b"), RuntimeValue::Dict(Shared::new(inner)));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::string(r#""hello""#, Ok(RuntimeValue::String(Shared::new("hello".to_string()))))]
- #[case::number(r#"42"#, Ok(RuntimeValue::Number(42.into())))]
- #[case::boolean(r#"false"#, Ok(RuntimeValue::Boolean(false)))]
- #[case::null(r#"null"#, Ok(RuntimeValue::NONE))]
- fn test_json_parse(#[case] json: &str, #[case] expected: Result) {
- let ident = Ident::new("_json_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(json.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::invalid_json(r#"{"key": "value""#)]
- #[case::invalid_type(RuntimeValue::Number(1.into()))]
- fn test_json_parse_error(#[case] input: impl Into) {
- let ident = Ident::new("_json_parse");
- let arg: RuntimeValue = match input.into() {
- RuntimeValue::Number(n) => RuntimeValue::Number(n),
- s => RuntimeValue::String(Shared::new(s.to_string())),
- };
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![arg].into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::mapping(
- "key: value",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("key"), RuntimeValue::String(Shared::new("value".to_string())));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::sequence(
- "- 1\n- 2\n- 3",
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- RuntimeValue::Number(3.into()),
- ])))
- )]
- #[case::nested(
- "a:\n b: 42",
- {
- let mut inner = DictMap::default();
- inner.insert(Ident::new("b"), RuntimeValue::Number(42.into()));
- let mut map = DictMap::default();
- map.insert(Ident::new("a"), RuntimeValue::Dict(Shared::new(inner)));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::boolean(
- "flag: true",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("flag"), RuntimeValue::Boolean(true));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::null(
- "value: null",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("value"), RuntimeValue::NONE);
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::float(
- "ratio: 1.5",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("ratio"), RuntimeValue::Number(1.5.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::multi_document(
- "a: 1\n---\nb: 2\n",
- {
- let mut first = DictMap::default();
- first.insert(Ident::new("a"), RuntimeValue::Number(1.into()));
- let mut second = DictMap::default();
- second.insert(Ident::new("b"), RuntimeValue::Number(2.into()));
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Dict(Shared::new(first)),
- RuntimeValue::Dict(Shared::new(second)),
- ])))
- }
- )]
- #[case::infinity(
- "up: .inf\ndown: -.inf\nplus: +.Inf",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("up"), RuntimeValue::Number(f64::INFINITY.into()));
- map.insert(Ident::new("down"), RuntimeValue::Number(f64::NEG_INFINITY.into()));
- map.insert(Ident::new("plus"), RuntimeValue::Number(f64::INFINITY.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::non_string_keys(
- "1: a\ntrue: b\n1.5: c\nnull: d",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("1"), RuntimeValue::String(Shared::new("a".to_string())));
- map.insert(Ident::new("true"), RuntimeValue::String(Shared::new("b".to_string())));
- map.insert(Ident::new("1.5"), RuntimeValue::String(Shared::new("c".to_string())));
- map.insert(Ident::new("null"), RuntimeValue::String(Shared::new("d".to_string())));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- fn test_yaml_parse(#[case] yaml: &str, #[case] expected: Result) {
- let ident = Ident::new("_yaml_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(yaml.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::invalid_type(RuntimeValue::Number(1.into()))]
- fn test_yaml_parse_error(#[case] input: impl Into) {
- let ident = Ident::new("_yaml_parse");
- let arg: RuntimeValue = match input.into() {
- RuntimeValue::Number(n) => RuntimeValue::Number(n),
- s => RuntimeValue::String(Shared::new(s.to_string())),
- };
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![arg].into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::simple_kv(
- "a: 1\nb: 2",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("a"), RuntimeValue::Number(1.into()));
- map.insert(Ident::new("b"), RuntimeValue::Number(2.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::nested_indent(
- "parent:\n child: value",
- {
- let mut child_map = DictMap::default();
- child_map.insert(Ident::new("child"), RuntimeValue::String(Shared::new("value".to_string())));
- let mut parent_map = DictMap::default();
- parent_map.insert(Ident::new("parent"), RuntimeValue::Dict(Shared::new(child_map)));
- Ok(RuntimeValue::Dict(Shared::new(parent_map)))
- }
- )]
- #[case::tabular_data(
- "hikes[2]{id,name}:\n 1,Blue Lake\n 2,Ridge Trail",
- {
- let mut row1 = DictMap::default();
- row1.insert(Ident::new("id"), RuntimeValue::Number(1.into()));
- row1.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Blue Lake".to_string())));
- let mut row2 = DictMap::default();
- row2.insert(Ident::new("id"), RuntimeValue::Number(2.into()));
- row2.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Ridge Trail".to_string())));
- let mut map = DictMap::default();
- map.insert(Ident::new("hikes"), RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(row1)), RuntimeValue::Dict(Shared::new(row2))])));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::inline_array(
- "items[3]: 1, 2, 3",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- RuntimeValue::Number(3.into()),
- ])));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::expanded_array(
- "items[2]:\n - 1\n - 2",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("items"), RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- ])));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::primitives(
- "s: \"string\"\nb: true\nn: null\nf: false",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("s"), RuntimeValue::String(Shared::new("string".to_string())));
- map.insert(Ident::new("b"), RuntimeValue::TRUE);
- map.insert(Ident::new("n"), RuntimeValue::NONE);
- map.insert(Ident::new("f"), RuntimeValue::FALSE);
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- fn test_toon_parse(#[case] toon: &str, #[case] expected: Result) {
- let ident = Ident::new("_toon_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(toon.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- // Each case is a lone `RuntimeValue`: multi-key dicts are exercised separately in
- // `test_toon_stringify_round_trip` instead of here, since `RuntimeValue::Dict` (a
- // `BTreeMap`) orders keys by interned symbol id, which isn't stable across
- // a test binary run and would make an exact-string assertion on more than one key flaky.
- #[rstest]
- #[case::string(RuntimeValue::String(Shared::new("hello".to_string())), "hello")]
- #[case::number(RuntimeValue::Number(42.into()), "42")]
- #[case::bool_true(RuntimeValue::TRUE, "true")]
- #[case::bool_false(RuntimeValue::FALSE, "false")]
- #[case::none(RuntimeValue::NONE, "null")]
- #[case::single_key_dict(
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())));
- RuntimeValue::Dict(Shared::new(map))
- },
- "name: Alice"
- )]
- #[case::array_of_primitives(
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())])),
- "[2]: 1,2"
- )]
- #[case::empty_array(RuntimeValue::Array(Shared::new(vec![])), "[]")]
- #[case::empty_dict(RuntimeValue::Dict(Shared::new(DictMap::default())), "")]
- #[case::empty_string_needs_quoting(RuntimeValue::String(Shared::new("".to_string())), "\"\"")]
- #[case::numeric_like_string_needs_quoting(RuntimeValue::String(Shared::new("123".to_string())), "\"123\"")]
- #[case::keyword_like_string_needs_quoting(RuntimeValue::String(Shared::new("true".to_string())), "\"true\"")]
- #[case::string_with_colon_needs_quoting(RuntimeValue::String(Shared::new("a:b".to_string())), "\"a:b\"")]
- #[case::string_with_delimiter_needs_quoting(RuntimeValue::String(Shared::new("a,b".to_string())), "\"a,b\"")]
- #[case::string_starting_with_dash_needs_quoting(RuntimeValue::String(Shared::new("-x".to_string())), "\"-x\"")]
- #[case::integer_above_i64_max(RuntimeValue::Number(2f64.powi(63).into()), "9223372036854775808")]
- #[case::integer_above_i64_max_up_to_u64(RuntimeValue::Number(1e19.into()), "10000000000000000000")]
- #[case::integer_i64_min(RuntimeValue::Number((i64::MIN as f64).into()), "-9223372036854775808")]
- fn test_toon_stringify(#[case] input: RuntimeValue, #[case] expected: &str) {
- let ident = Ident::new("_toon_stringify");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default());
- assert_eq!(result, Ok(RuntimeValue::String(Shared::new(expected.to_string()))));
- }
-
- fn toon_tabular_row(id: i64, name: &str) -> RuntimeValue {
- let mut map = DictMap::default();
- map.insert(Ident::new("id"), RuntimeValue::Number(id.into()));
- map.insert(Ident::new("name"), RuntimeValue::String(Shared::new(name.to_string())));
- RuntimeValue::Dict(Shared::new(map))
- }
-
- // Structural round trip (stringify then re-parse) rather than an exact string:
- // `RuntimeValue`'s `Dict` equality ignores key order, so this stays robust regardless
- // of how the global symbol interner happens to order a multi-key dict's fields.
- #[rstest]
- #[case::tabular_array(RuntimeValue::Array(Shared::new(vec![
- toon_tabular_row(1, "Blue Lake"),
- toon_tabular_row(2, "Ridge Trail"),
- ])))]
- #[case::nested_dict({
- let mut inner = DictMap::default();
- inner.insert(Ident::new("inner"), RuntimeValue::Number(1.into()));
- let mut outer = DictMap::default();
- outer.insert(Ident::new("outer"), RuntimeValue::Dict(Shared::new(inner)));
- RuntimeValue::Dict(Shared::new(outer))
- })]
- #[case::non_uniform_array(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(1.into()),
- toon_tabular_row(2, "Ridge Trail"),
- RuntimeValue::String(Shared::new("text".to_string())),
- ])))]
- fn test_toon_stringify_round_trip(#[case] original: RuntimeValue) {
- let env = VmEnv::default();
-
- let ident_stringify = Ident::new("_toon_stringify");
- let stringified = eval_builtin(
- &RuntimeValue::None,
- &ident_stringify,
- vec![original.clone()].into(),
- &env,
- )
- .unwrap();
- let RuntimeValue::String(toon_str) = stringified else {
- panic!("expected a string result");
- };
-
- let ident_parse = Ident::new("_toon_parse");
- let round_tripped = eval_builtin(
- &RuntimeValue::None,
- &ident_parse,
- vec![RuntimeValue::String(toon_str)].into(),
- &env,
- )
- .unwrap();
-
- assert_eq!(round_tripped, original);
- }
-
- #[rstest]
- #[case::simple_kv(
- "name = \"Alice\"\nage = 30",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())));
- map.insert(Ident::new("age"), RuntimeValue::Number(30.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::boolean(
- "enabled = true\ndisabled = false",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("enabled"), RuntimeValue::Boolean(true));
- map.insert(Ident::new("disabled"), RuntimeValue::Boolean(false));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::nested_table(
- "[server]\nhost = \"localhost\"\nport = 8080",
- {
- let mut inner = DictMap::default();
- inner.insert(Ident::new("host"), RuntimeValue::String(Shared::new("localhost".to_string())));
- inner.insert(Ident::new("port"), RuntimeValue::Number(8080.into()));
- let mut map = DictMap::default();
- map.insert(Ident::new("server"), RuntimeValue::Dict(Shared::new(inner)));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::array(
- "tags = [\"rust\", \"toml\"]",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("tags"), RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("rust".to_string())),
- RuntimeValue::String(Shared::new("toml".to_string())),
- ])));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::datetime(
- "d = 1979-05-27T07:32:00Z",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("d"), RuntimeValue::String(Shared::new("1979-05-27T07:32:00Z".to_string())));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- #[case::inf(
- "f = inf",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("f"), RuntimeValue::Number(f64::INFINITY.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- fn test_toml_parse(#[case] toml: &str, #[case] expected: Result) {
- let ident = Ident::new("_toml_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(toml.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::invalid_toml("name = ")]
- fn test_toml_parse_error(#[case] input: &str) {
- let ident = Ident::new("_toml_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(input.to_string()))].into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::invalid_type(RuntimeValue::Number(1.into()))]
- fn test_toml_parse_invalid_type(#[case] input: RuntimeValue) {
- let ident = Ident::new("_toml_parse");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::simple_map(
- // {"name": "Alice", "age": 30}
- "omRuYW1lZUFsaWNlY2FnZRge",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())));
- map.insert(Ident::new("age"), RuntimeValue::Number(30.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- fn test_cbor_parse(#[case] input: &str, #[case] expected: Result) {
- let ident = Ident::new("_cbor_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(input.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::invalid_base64("not-valid-base64!!!")]
- #[case::invalid_cbor("aGVsbG8=")]
- fn test_cbor_parse_error(#[case] input: &str) {
- let ident = Ident::new("_cbor_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(input.to_string()))].into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::invalid_type(RuntimeValue::Number(1.into()))]
- fn test_cbor_parse_invalid_type(#[case] input: RuntimeValue) {
- let ident = Ident::new("_cbor_parse");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- #[rstest]
- #[case::simple_map(
- // {"name": "Alice", "age": 30} encoded as CBOR then base64
- "omRuYW1lZUFsaWNlY2FnZRge",
- {
- let mut map = DictMap::default();
- map.insert(Ident::new("name"), RuntimeValue::String(Shared::new("Alice".to_string())));
- map.insert(Ident::new("age"), RuntimeValue::Number(30.into()));
- Ok(RuntimeValue::Dict(Shared::new(map)))
- }
- )]
- fn test_cbor_stringify_roundtrip(#[case] base64_input: &str, #[case] expected: Result) {
- let env = VmEnv::default();
-
- // parse
- let ident_parse = Ident::new("_cbor_parse");
- let parsed = eval_builtin(
- &RuntimeValue::None,
- &ident_parse,
- vec![RuntimeValue::String(Shared::new(base64_input.to_string()))].into(),
- &env,
- );
- assert!(parsed.is_ok());
- assert_eq!(parsed.as_ref().ok(), expected.as_ref().ok());
-
- // stringify
- let ident_stringify = Ident::new("_cbor_stringify");
- let bytes_result = eval_builtin(
- &RuntimeValue::None,
- &ident_stringify,
- vec![parsed.unwrap()].into(),
- &env,
- );
- assert!(bytes_result.is_ok());
- assert!(matches!(bytes_result.unwrap(), RuntimeValue::Bytes(_)));
- }
-
- #[test]
- fn test_cbor_parse_from_bytes() {
- // {"name": "Alice"} as raw CBOR bytes
- let cbor_bytes = base64::engine::general_purpose::STANDARD
- .decode("oWRuYW1lZUFsaWNl")
- .unwrap();
- let ident = Ident::new("_cbor_parse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(cbor_bytes.into())].into(),
- &VmEnv::default(),
- );
- assert!(result.is_ok());
- let mut expected = DictMap::default();
- expected.insert(
- Ident::new("name"),
- RuntimeValue::String(Shared::new("Alice".to_string())),
- );
- assert_eq!(result.unwrap(), RuntimeValue::Dict(Shared::new(expected)));
- }
-
- #[test]
- fn test_base64_bytes_input() {
- let ident = Ident::new("base64");
- let bytes = vec![0x48u8, 0x65, 0x6c, 0x6c, 0x6f]; // "Hello"
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(bytes.into())].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::String(Shared::new("SGVsbG8=".to_string()))));
- }
-
- #[rstest]
- #[case::string(
- RuntimeValue::String(Shared::new("hello".to_string())),
- Ok(RuntimeValue::Bytes(Shared::new(vec![0x68, 0x65, 0x6c, 0x6c, 0x6f])))
- )]
- #[case::empty_string(
- RuntimeValue::String(Shared::new("".to_string())),
- Ok(RuntimeValue::Bytes(Shared::new(vec![])))
- )]
- #[case::utf8_string(
- RuntimeValue::String(Shared::new("ใ".to_string())),
- Ok(RuntimeValue::Bytes(Shared::new(vec![0xe3, 0x81, 0x82])))
- )]
- #[case::array_of_numbers(
- RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(0.into()),
- RuntimeValue::Number(255.into()),
- RuntimeValue::Number(128.into()),
- ])),
- Ok(RuntimeValue::Bytes(Shared::new(vec![0, 255, 128])))
- )]
- #[case::bytes_identity(
- RuntimeValue::Bytes(Shared::new(vec![1, 2, 3])),
- Ok(RuntimeValue::Bytes(Shared::new(vec![1, 2, 3])))
- )]
- fn test_to_bytes(#[case] input: RuntimeValue, #[case] expected: Result) {
- let ident = Ident::new("to_bytes");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default());
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::number(RuntimeValue::Number(42.into()))]
- #[case::array_with_non_number(RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("x".to_string()))])))]
- #[case::array_with_negative(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number((-1i64).into())])))]
- #[case::array_with_256(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(256i64.into())])))]
- #[case::array_with_fractional(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.5f64.into())])))]
- #[case::array_with_nan(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(f64::NAN.into())])))]
- #[case::array_with_infinity(RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(f64::INFINITY.into())])))]
- fn test_to_bytes_invalid(#[case] input: RuntimeValue) {
- let ident = Ident::new("to_bytes");
- let result = eval_builtin(&RuntimeValue::None, &ident, vec![input].into(), &VmEnv::default());
- assert!(result.is_err());
- }
-
- #[test]
- fn test_bytes_add() {
- let ident = Ident::new("add");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![1, 2])),
- RuntimeValue::Bytes(Shared::new(vec![3, 4])),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![1, 2, 3, 4]))));
- }
-
- #[test]
- fn test_bytes_reverse() {
- let ident = Ident::new("reverse");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(Shared::new(vec![1, 2, 3]))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![3, 2, 1]))));
- }
-
- #[test]
- fn test_bytes_slice() {
- let ident = Ident::new("slice");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![10, 20, 30, 40, 50])),
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(4.into()),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![20, 30, 40]))));
- }
-
- #[test]
- fn test_md5_bytes_input() {
- let ident = Ident::new("md5");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(
- result,
- Ok(RuntimeValue::String(Shared::new(
- "5d41402abc4b2a76b9719d911017c592".to_string()
- )))
- );
- }
-
- #[test]
- fn test_sha256_bytes_input() {
- let ident = Ident::new("sha256");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(
- result,
- Ok(RuntimeValue::String(Shared::new(
- "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824".to_string()
- )))
- );
- }
-
- #[test]
- fn test_sha512_string_input() {
- let ident = Ident::new("sha512");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new("hello".to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(
- result,
- Ok(RuntimeValue::String(Shared::new(
- "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043".to_string()
- )))
- );
- }
-
- #[test]
- fn test_sha512_bytes_input() {
- let ident = Ident::new("sha512");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(
- result,
- Ok(RuntimeValue::String(Shared::new(
- "9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043".to_string()
- )))
- );
- }
-
- #[rstest]
- #[case::lowercase("deadbeef", Ok(RuntimeValue::Bytes(Shared::new(vec![0xde, 0xad, 0xbe, 0xef]))))]
- #[case::uppercase("DEADBEEF", Ok(RuntimeValue::Bytes(Shared::new(vec![0xde, 0xad, 0xbe, 0xef]))))]
- #[case::empty("", Ok(RuntimeValue::Bytes(Shared::new(vec![]))))]
- fn test_from_hex(#[case] input: &str, #[case] expected: Result) {
- let ident = Ident::new("from_hex");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(input.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[rstest]
- #[case::odd_length("abc", true)]
- #[case::invalid_chars("zzzz", true)]
- fn test_from_hex_invalid(#[case] input: &str, #[case] is_err: bool) {
- let ident = Ident::new("from_hex");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::String(Shared::new(input.to_string()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result.is_err(), is_err);
- }
-
- #[rstest]
- #[case::basic(vec![0xde, 0xad, 0xbe, 0xef], Ok(RuntimeValue::String(Shared::new("deadbeef".to_string()))))]
- #[case::empty(vec![], Ok(RuntimeValue::String(Shared::new("".to_string()))))]
- #[case::zero_ff(vec![0x00, 0xff], Ok(RuntimeValue::String(Shared::new("00ff".to_string()))))]
- #[case::all_zeros(vec![0x00, 0x00], Ok(RuntimeValue::String(Shared::new("0000".to_string()))))]
- fn test_to_hex(#[case] input: Vec, #[case] expected: Result) {
- let ident = Ident::new("to_hex");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(input.into())].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[test]
- fn test_to_hex_roundtrip() {
- let env = VmEnv::default();
- let original = vec![0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef];
- let hex = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("to_hex"),
- vec![RuntimeValue::Bytes(Shared::new(original.clone()))].into(),
- &env,
- )
- .unwrap();
- let roundtripped = eval_builtin(&RuntimeValue::None, &Ident::new("from_hex"), vec![hex].into(), &env).unwrap();
- assert_eq!(roundtripped, RuntimeValue::Bytes(original.into()));
- }
-
- #[rstest]
- #[case::empty(vec![], Ok(RuntimeValue::String(Shared::new("00000000".to_string()))))]
- #[case::short(
- b"hi".to_vec(),
- Ok(RuntimeValue::String(Shared::new("00000000 68 69 |hi|".to_string())))
- )]
- fn test_hexdump(#[case] input: Vec, #[case] expected: Result) {
- let ident = Ident::new("hexdump");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(input.into())].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, expected);
- }
-
- #[test]
- fn test_hexdump_none() {
- let result = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("hexdump"),
- vec![RuntimeValue::None].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::NONE));
- }
-
- #[test]
- fn test_utf8_valid() {
- let ident = Ident::new("utf8");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(Shared::new(b"hello".to_vec()))].into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::String(Shared::new("hello".to_string()))));
- }
-
- #[test]
- fn test_utf8_invalid() {
- let ident = Ident::new("utf8");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![RuntimeValue::Bytes(Shared::new(vec![0xff, 0xfe]))].into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[test]
- fn test_decode_shift_jis() {
- let ident = Ident::new("decode");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![0x82, 0xa0])),
- RuntimeValue::String(Shared::new("shift_jis".to_string())),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::String(Shared::new("ใ".to_string()))));
- }
-
- #[test]
- fn test_decode_unknown_label() {
- let ident = Ident::new("decode");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![0x00])),
- RuntimeValue::String(Shared::new("not-a-real-encoding".to_string())),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[test]
- fn test_decode_invalid_bytes() {
- let ident = Ident::new("decode");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![0xff, 0xff])),
- RuntimeValue::String(Shared::new("shift_jis".to_string())),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[test]
- fn test_encode_shift_jis() {
- let ident = Ident::new("encode");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::String(Shared::new("ใ".to_string())),
- RuntimeValue::String(Shared::new("shift_jis".to_string())),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![0x82, 0xa0]))));
- }
-
- #[test]
- fn test_encode_decode_roundtrip() {
- let env = VmEnv::default();
- let encoded = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("encode"),
- vec![
- RuntimeValue::String(Shared::new("ใใใซใกใฏ".to_string())),
- RuntimeValue::String(Shared::new("euc-jp".to_string())),
- ]
- .into(),
- &env,
- )
- .unwrap();
- let decoded = eval_builtin(
- &RuntimeValue::None,
- &Ident::new("decode"),
- vec![encoded, RuntimeValue::String(Shared::new("euc-jp".to_string()))].into(),
- &env,
- );
- assert_eq!(decoded, Ok(RuntimeValue::String(Shared::new("ใใใซใกใฏ".to_string()))));
- }
-
- #[test]
- fn test_encode_unmappable_character() {
- let ident = Ident::new("encode");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::String(Shared::new("๐".to_string())),
- RuntimeValue::String(Shared::new("shift_jis".to_string())),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[test]
- fn test_xor_basic() {
- let ident = Ident::new("xor");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![0xaa, 0xbb])),
- RuntimeValue::Bytes(Shared::new(vec![0x55, 0x44])),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![0xff, 0xff]))));
- }
-
- #[test]
- fn test_xor_identity() {
- let ident = Ident::new("xor");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![0x01, 0x02, 0x03])),
- RuntimeValue::Bytes(Shared::new(vec![0x00, 0x00, 0x00])),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert_eq!(result, Ok(RuntimeValue::Bytes(Shared::new(vec![0x01, 0x02, 0x03]))));
- }
-
- #[test]
- fn test_xor_length_mismatch() {
- let ident = Ident::new("xor");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Bytes(Shared::new(vec![0x01, 0x02])),
- RuntimeValue::Bytes(Shared::new(vec![0x01])),
- ]
- .into(),
- &VmEnv::default(),
- );
- assert!(result.is_err());
- }
-
- #[test]
- fn test_diff_strings() {
- let ident = Ident::new("_diff");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::String(Shared::new("abc".into())),
- RuntimeValue::String(Shared::new("abc ".into())),
- ]
- .into(),
- &VmEnv::default(),
- );
-
- assert!(result.is_ok());
- if let Ok(RuntimeValue::Array(changes)) = result {
- // line-level diff: delete "abc" + insert "abc " (replace pair)
- assert_eq!(changes.len(), 2);
- if let RuntimeValue::Dict(ref m) = changes[0] {
- assert_eq!(
- m.get(&Ident::new("tag")),
- Some(&RuntimeValue::String(Shared::new("delete".into())))
- );
- assert_eq!(
- m.get(&Ident::new("value")),
- Some(&RuntimeValue::String(Shared::new("abc".into())))
- );
- assert!(m.contains_key(&Ident::new("inline")));
- } else {
- panic!("Expected Dict change");
- }
- if let RuntimeValue::Dict(ref m) = changes[1] {
- assert_eq!(
- m.get(&Ident::new("tag")),
- Some(&RuntimeValue::String(Shared::new("insert".into())))
- );
- assert_eq!(
- m.get(&Ident::new("value")),
- Some(&RuntimeValue::String(Shared::new("abc ".into())))
- );
- // inline should show the trailing space as "insert"
- if let Some(RuntimeValue::Array(inline)) = m.get(&Ident::new("inline")) {
- let last = inline.last().expect("inline should not be empty");
- if let RuntimeValue::Dict(lm) = last {
- assert_eq!(
- lm.get(&Ident::new("tag")),
- Some(&RuntimeValue::String(Shared::new("insert".into())))
- );
- assert_eq!(
- lm.get(&Ident::new("value")),
- Some(&RuntimeValue::String(Shared::new(" ".into())))
- );
- } else {
- panic!("Expected Dict in inline");
- }
- } else {
- panic!("Expected inline Array");
- }
- } else {
- panic!("Expected Dict change");
- }
- } else {
- panic!("Expected Array result");
- }
- }
-
- #[test]
- fn test_diff_arrays() {
- let ident = Ident::new("_diff");
- let result = eval_builtin(
- &RuntimeValue::None,
- &ident,
- vec![
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into())])),
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(2.into())])),
- ]
- .into(),
- &VmEnv::default(),
- );
-
- assert!(result.is_ok());
- if let Ok(RuntimeValue::Array(changes)) = result {
- assert_eq!(changes.len(), 2); // delete 1, insert 2
- if let RuntimeValue::Dict(ref m) = changes[0] {
- assert_eq!(
- m.get(&Ident::new("tag")),
- Some(&RuntimeValue::String(Shared::new("delete".into())))
- );
- assert_eq!(m.get(&Ident::new("value")), Some(&RuntimeValue::Number(1.into())));
- // non-string elements have no inline field
- assert!(!m.contains_key(&Ident::new("inline")));
- } else {
- panic!("Expected Dict change");
- }
- if let RuntimeValue::Dict(ref m) = changes[1] {
- assert_eq!(
- m.get(&Ident::new("tag")),
- Some(&RuntimeValue::String(Shared::new("insert".into())))
- );
- assert_eq!(m.get(&Ident::new("value")), Some(&RuntimeValue::Number(2.into())));
- assert!(!m.contains_key(&Ident::new("inline")));
- } else {
- panic!("Expected Dict change");
- }
- } else {
- panic!("Expected Array result");
- }
- }
-
- #[rstest]
- #[case::single_number(vec![RuntimeValue::Number(1.into())], vec![1u8])]
- #[case::multiple_numbers(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())], vec![1u8, 2u8])]
- #[case::number_array(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]))], vec![1u8, 2u8])]
- #[case::empty(vec![], vec![])]
- #[case::ignores_strings(vec![RuntimeValue::String(Shared::new("x".into()))], vec![])]
- fn test_collect_depth_values(#[case] args: Vec, #[case] expected: Vec) {
- assert_eq!(collect_depth_values(&args), expected);
- }
-
- #[rstest]
- #[case::single_string(vec![RuntimeValue::String(Shared::new("rust".into()))], vec!["rust".to_string()])]
- #[case::multiple_strings(vec![RuntimeValue::String(Shared::new("rust".into())), RuntimeValue::String(Shared::new("go".into()))], vec!["rust".to_string(), "go".to_string()])]
- #[case::string_array(vec![RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("rust".into())), RuntimeValue::String(Shared::new("go".into()))]))], vec!["rust".to_string(), "go".to_string()])]
- #[case::empty(vec![], vec![])]
- #[case::ignores_numbers(vec![RuntimeValue::Number(1.into())], vec![])]
- fn test_collect_string_values(#[case] args: Vec, #[case] expected: Vec) {
- assert_eq!(collect_string_values(&args), expected);
- }
-
- #[rstest]
- #[case::heading_depth_match(
- Node::Heading(mq_markdown::Heading { depth: mq_markdown::HeadingDepth::H1, values: vec![], position: None }),
- Selector::Heading(None),
- vec![RuntimeValue::Number(1.into())],
- true
- )]
- #[case::heading_depth_no_match(
- Node::Heading(mq_markdown::Heading { depth: mq_markdown::HeadingDepth::H2, values: vec![], position: None }),
- Selector::Heading(None),
- vec![RuntimeValue::Number(1.into())],
- false
- )]
- #[case::heading_multi_depth_match(
- Node::Heading(mq_markdown::Heading { depth: mq_markdown::HeadingDepth::H2, values: vec![], position: None }),
- Selector::Heading(None),
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())],
- true
- )]
- #[case::heading_no_args_fallback(
- Node::Heading(mq_markdown::Heading { depth: mq_markdown::HeadingDepth::H1, values: vec![], position: None }),
- Selector::Heading(None),
- vec![],
- true
- )]
- #[case::code_lang_match(
- Node::Code(mq_markdown::Code { lang: Some("rust".to_string()), meta: None, value: "fn main() {}".to_string(), fence: true, position: None }),
- Selector::Code,
- vec![RuntimeValue::String(Shared::new("rust".into()))],
- true
- )]
- #[case::code_lang_no_match(
- Node::Code(mq_markdown::Code { lang: Some("python".to_string()), meta: None, value: "pass".to_string(), fence: true, position: None }),
- Selector::Code,
- vec![RuntimeValue::String(Shared::new("rust".into()))],
- false
- )]
- #[case::code_no_args_fallback(
- Node::Code(mq_markdown::Code { lang: None, meta: None, value: "".to_string(), fence: true, position: None }),
- Selector::Code,
- vec![],
- true
- )]
- #[case::callout_kind_match(
- Node::Callout(mq_markdown::Callout { fold: None, kind: "NOTE".to_string(), title: None, values: vec![], position: None }),
- Selector::Callout,
- vec![RuntimeValue::String(Shared::new("NOTE".into()))],
- true
- )]
- #[case::callout_kind_no_match(
- Node::Callout(mq_markdown::Callout { fold: None, kind: "WARNING".to_string(), title: None, values: vec![], position: None }),
- Selector::Callout,
- vec![RuntimeValue::String(Shared::new("NOTE".into()))],
- false
- )]
- #[case::callout_multi_kind_match(
- Node::Callout(mq_markdown::Callout { fold: None, kind: "TIP".to_string(), title: None, values: vec![], position: None }),
- Selector::Callout,
- vec![RuntimeValue::String(Shared::new("NOTE".into())), RuntimeValue::String(Shared::new("TIP".into()))],
- true
- )]
- #[case::callout_no_args_fallback(
- Node::Callout(mq_markdown::Callout { fold: None, kind: "NOTE".to_string(), title: None, values: vec![], position: None }),
- Selector::Callout,
- vec![],
- true
- )]
- #[case::callout_non_callout_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::Callout,
- vec![RuntimeValue::String(Shared::new("NOTE".into()))],
- false
- )]
- #[case::wikilink_target_match(
- Node::WikiLink(mq_markdown::WikiLink { target: "Some Page".to_string(), text: None, position: None }),
- Selector::WikiLink,
- vec![RuntimeValue::String(Shared::new("Some Page".into()))],
- true
- )]
- #[case::wikilink_target_no_match(
- Node::WikiLink(mq_markdown::WikiLink { target: "Other Page".to_string(), text: None, position: None }),
- Selector::WikiLink,
- vec![RuntimeValue::String(Shared::new("Some Page".into()))],
- false
- )]
- #[case::wikilink_multi_target_match(
- Node::WikiLink(mq_markdown::WikiLink { target: "Other Page".to_string(), text: None, position: None }),
- Selector::WikiLink,
- vec![RuntimeValue::String(Shared::new("Some Page".into())), RuntimeValue::String(Shared::new("Other Page".into()))],
- true
- )]
- #[case::wikilink_no_args_fallback(
- Node::WikiLink(mq_markdown::WikiLink { target: "Some Page".to_string(), text: None, position: None }),
- Selector::WikiLink,
- vec![],
- true
- )]
- #[case::wikilink_non_wikilink_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::WikiLink,
- vec![RuntimeValue::String(Shared::new("Some Page".into()))],
- false
- )]
- #[case::embed_target_match(
- Node::Embed(mq_markdown::Embed { target: "image.png".to_string(), display: None, position: None }),
- Selector::Embed,
- vec![RuntimeValue::String(Shared::new("image.png".into()))],
- true
- )]
- #[case::embed_target_no_match(
- Node::Embed(mq_markdown::Embed { target: "note.md".to_string(), display: None, position: None }),
- Selector::Embed,
- vec![RuntimeValue::String(Shared::new("image.png".into()))],
- false
- )]
- #[case::embed_multi_target_match(
- Node::Embed(mq_markdown::Embed { target: "note.md".to_string(), display: None, position: None }),
- Selector::Embed,
- vec![RuntimeValue::String(Shared::new("image.png".into())), RuntimeValue::String(Shared::new("note.md".into()))],
- true
- )]
- #[case::embed_no_args_fallback(
- Node::Embed(mq_markdown::Embed { target: "image.png".to_string(), display: None, position: None }),
- Selector::Embed,
- vec![],
- true
- )]
- #[case::embed_non_embed_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::Embed,
- vec![RuntimeValue::String(Shared::new("image.png".into()))],
- false
- )]
- #[case::link_ref_ident_match(
- Node::LinkRef(mq_markdown::LinkRef { ident: "ref".to_string(), label: None, values: vec![], position: None }),
- Selector::LinkRef,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- true
- )]
- #[case::link_ref_ident_no_match(
- Node::LinkRef(mq_markdown::LinkRef { ident: "other".to_string(), label: None, values: vec![], position: None }),
- Selector::LinkRef,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- false
- )]
- #[case::link_ref_multi_ident_match(
- Node::LinkRef(mq_markdown::LinkRef { ident: "other".to_string(), label: None, values: vec![], position: None }),
- Selector::LinkRef,
- vec![RuntimeValue::String(Shared::new("ref".into())), RuntimeValue::String(Shared::new("other".into()))],
- true
- )]
- #[case::link_ref_no_args_fallback(
- Node::LinkRef(mq_markdown::LinkRef { ident: "ref".to_string(), label: None, values: vec![], position: None }),
- Selector::LinkRef,
- vec![],
- true
- )]
- #[case::link_ref_non_link_ref_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::LinkRef,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- false
- )]
- #[case::image_ref_ident_match(
- Node::ImageRef(mq_markdown::ImageRef { alt: "".to_string(), ident: "ref".to_string(), label: None, position: None }),
- Selector::ImageRef,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- true
- )]
- #[case::image_ref_ident_no_match(
- Node::ImageRef(mq_markdown::ImageRef { alt: "".to_string(), ident: "other".to_string(), label: None, position: None }),
- Selector::ImageRef,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- false
- )]
- #[case::image_ref_multi_ident_match(
- Node::ImageRef(mq_markdown::ImageRef { alt: "".to_string(), ident: "other".to_string(), label: None, position: None }),
- Selector::ImageRef,
- vec![RuntimeValue::String(Shared::new("ref".into())), RuntimeValue::String(Shared::new("other".into()))],
- true
- )]
- #[case::image_ref_no_args_fallback(
- Node::ImageRef(mq_markdown::ImageRef { alt: "".to_string(), ident: "ref".to_string(), label: None, position: None }),
- Selector::ImageRef,
- vec![],
- true
- )]
- #[case::image_ref_non_image_ref_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::ImageRef,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- false
- )]
- #[case::footnote_ref_ident_match(
- Node::FootnoteRef(mq_markdown::FootnoteRef { ident: "1".to_string(), label: None, position: None }),
- Selector::FootnoteRef,
- vec![RuntimeValue::String(Shared::new("1".into()))],
- true
- )]
- #[case::footnote_ref_ident_no_match(
- Node::FootnoteRef(mq_markdown::FootnoteRef { ident: "2".to_string(), label: None, position: None }),
- Selector::FootnoteRef,
- vec![RuntimeValue::String(Shared::new("1".into()))],
- false
- )]
- #[case::footnote_ref_multi_ident_match(
- Node::FootnoteRef(mq_markdown::FootnoteRef { ident: "2".to_string(), label: None, position: None }),
- Selector::FootnoteRef,
- vec![RuntimeValue::String(Shared::new("1".into())), RuntimeValue::String(Shared::new("2".into()))],
- true
- )]
- #[case::footnote_ref_no_args_fallback(
- Node::FootnoteRef(mq_markdown::FootnoteRef { ident: "1".to_string(), label: None, position: None }),
- Selector::FootnoteRef,
- vec![],
- true
- )]
- #[case::footnote_ref_non_footnote_ref_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::FootnoteRef,
- vec![RuntimeValue::String(Shared::new("1".into()))],
- false
- )]
- #[case::footnote_ident_match(
- Node::Footnote(mq_markdown::Footnote { ident: "1".to_string(), values: vec![], position: None }),
- Selector::Footnote,
- vec![RuntimeValue::String(Shared::new("1".into()))],
- true
- )]
- #[case::footnote_ident_no_match(
- Node::Footnote(mq_markdown::Footnote { ident: "2".to_string(), values: vec![], position: None }),
- Selector::Footnote,
- vec![RuntimeValue::String(Shared::new("1".into()))],
- false
- )]
- #[case::footnote_multi_ident_match(
- Node::Footnote(mq_markdown::Footnote { ident: "2".to_string(), values: vec![], position: None }),
- Selector::Footnote,
- vec![RuntimeValue::String(Shared::new("1".into())), RuntimeValue::String(Shared::new("2".into()))],
- true
- )]
- #[case::footnote_no_args_fallback(
- Node::Footnote(mq_markdown::Footnote { ident: "1".to_string(), values: vec![], position: None }),
- Selector::Footnote,
- vec![],
- true
- )]
- #[case::footnote_non_footnote_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::Footnote,
- vec![RuntimeValue::String(Shared::new("1".into()))],
- false
- )]
- #[case::definition_ident_match(
- Node::Definition(mq_markdown::Definition { position: None, url: mq_markdown::Url::new("https://example.com".to_string()), title: None, ident: "ref".to_string(), label: None }),
- Selector::Definition,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- true
- )]
- #[case::definition_ident_no_match(
- Node::Definition(mq_markdown::Definition { position: None, url: mq_markdown::Url::new("https://example.com".to_string()), title: None, ident: "other".to_string(), label: None }),
- Selector::Definition,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- false
- )]
- #[case::definition_multi_ident_match(
- Node::Definition(mq_markdown::Definition { position: None, url: mq_markdown::Url::new("https://example.com".to_string()), title: None, ident: "other".to_string(), label: None }),
- Selector::Definition,
- vec![RuntimeValue::String(Shared::new("ref".into())), RuntimeValue::String(Shared::new("other".into()))],
- true
- )]
- #[case::definition_no_args_fallback(
- Node::Definition(mq_markdown::Definition { position: None, url: mq_markdown::Url::new("https://example.com".to_string()), title: None, ident: "ref".to_string(), label: None }),
- Selector::Definition,
- vec![],
- true
- )]
- #[case::definition_non_definition_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::Definition,
- vec![RuntimeValue::String(Shared::new("ref".into()))],
- false
- )]
- #[case::mdx_jsx_flow_element_name_match(
- Node::MdxJsxFlowElement(mq_markdown::MdxJsxFlowElement { name: Some("Alert".to_string()), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxFlowElement,
- vec![RuntimeValue::String(Shared::new("Alert".into()))],
- true
- )]
- #[case::mdx_jsx_flow_element_name_no_match(
- Node::MdxJsxFlowElement(mq_markdown::MdxJsxFlowElement { name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxFlowElement,
- vec![RuntimeValue::String(Shared::new("Alert".into()))],
- false
- )]
- #[case::mdx_jsx_flow_element_multi_name_match(
- Node::MdxJsxFlowElement(mq_markdown::MdxJsxFlowElement { name: Some("div".to_string()), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxFlowElement,
- vec![RuntimeValue::String(Shared::new("Alert".into())), RuntimeValue::String(Shared::new("div".into()))],
- true
- )]
- #[case::mdx_jsx_flow_element_no_args_fallback(
- Node::MdxJsxFlowElement(mq_markdown::MdxJsxFlowElement { name: Some("Alert".to_string()), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxFlowElement,
- vec![],
- true
- )]
- #[case::mdx_jsx_flow_element_non_matching_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::MdxJsxFlowElement,
- vec![RuntimeValue::String(Shared::new("Alert".into()))],
- false
- )]
- #[case::mdx_jsx_text_element_name_match(
- Node::MdxJsxTextElement(mq_markdown::MdxJsxTextElement { name: Some(SmolStr::new("Alert")), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxTextElement,
- vec![RuntimeValue::String(Shared::new("Alert".into()))],
- true
- )]
- #[case::mdx_jsx_text_element_name_no_match(
- Node::MdxJsxTextElement(mq_markdown::MdxJsxTextElement { name: Some(SmolStr::new("span")), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxTextElement,
- vec![RuntimeValue::String(Shared::new("Alert".into()))],
- false
- )]
- #[case::mdx_jsx_text_element_multi_name_match(
- Node::MdxJsxTextElement(mq_markdown::MdxJsxTextElement { name: Some(SmolStr::new("span")), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxTextElement,
- vec![RuntimeValue::String(Shared::new("Alert".into())), RuntimeValue::String(Shared::new("span".into()))],
- true
- )]
- #[case::mdx_jsx_text_element_no_args_fallback(
- Node::MdxJsxTextElement(mq_markdown::MdxJsxTextElement { name: Some(SmolStr::new("Alert")), attributes: Vec::new(), children: Vec::new(), position: None }),
- Selector::MdxJsxTextElement,
- vec![],
- true
- )]
- #[case::mdx_jsx_text_element_non_matching_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::MdxJsxTextElement,
- vec![RuntimeValue::String(Shared::new("Alert".into()))],
- false
- )]
- #[case::non_heading_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::Heading(None),
- vec![RuntimeValue::Number(1.into())],
- false
- )]
- #[case::list_index_match(
- Node::List(mq_markdown::List { marker: None, index: 2, level: 0, checked: None, ordered: false, start: None, spread: false, values: vec![], position: None }),
- Selector::List(None, None),
- vec![RuntimeValue::Number(2.into())],
- true
- )]
- #[case::list_index_no_match(
- Node::List(mq_markdown::List { marker: None, index: 0, level: 0, checked: None, ordered: false, start: None, spread: false, values: vec![], position: None }),
- Selector::List(None, None),
- vec![RuntimeValue::Number(1.into())],
- false
- )]
- #[case::list_multi_index_match(
- Node::List(mq_markdown::List { marker: None, index: 3, level: 0, checked: None, ordered: false, start: None, spread: false, values: vec![], position: None }),
- Selector::List(None, None),
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(3.into())],
- true
- )]
- #[case::list_negative_index_no_match(
- Node::List(mq_markdown::List { marker: None, index: 0, level: 0, checked: None, ordered: false, start: None, spread: false, values: vec![], position: None }),
- Selector::List(None, None),
- vec![RuntimeValue::Number((-1).into())],
- false
- )]
- #[case::list_fractional_index_no_match(
- Node::List(mq_markdown::List { marker: None, index: 1, level: 0, checked: None, ordered: false, start: None, spread: false, values: vec![], position: None }),
- Selector::List(None, None),
- vec![RuntimeValue::Number(1.5.into())],
- false
- )]
- #[case::table_negative_row_no_match(
- Node::TableCell(mq_markdown::TableCell { column: 0, row: 0, values: vec![], position: None }),
- Selector::Table(None, None),
- vec![RuntimeValue::Number((-1).into())],
- false
- )]
- #[case::list_no_args_fallback(
- Node::List(mq_markdown::List { marker: None, index: 0, level: 0, checked: None, ordered: false, start: None, spread: false, values: vec![], position: None }),
- Selector::List(None, None),
- vec![],
- true
- )]
- #[case::list_non_list_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::List(None, None),
- vec![RuntimeValue::Number(0.into())],
- false
- )]
- #[case::table_row_match(
- Node::TableCell(mq_markdown::TableCell { column: 0, row: 1, values: vec![], position: None }),
- Selector::Table(None, None),
- vec![RuntimeValue::Number(1.into())],
- true
- )]
- #[case::table_row_no_match(
- Node::TableCell(mq_markdown::TableCell { column: 0, row: 0, values: vec![], position: None }),
- Selector::Table(None, None),
- vec![RuntimeValue::Number(1.into())],
- false
- )]
- #[case::table_row_and_col_match(
- Node::TableCell(mq_markdown::TableCell { column: 2, row: 1, values: vec![], position: None }),
- Selector::Table(None, None),
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())],
- true
- )]
- #[case::table_row_and_col_no_match(
- Node::TableCell(mq_markdown::TableCell { column: 0, row: 1, values: vec![], position: None }),
- Selector::Table(None, None),
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())],
- false
- )]
- #[case::table_no_args_fallback(
- Node::TableCell(mq_markdown::TableCell { column: 0, row: 0, values: vec![], position: None }),
- Selector::Table(None, None),
- vec![],
- true
- )]
- #[case::table_non_table_node(
- Node::HorizontalRule(mq_markdown::HorizontalRule { marker: None, position: None }),
- Selector::Table(None, None),
- vec![RuntimeValue::Number(0.into())],
- false
- )]
- fn test_eval_selector_with_args(
- #[case] node: Node,
- #[case] selector: Selector,
- #[case] args: Vec,
- #[case] expected_match: bool,
- ) {
- let result = eval_selector_with_args(&node, &selector, &args);
- assert_eq!(!result.is_none(), expected_match);
- }
-
- fn env() -> VmEnv {
- VmEnv::default()
- }
-
- fn call(name: &str, args: Vec) -> Result {
- eval_builtin(&RuntimeValue::None, &Ident::new(name), args.into(), &env())
- }
-
- #[rstest]
- #[case::get("get", "key_only_looked_up_by_get")]
- #[case::has("has", "key_only_looked_up_by_has")]
- #[case::del("del", "key_only_looked_up_by_del")]
- fn test_dict_lookup_does_not_intern_missing_key(#[case] name: &str, #[case] key: &str) {
- let dict = RuntimeValue::Dict(Shared::new(DictMap::from_iter([(
- Ident::new("present"),
- RuntimeValue::Number(1.into()),
- )])));
-
- let result = call(name, vec![dict.clone(), key.into()]).unwrap();
-
- assert!(Ident::lookup(key).is_none());
- match name {
- "get" => assert_eq!(result, RuntimeValue::NONE),
- "has" => assert_eq!(result, RuntimeValue::Boolean(false)),
- _ => assert_eq!(result, dict),
- }
- }
-
- // =========================================================================
- // band
- // =========================================================================
-
- #[rstest]
- #[case(vec![0xff, 0xff], vec![0xff, 0xff], vec![0xff, 0xff])]
- #[case(vec![0xf0, 0x0f], vec![0xff, 0xff], vec![0xf0, 0x0f])]
- #[case(vec![0xaa, 0x55], vec![0x55, 0xaa], vec![0x00, 0x00])]
- #[case(vec![0xff], vec![0x00], vec![0x00])]
- #[case(vec![], vec![], vec![])]
- fn test_band(#[case] b1: Vec, #[case] b2: Vec, #[case] expected: Vec) {
- assert_eq!(
- call(
- "band",
- vec![RuntimeValue::Bytes(b1.into()), RuntimeValue::Bytes(b2.into())]
- ),
- Ok(RuntimeValue::Bytes(expected.into()))
- );
- }
-
- #[rstest]
- #[case(vec![0x01, 0x02], vec![0x01])]
- #[case(vec![], vec![0x00])]
- fn test_band_length_mismatch(#[case] b1: Vec, #[case] b2: Vec) {
- assert!(
- call(
- "band",
- vec![RuntimeValue::Bytes(b1.into()), RuntimeValue::Bytes(b2.into())]
- )
- .is_err()
- );
- }
-
- #[test]
- fn test_band_type_error() {
- assert!(
- call(
- "band",
- vec![
- RuntimeValue::String(Shared::new("a".into())),
- RuntimeValue::Bytes(Shared::new(vec![0x01]))
- ]
- )
- .is_err()
- );
- }
-
- // =========================================================================
- // bor
- // =========================================================================
-
- #[rstest]
- #[case(vec![0x00, 0x00], vec![0x00, 0x00], vec![0x00, 0x00])]
- #[case(vec![0xf0, 0x00], vec![0x0f, 0x00], vec![0xff, 0x00])]
- #[case(vec![0xaa, 0x55], vec![0x55, 0xaa], vec![0xff, 0xff])]
- #[case(vec![0x00], vec![0xff], vec![0xff])]
- #[case(vec![], vec![], vec![])]
- fn test_bor(#[case] b1: Vec, #[case] b2: Vec, #[case] expected: Vec) {
- assert_eq!(
- call(
- "bor",
- vec![RuntimeValue::Bytes(b1.into()), RuntimeValue::Bytes(b2.into())]
- ),
- Ok(RuntimeValue::Bytes(expected.into()))
- );
- }
-
- #[rstest]
- #[case(vec![0x01, 0x02], vec![0x01])]
- #[case(vec![], vec![0x00])]
- fn test_bor_length_mismatch(#[case] b1: Vec, #[case] b2: Vec) {
- assert!(
- call(
- "bor",
- vec![RuntimeValue::Bytes(b1.into()), RuntimeValue::Bytes(b2.into())]
- )
- .is_err()
- );
- }
-
- #[test]
- fn test_bor_type_error() {
- assert!(
- call(
- "bor",
- vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Bytes(Shared::new(vec![0x01]))
- ]
- )
- .is_err()
- );
- }
-
- // =========================================================================
- // bnot
- // =========================================================================
-
- #[rstest]
- #[case(vec![0x00], vec![0xff])]
- #[case(vec![0xff], vec![0x00])]
- #[case(vec![0xf0, 0x0f], vec![0x0f, 0xf0])]
- #[case(vec![0x55, 0xaa], vec![0xaa, 0x55])]
- #[case(vec![], vec![])]
- fn test_bnot(#[case] input: Vec, #[case] expected: Vec) {
- assert_eq!(
- call("bnot", vec![RuntimeValue::Bytes(input.into())]),
- Ok(RuntimeValue::Bytes(expected.into()))
- );
- }
-
- #[test]
- fn test_bnot_double_negation() {
- let original = vec![0xde, 0xad, 0xbe, 0xef];
- let once = call("bnot", vec![RuntimeValue::Bytes(Shared::new(original.clone()))]).unwrap();
- let twice = call("bnot", vec![once]).unwrap();
- assert_eq!(twice, RuntimeValue::Bytes(original.into()));
- }
-
- #[test]
- fn test_bnot_type_error() {
- assert!(call("bnot", vec![RuntimeValue::String(Shared::new("a".into()))]).is_err());
- }
-
- // =========================================================================
- // starts_with / ends_with for bytes
- // =========================================================================
-
- #[rstest]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02], true)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02, 0x03], true)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x02, 0x03], false)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02, 0x03, 0x04], false)]
- #[case(vec![0x01], vec![], true)]
- #[case(vec![], vec![], true)]
- fn test_bytes_starts_with(#[case] haystack: Vec, #[case] prefix: Vec, #[case] expected: bool) {
- assert_eq!(
- call(
- "starts_with",
- vec![RuntimeValue::Bytes(haystack.into()), RuntimeValue::Bytes(prefix.into())]
- ),
- Ok(RuntimeValue::Boolean(expected))
- );
- }
-
- #[rstest]
- #[case(vec![0x01, 0x02, 0x03], vec![0x02, 0x03], true)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02, 0x03], true)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02], false)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x00, 0x01, 0x02, 0x03], false)]
- #[case(vec![0x01], vec![], true)]
- #[case(vec![], vec![], true)]
- fn test_bytes_ends_with(#[case] haystack: Vec, #[case] suffix: Vec, #[case] expected: bool) {
- assert_eq!(
- call(
- "ends_with",
- vec![RuntimeValue::Bytes(haystack.into()), RuntimeValue::Bytes(suffix.into())]
- ),
- Ok(RuntimeValue::Boolean(expected))
- );
- }
-
- // =========================================================================
- // index / rindex for bytes
- // =========================================================================
-
- #[rstest]
- #[case(vec![0x01, 0x02, 0x03, 0x02], vec![0x02], 1)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x04], -1)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02], 0)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x02, 0x03], 1)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02, 0x03], 0)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02, 0x03, 0x04], -1)]
- #[case(vec![], vec![0x01], -1)]
- fn test_bytes_index(#[case] haystack: Vec, #[case] needle: Vec, #[case] expected: i64) {
- assert_eq!(
- call(
- "index",
- vec![RuntimeValue::Bytes(haystack.into()), RuntimeValue::Bytes(needle.into())]
- ),
- Ok(RuntimeValue::Number(expected.into()))
- );
- }
-
- #[rstest]
- #[case(vec![0x01, 0x02, 0x03, 0x02], vec![0x02], 3)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x04], -1)]
- #[case(vec![0x01, 0x02, 0x03, 0x01, 0x02], vec![0x01, 0x02], 3)]
- #[case(vec![0x01, 0x02, 0x03], vec![0x01, 0x02, 0x03], 0)]
- #[case(vec![], vec![0x01], -1)]
- fn test_bytes_rindex(#[case] haystack: Vec, #[case] needle: Vec, #[case] expected: i64) {
- assert_eq!(
- call(
- "rindex",
- vec![RuntimeValue::Bytes(haystack.into()), RuntimeValue::Bytes(needle.into())]
- ),
- Ok(RuntimeValue::Number(expected.into()))
- );
- }
-
- #[test]
- fn test_bytes_index_rindex_agree_single_occurrence() {
- let h = vec![0xaa, 0xbb, 0xcc];
- let n = vec![0xbb];
- let idx = call(
- "index",
- vec![
- RuntimeValue::Bytes(Shared::new(h.clone())),
- RuntimeValue::Bytes(Shared::new(n.clone())),
- ],
- )
- .unwrap();
- let ridx = call(
- "rindex",
- vec![RuntimeValue::Bytes(h.into()), RuntimeValue::Bytes(n.into())],
- )
- .unwrap();
- assert_eq!(idx, ridx);
- }
-
- // =========================================================================
- // indices
- // =========================================================================
-
- #[rstest]
- #[case::string_multiple("hello world hello", "hello", vec![0, 12])]
- #[case::string_overlap_not_double_counted("aaaa", "aa", vec![0, 2])]
- #[case::string_not_found("hello", "xyz", vec![])]
- #[case::string_empty_needle("hello", "", vec![])]
- fn test_string_indices(#[case] haystack: &str, #[case] needle: &str, #[case] expected: Vec) {
- assert_eq!(
- call(
- "indices",
- vec![
- RuntimeValue::String(Shared::new(haystack.to_string())),
- RuntimeValue::String(Shared::new(needle.to_string()))
- ]
- ),
- Ok(RuntimeValue::Array(Shared::new(
- expected.into_iter().map(|i| RuntimeValue::Number(i.into())).collect()
- )))
- );
- }
-
- #[rstest]
- #[case(vec![0x01, 0x02, 0x03, 0x02], vec![0x02], vec![1, 3])]
- #[case(vec![0x01, 0x02, 0x03], vec![0x04], vec![])]
- #[case(vec![0x01, 0x02, 0x01, 0x02], vec![0x01, 0x02], vec![0, 2])]
- #[case(vec![], vec![0x01], vec![])]
- #[case(vec![0x01], vec![], vec![])]
- fn test_bytes_indices(#[case] haystack: Vec, #[case] needle: Vec, #[case] expected: Vec) {
- assert_eq!(
- call(
- "indices",
- vec![RuntimeValue::Bytes(haystack.into()), RuntimeValue::Bytes(needle.into())]
- ),
- Ok(RuntimeValue::Array(Shared::new(
- expected.into_iter().map(|i| RuntimeValue::Number(i.into())).collect()
- )))
- );
- }
-
- #[test]
- fn test_array_indices() {
- let array = RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::String(Shared::new("b".to_string())),
- RuntimeValue::String(Shared::new("a".to_string())),
- ]));
- assert_eq!(
- call(
- "indices",
- vec![array, RuntimeValue::String(Shared::new("a".to_string()))]
- ),
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Number(0.into()),
- RuntimeValue::Number(2.into()),
- ])))
- );
- }
-
- #[test]
- fn test_array_indices_not_found() {
- let array = RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("a".to_string()))]));
- assert_eq!(
- call(
- "indices",
- vec![array, RuntimeValue::String(Shared::new("z".to_string()))]
- ),
- Ok(RuntimeValue::Array(Shared::new(Vec::new())))
- );
- }
-
- // =========================================================================
- // repeat for bytes
- // =========================================================================
-
- #[rstest]
- #[case(vec![0x01, 0x02], 0, vec![])]
- #[case(vec![0x01, 0x02], 1, vec![0x01, 0x02])]
- #[case(vec![0x01, 0x02], 3, vec![0x01, 0x02, 0x01, 0x02, 0x01, 0x02])]
- #[case(vec![0xff], 4, vec![0xff, 0xff, 0xff, 0xff])]
- #[case(vec![], 5, vec![])]
- fn test_bytes_repeat(#[case] input: Vec, #[case] n: u32, #[case] expected: Vec) {
- assert_eq!(
- call(
- "repeat",
- vec![
- RuntimeValue::Bytes(input.into()),
- RuntimeValue::Number((n as f64).into())
- ]
- ),
- Ok(RuntimeValue::Bytes(expected.into()))
- );
- }
-
- // =========================================================================
- // pack
- // =========================================================================
-
- #[rstest]
- #[case("u8", 0.0, vec![0x00])]
- #[case("u8", 255.0, vec![0xff])]
- #[case("i8", -1.0, vec![0xff])]
- #[case("i8", -128.0, vec![0x80])]
- #[case("i8", 127.0, vec![0x7f])]
- #[case("u16be", 256.0, vec![0x01, 0x00])]
- #[case("u16le", 256.0, vec![0x00, 0x01])]
- #[case("i16be", -1.0, vec![0xff, 0xff])]
- #[case("i16le", -1.0, vec![0xff, 0xff])]
- #[case("u32be", 1.0, vec![0x00, 0x00, 0x00, 0x01])]
- #[case("u32le", 1.0, vec![0x01, 0x00, 0x00, 0x00])]
- #[case("i32be", -1.0, vec![0xff, 0xff, 0xff, 0xff])]
- #[case("i32le", -1.0, vec![0xff, 0xff, 0xff, 0xff])]
- #[case("u64be", 1.0, vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01])]
- #[case("u64le", 1.0, vec![0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])]
- #[case("i64be", -1.0, vec![0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff])]
- #[case("f32be", 1.0, vec![0x3f, 0x80, 0x00, 0x00])]
- #[case("f32le", 1.0, vec![0x00, 0x00, 0x80, 0x3f])]
- #[case("f64be", 1.0, vec![0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])]
- #[case("f64le", 1.0, vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f])]
- fn test_pack(#[case] fmt: &str, #[case] value: f64, #[case] expected: Vec) {
- assert_eq!(
- call(
- "pack",
- vec![
- RuntimeValue::String(Shared::new(fmt.into())),
- RuntimeValue::Number(value.into())
- ]
- ),
- Ok(RuntimeValue::Bytes(expected.into()))
- );
- }
-
- #[rstest]
- #[case("z99")]
- #[case("u16")]
- #[case("")]
- fn test_pack_unknown_format(#[case] fmt: &str) {
- assert!(
- call(
- "pack",
- vec![
- RuntimeValue::String(Shared::new(fmt.into())),
- RuntimeValue::Number(0.0.into())
- ]
- )
- .is_err()
- );
- }
-
- #[test]
- fn test_pack_type_error() {
- assert!(
- call(
- "pack",
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(0.0.into())]
- )
- .is_err()
- );
- }
-
- // =========================================================================
- // unpack
- // =========================================================================
-
- #[rstest]
- #[case("u8", vec![0x2a], 42.0)]
- #[case("i8", vec![0xff], -1.0)]
- #[case("u16be", vec![0x01, 0x00], 256.0)]
- #[case("u16le", vec![0x00, 0x01], 256.0)]
- #[case("i16be", vec![0xff, 0xff], -1.0)]
- #[case("i16le", vec![0xff, 0xff], -1.0)]
- #[case("u32be", vec![0x00, 0x00, 0x00, 0x01], 1.0)]
- #[case("u32le", vec![0x01, 0x00, 0x00, 0x00], 1.0)]
- #[case("i32be", vec![0xff, 0xff, 0xff, 0xff], -1.0)]
- #[case("i32le", vec![0xff, 0xff, 0xff, 0xff], -1.0)]
- #[case("u64be", vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01], 1.0)]
- #[case("u64le", vec![0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], 1.0)]
- #[case("i64be", vec![0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff], -1.0)]
- #[case("f32be", vec![0x3f, 0x80, 0x00, 0x00], 1.0)]
- #[case("f32le", vec![0x00, 0x00, 0x80, 0x3f], 1.0)]
- #[case("f64be", vec![0x3f, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], 1.0)]
- #[case("f64le", vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0x3f], 1.0)]
- fn test_unpack(#[case] fmt: &str, #[case] bytes: Vec, #[case] expected: f64) {
- assert_eq!(
- call(
- "unpack",
- vec![
- RuntimeValue::String(Shared::new(fmt.into())),
- RuntimeValue::Bytes(bytes.into())
- ]
- ),
- Ok(RuntimeValue::Number(expected.into()))
- );
- }
-
- #[rstest]
- #[case("u8", vec![])]
- #[case("u16be", vec![0x00])]
- #[case("u32be", vec![0x00, 0x00, 0x00])]
- #[case("u64be", vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])]
- #[case("f32be", vec![0x00, 0x00, 0x00])]
- #[case("f64be", vec![0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00])]
- fn test_unpack_too_short(#[case] fmt: &str, #[case] bytes: Vec) {
- assert!(
- call(
- "unpack",
- vec![
- RuntimeValue::String(Shared::new(fmt.into())),
- RuntimeValue::Bytes(bytes.into())
- ]
- )
- .is_err()
- );
- }
-
- #[rstest]
- #[case("z99")]
- #[case("")]
- fn test_unpack_unknown_format(#[case] fmt: &str) {
- assert!(
- call(
- "unpack",
- vec![
- RuntimeValue::String(Shared::new(fmt.into())),
- RuntimeValue::Bytes(Shared::new(vec![0x00]))
- ]
- )
- .is_err()
- );
- }
-
- #[test]
- fn test_unpack_type_error() {
- assert!(
- call(
- "unpack",
- vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Bytes(Shared::new(vec![0x00]))
- ]
- )
- .is_err()
- );
- }
-
- #[rstest]
- #[case("u8", 42.0)]
- #[case("i8", -5.0)]
- #[case("u16be", 1234.0)]
- #[case("u16le", 1234.0)]
- #[case("i16be", -1000.0)]
- #[case("i16le", -1000.0)]
- #[case("u32be", 100000.0)]
- #[case("u32le", 100000.0)]
- #[case("i32be", -100000.0)]
- #[case("i32le", -100000.0)]
- #[case("u64be", 1000000.0)]
- #[case("u64le", 1000000.0)]
- #[case("i64be", -1000000.0)]
- #[case("i64le", -1000000.0)]
- #[case("f32be", 1.5)]
- #[case("f32le", 1.5)]
- #[case("f64be", 1.23456789)]
- #[case("f64le", 1.23456789)]
- fn test_pack_unpack_roundtrip(#[case] fmt: &str, #[case] value: f64) {
- let packed = call(
- "pack",
- vec![
- RuntimeValue::String(Shared::new(fmt.into())),
- RuntimeValue::Number(value.into()),
- ],
- )
- .unwrap();
- let result = call("unpack", vec![RuntimeValue::String(Shared::new(fmt.into())), packed]).unwrap();
- match result {
- RuntimeValue::Number(n) => assert!((n.value() - value).abs() < 1e-5),
- _ => panic!("expected Number"),
- }
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_open_file_capability_gate_and_incremental_reads() {
- use std::io::Write;
-
- let mut tmp = tempfile::NamedTempFile::new().expect("failed to create temp file");
- tmp.write_all(b"one\r\ntwo\nthree").expect("failed to write");
- let path = RuntimeValue::String(Shared::new(tmp.path().to_string_lossy().to_string()));
- let string = |s: &str| RuntimeValue::String(Shared::new(s.to_string()));
-
- assert!(
- call("open_file", vec![path.clone()]).is_err(),
- "open_file should be blocked when read access is not allowed"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
- let handle = call("open_file", vec![path.clone()]).expect("open_file should succeed");
- assert_eq!(call("type", vec![handle.clone()]), Ok(string("file")));
-
- assert_eq!(call("read_line", vec![handle.clone()]), Ok(string("one")));
- assert_eq!(
- call("read_bytes", vec![handle.clone(), RuntimeValue::Number(2.into())]),
- Ok(RuntimeValue::Bytes(Shared::new(b"tw".to_vec())))
- );
- assert_eq!(call("read_line", vec![handle.clone()]), Ok(string("o")));
- assert_eq!(call("read_line", vec![handle.clone()]), Ok(string("three")));
- assert_eq!(call("read_line", vec![handle.clone()]), Ok(RuntimeValue::NONE));
- assert_eq!(
- call("read_bytes", vec![handle.clone(), RuntimeValue::Number(1.into())]),
- Ok(RuntimeValue::NONE)
- );
-
- assert_eq!(
- call("status", vec![handle.clone()]),
- Ok(RuntimeValue::Symbol(Ident::new("open")))
- );
- assert_eq!(call("close", vec![handle.clone()]), Ok(handle.clone()));
- assert_eq!(
- call("status", vec![handle.clone()]),
- Ok(RuntimeValue::Symbol(Ident::new("closed")))
- );
- assert!(call("read_line", vec![handle.clone()]).is_err());
-
- let open = call("open_file", vec![path.clone()]).unwrap();
- for size in [0.0, 1e-20, -1.0, 1.5, 1e300, (MAX_READ_BYTES + 1) as f64] {
- assert!(
- call("read_bytes", vec![open.clone(), RuntimeValue::Number(size.into())]).is_err(),
- "read_bytes should reject size {size}"
- );
- }
- assert!(call("read_line", vec![path.clone()]).is_err());
- assert!(call("open_file", vec![RuntimeValue::Number(42.into())]).is_err());
- assert!(
- call(
- "open_file",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_file.txt".into()
- ))]
- )
- .is_err(),
- "open_file should error for a nonexistent file"
- );
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_read_capability_gate_and_success() {
- use std::io::Write;
-
- let get = |entry: &RuntimeValue, key: &str| match entry {
- RuntimeValue::Dict(d) => d.get(&Ident::new(key)).cloned().unwrap_or(RuntimeValue::NONE),
- other => panic!("expected Dict, got {other:?}"),
- };
- let as_entries = |result: RuntimeValue| match result {
- RuntimeValue::Array(entries) => entries,
- other => panic!("expected Array, got {other:?}"),
- };
-
- let mut text_tmp = tempfile::NamedTempFile::new().expect("failed to create temp file");
- text_tmp.write_all(b"hello").expect("failed to write");
- let text_path = text_tmp.path().to_string_lossy().to_string();
-
- let mut bytes_tmp = tempfile::NamedTempFile::new().expect("failed to create temp file");
- bytes_tmp.write_all(&[0x89, 0x50, 0x4e, 0x47]).expect("failed to write");
- let bytes_path = bytes_tmp.path().to_string_lossy().to_string();
-
- let collection_dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(collection_dir.path().join("root.md"), "# Root\n").expect("failed to write");
-
- // No ambient Io guard installed yet: the default is all-denied.
- assert!(
- call("read_file", vec![RuntimeValue::String(Shared::new(text_path.clone()))]).is_err(),
- "read_file should be blocked when read access is not allowed"
- );
- assert!(
- call(
- "read_file_bytes",
- vec![RuntimeValue::String(Shared::new(bytes_path.clone()))]
- )
- .is_err(),
- "read_file_bytes should be blocked when read access is not allowed"
- );
- assert!(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- collection_dir.path().to_string_lossy().into_owned()
- ))],
- )
- .is_err(),
- "collection should be blocked when read access is not allowed"
- );
- assert!(
- call(
- "file_exists",
- vec![RuntimeValue::String(Shared::new(text_path.clone()))]
- )
- .is_err(),
- "file_exists should be blocked when read access is not allowed"
- );
- assert!(
- call("file_size", vec![RuntimeValue::String(Shared::new(text_path.clone()))]).is_err(),
- "file_size should be blocked when read access is not allowed"
- );
- assert!(
- call("file_info", vec![RuntimeValue::String(Shared::new(text_path.clone()))]).is_err(),
- "file_info should be blocked when read access is not allowed"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
- assert_eq!(
- call("read_file", vec![RuntimeValue::String(Shared::new(text_path.clone()))]),
- Ok(RuntimeValue::String(Shared::new("hello".to_string())))
- );
- assert_eq!(
- call(
- "read_file_bytes",
- vec![RuntimeValue::String(Shared::new(bytes_path.clone()))]
- ),
- Ok(RuntimeValue::Bytes(Shared::new(vec![0x89, 0x50, 0x4e, 0x47])))
- );
-
- let result = call(
- "read_file_bytes",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_file.png".into(),
- ))],
- );
- assert!(result.is_err(), "read_file_bytes should error for a nonexistent file");
-
- let result = call(
- "read_file",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_file.md".into(),
- ))],
- );
- assert!(result.is_err(), "read_file should error for a nonexistent file");
-
- assert_eq!(
- call(
- "file_exists",
- vec![RuntimeValue::String(Shared::new(text_path.clone()))]
- ),
- Ok(RuntimeValue::Boolean(true))
- );
- assert_eq!(
- call(
- "file_exists",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_file.md".into()
- ))]
- ),
- Ok(RuntimeValue::Boolean(false))
- );
- assert!(call("file_exists", vec![RuntimeValue::Number(42.into())]).is_err());
-
- assert_eq!(
- call("file_size", vec![RuntimeValue::String(Shared::new(text_path.clone()))]),
- Ok(RuntimeValue::Number(5.into()))
- );
- assert!(
- call(
- "file_size",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_file.md".into()
- ))]
- )
- .is_err(),
- "file_size should error for a nonexistent file"
- );
- assert!(call("file_size", vec![RuntimeValue::Number(42.into())]).is_err());
-
- match call("file_info", vec![RuntimeValue::String(Shared::new(text_path.clone()))])
- .expect("file_info should succeed")
- {
- RuntimeValue::Dict(d) => {
- assert_eq!(
- d.get(&Ident::new("path")),
- Some(&RuntimeValue::String(Shared::new(text_path.clone())))
- );
- assert_eq!(
- d.get(&Ident::new("kind")),
- Some(&RuntimeValue::String(Shared::new("file".to_string())))
- );
- assert_eq!(d.get(&Ident::new("size")), Some(&RuntimeValue::Number(5.into())));
- assert!(matches!(d.get(&Ident::new("modified")), Some(RuntimeValue::Number(_))));
- }
- other => panic!("expected Dict, got {other:?}"),
- }
- assert!(
- call(
- "file_info",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_file.md".into()
- ))]
- )
- .is_err(),
- "file_info should error for a nonexistent file"
- );
- assert!(call("file_info", vec![RuntimeValue::Number(42.into())]).is_err());
-
- #[cfg(unix)]
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- match call(
- "file_info",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("file_info should succeed for a directory")
- {
- RuntimeValue::Dict(d) => {
- assert_eq!(
- d.get(&Ident::new("kind")),
- Some(&RuntimeValue::String(Shared::new("dir".to_string())))
- );
- }
- other => panic!("expected Dict, got {other:?}"),
- }
-
- let target = dir.path().join("target.txt");
- std::fs::write(&target, "hello world").expect("failed to write");
- let link = dir.path().join("link.txt");
- std::os::unix::fs::symlink(&target, &link).expect("failed to create symlink");
- match call(
- "file_info",
- vec![RuntimeValue::String(Shared::new(link.to_string_lossy().into_owned()))],
- )
- .expect("file_info should succeed for a symlink")
- {
- RuntimeValue::Dict(d) => {
- assert_eq!(
- d.get(&Ident::new("kind")),
- Some(&RuntimeValue::String(Shared::new("symlink".to_string())))
- );
- }
- other => panic!("expected Dict, got {other:?}"),
- }
-
- // A cyclic symlink resolves instantly since file_info never follows symlinks.
- let a = dir.path().join("a");
- let b = dir.path().join("b");
- std::os::unix::fs::symlink(&b, &a).expect("failed to create symlink");
- std::os::unix::fs::symlink(&a, &b).expect("failed to create symlink");
- match call(
- "file_info",
- vec![RuntimeValue::String(Shared::new(a.to_string_lossy().into_owned()))],
- )
- .expect("file_info should succeed for a cyclic symlink")
- {
- RuntimeValue::Dict(d) => {
- assert_eq!(
- d.get(&Ident::new("kind")),
- Some(&RuntimeValue::String(Shared::new("symlink".to_string())))
- );
- }
- other => panic!("expected Dict, got {other:?}"),
- }
- }
-
- // Basic collection: YAML/TOML frontmatter, title, content, sorted by path.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(
- dir.path().join("b.md"),
- "---\ntitle: Hello\ntags:\n - rust\n---\n\n# Hello\n\nbody text\n",
- )
- .expect("failed to write");
- std::fs::write(dir.path().join("a.md"), "+++\ntitle = \"World\"\n+++\n\n# World\n")
- .expect("failed to write");
- std::fs::write(dir.path().join("c.md"), "# No frontmatter\n\nplain\n").expect("failed to write");
- std::fs::write(dir.path().join("ignore.txt"), "not markdown").expect("failed to write");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed"),
- );
- assert_eq!(entries.len(), 3);
-
- // sorted by path: a.md, b.md, c.md
- assert_eq!(
- get(&entries[0], "title"),
- RuntimeValue::String(Shared::new("World".into()))
- );
- let mut toml_frontmatter = DictMap::default();
- toml_frontmatter.insert(Ident::new("title"), RuntimeValue::String(Shared::new("World".into())));
- assert_eq!(
- get(&entries[0], "frontmatter"),
- RuntimeValue::Dict(Shared::new(toml_frontmatter))
- );
-
- assert_eq!(
- get(&entries[1], "title"),
- RuntimeValue::String(Shared::new("Hello".into()))
- );
- match get(&entries[1], "frontmatter") {
- RuntimeValue::Dict(d) => {
- assert_eq!(
- d.get(&Ident::new("title")),
- Some(&RuntimeValue::String(Shared::new("Hello".into())))
- );
- assert_eq!(
- d.get(&Ident::new("tags")),
- Some(&RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(
- Shared::new("rust".into())
- )])))
- );
- }
- other => panic!("expected Dict, got {other:?}"),
- }
- match get(&entries[1], "content") {
- RuntimeValue::Array(nodes) => {
- assert!(nodes.iter().any(|n| match n {
- RuntimeValue::Markdown(node, _) => node.value().contains("body text"),
- _ => false,
- }));
- assert!(
- !nodes
- .iter()
- .any(|n| matches!(n, RuntimeValue::Markdown(node, _) if node.is_yaml()))
- );
- }
- other => panic!("expected Array, got {other:?}"),
- }
-
- assert_eq!(
- get(&entries[2], "title"),
- RuntimeValue::String(Shared::new("No frontmatter".into()))
- );
- assert_eq!(get(&entries[2], "frontmatter"), RuntimeValue::NONE);
- }
-
- // Recurses into subdirectories.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("root.md"), "# Root\n").expect("failed to write");
-
- let sub = dir.path().join("sub");
- std::fs::create_dir(&sub).expect("failed to create subdir");
- std::fs::write(sub.join("nested.md"), "# Nested\n").expect("failed to write");
-
- let nested_sub = sub.join("deeper");
- std::fs::create_dir(&nested_sub).expect("failed to create nested subdir");
- std::fs::write(nested_sub.join("deepest.md"), "# Deepest\n").expect("failed to write");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed"),
- );
- assert_eq!(entries.len(), 3);
-
- let titles: Vec<_> = entries
- .iter()
- .map(|entry| match get(entry, "title") {
- RuntimeValue::String(s) => s,
- other => panic!("expected String, got {other:?}"),
- })
- .collect();
- assert!(titles.iter().any(|t| **t == "Root"));
- assert!(titles.iter().any(|t| **t == "Nested"));
- assert!(titles.iter().any(|t| **t == "Deepest"));
- }
-
- #[cfg(unix)]
- {
- // Follows symlinks to both files and directories.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("real.md"), "# Real\n").expect("failed to write");
-
- let real_sub = dir.path().join("real_sub");
- std::fs::create_dir(&real_sub).expect("failed to create subdir");
- std::fs::write(real_sub.join("inside.md"), "# Inside\n").expect("failed to write");
-
- // Symlink to a file, placed directly in the root directory.
- std::os::unix::fs::symlink(dir.path().join("real.md"), dir.path().join("linked.md"))
- .expect("failed to create file symlink");
-
- // Symlink to a directory, which should be traversed like a normal directory.
- std::os::unix::fs::symlink(&real_sub, dir.path().join("linked_dir"))
- .expect("failed to create dir symlink");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed"),
- );
- let titles: Vec<_> = entries
- .iter()
- .map(|entry| match get(entry, "title") {
- RuntimeValue::String(s) => s,
- other => panic!("expected String, got {other:?}"),
- })
- .collect();
-
- // real.md, linked.md (-> real.md), real_sub/inside.md, linked_dir/inside.md (-> real_sub/inside.md)
- assert_eq!(entries.len(), 4);
- assert_eq!(titles.iter().filter(|t| ***t == "Real").count(), 2);
- assert_eq!(titles.iter().filter(|t| ***t == "Inside").count(), 2);
- }
-
- // Detects and stops at symlink cycles.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("root.md"), "# Root\n").expect("failed to write");
-
- let sub = dir.path().join("sub");
- std::fs::create_dir(&sub).expect("failed to create subdir");
-
- // Symlink back to the root directory, creating a cycle.
- std::os::unix::fs::symlink(dir.path(), sub.join("back_to_root")).expect("failed to create dir symlink");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed despite the symlink cycle"),
- );
- assert_eq!(entries.len(), 1);
- }
-
- // Skips broken symlinks instead of erroring.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("root.md"), "# Root\n").expect("failed to write");
-
- std::os::unix::fs::symlink(dir.path().join("does_not_exist.md"), dir.path().join("broken.md"))
- .expect("failed to create broken symlink");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed despite the broken symlink"),
- );
- assert_eq!(entries.len(), 1);
- }
- }
-
- // Skips empty subdirectories.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("root.md"), "# Root\n").expect("failed to write");
- std::fs::create_dir(dir.path().join("empty_sub")).expect("failed to create empty subdir");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed with an empty subdirectory present"),
- );
- assert_eq!(entries.len(), 1);
- }
-
- // Errors when the path is a file rather than a directory.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- let file_path = dir.path().join("not_a_dir.md");
- std::fs::write(&file_path, "# Root\n").expect("failed to write");
-
- let result = call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- file_path.to_string_lossy().into_owned(),
- ))],
- );
- assert!(result.is_err());
- }
-
- // Errors for a nonexistent directory.
- assert!(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- "/nonexistent/path/no_such_dir".into()
- ))],
- )
- .is_err()
- );
-
- // Errors for an invalid argument type.
- assert!(call("collection", vec![RuntimeValue::Number(42.into())]).is_err());
-
- // respect_gitignore defaults to false: prior behavior is unchanged.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join(".gitignore"), "ignored.md\n").expect("failed to write");
- std::fs::write(dir.path().join("ignored.md"), "# Ignored\n").expect("failed to write");
- std::fs::write(dir.path().join("kept.md"), "# Kept\n").expect("failed to write");
-
- let hidden_dir = dir.path().join(".hidden");
- std::fs::create_dir(&hidden_dir).expect("failed to create hidden dir");
- std::fs::write(hidden_dir.join("secret.md"), "# Secret\n").expect("failed to write");
-
- let entries = as_entries(
- call(
- "collection",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("collection should succeed"),
- );
- assert_eq!(
- entries.len(),
- 3,
- "ignored.md and .hidden/secret.md should still be collected"
- );
- }
-
- // respect_gitignore = true: skips dotfiles and .gitignore matches; nested .gitignore wins.
- {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join(".gitignore"), "*.log.md\nbuild/\n").expect("failed to write");
- std::fs::write(dir.path().join("kept.md"), "# Kept\n").expect("failed to write");
- std::fs::write(dir.path().join("debug.log.md"), "# Debug\n").expect("failed to write");
-
- let hidden_dir = dir.path().join(".hidden");
- std::fs::create_dir(&hidden_dir).expect("failed to create hidden dir");
- std::fs::write(hidden_dir.join("secret.md"), "# Secret\n").expect("failed to write");
-
- let build_dir = dir.path().join("build");
- std::fs::create_dir(&build_dir).expect("failed to create build dir");
- std::fs::write(build_dir.join("out.md"), "# Out\n").expect("failed to write");
-
- // A subdirectory's .gitignore re-allows a file the parent's .gitignore ignores.
- let sub = dir.path().join("sub");
- std::fs::create_dir(&sub).expect("failed to create subdir");
- std::fs::write(sub.join(".gitignore"), "!debug.log.md\n").expect("failed to write");
- std::fs::write(sub.join("debug.log.md"), "# Sub debug\n").expect("failed to write");
-
- let entries = as_entries(
- call(
- "collection",
- vec![
- RuntimeValue::String(Shared::new(dir.path().to_string_lossy().into_owned())),
- RuntimeValue::Boolean(true),
- ],
- )
- .expect("collection should succeed"),
- );
- let titles: Vec<_> = entries
- .iter()
- .map(|entry| match get(entry, "title") {
- RuntimeValue::String(s) => s,
- other => panic!("expected String, got {other:?}"),
- })
- .collect();
-
- assert_eq!(entries.len(), 2, "titles: {titles:?}");
- assert!(titles.iter().any(|t| **t == "Kept"));
- assert!(titles.iter().any(|t| **t == "Sub debug"));
- }
-
- // Errors for an invalid respect_gitignore argument type.
- assert!(
- call(
- "collection",
- vec![
- RuntimeValue::String(Shared::new("/nonexistent/path".into())),
- RuntimeValue::Number(1.into())
- ],
- )
- .is_err()
- );
- }
-
- #[cfg(feature = "file-io")]
- fn walk_files_paths(result: RuntimeValue) -> Vec {
- match result {
- RuntimeValue::Array(entries) => entries
- .iter()
- .map(|e| match e {
- RuntimeValue::String(s) => s.to_string(),
- other => panic!("expected String, got {other:?}"),
- })
- .collect(),
- other => panic!("expected Array, got {other:?}"),
- }
- }
-
- #[cfg(feature = "file-io")]
- fn walk_files_options(entries: &[(&str, bool)]) -> RuntimeValue {
- let mut map = DictMap::default();
- for (key, value) in entries {
- map.insert(Ident::new(key), RuntimeValue::Boolean(*value));
- }
- RuntimeValue::Dict(Shared::new(map))
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_walk_files_requires_allow_read() {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("a.md"), "a").expect("failed to write");
-
- // No ambient Io guard installed: the default is all-denied.
- assert!(
- call(
- "walk_files",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned()
- ))],
- )
- .is_err(),
- "walk_files should be blocked when read access is not allowed"
- );
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_walk_files_enumerates_all_file_types_sorted() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("b.txt"), "b").expect("failed to write");
- std::fs::write(dir.path().join("a.md"), "a").expect("failed to write");
- let sub = dir.path().join("sub");
- std::fs::create_dir(&sub).expect("failed to create subdir");
- std::fs::write(sub.join("c.png"), "c").expect("failed to write");
-
- let paths = walk_files_paths(
- call(
- "walk_files",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("walk_files should succeed"),
- );
- assert_eq!(paths, vec!["a.md", "b.txt", "sub/c.png"]);
- }
-
- #[cfg(feature = "file-io")]
- #[rstest]
- // Default pattern ("**") matches every non-directory entry, Markdown or not.
- #[case("**", vec!["a.md", "b.txt", "sub/c.md"])]
- // A narrower glob restricts matches to a root-relative subset.
- #[case("**/*.md", vec!["a.md", "sub/c.md"])]
- fn test_walk_files_pattern(#[case] pattern: &str, #[case] expected: Vec<&str>) {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("a.md"), "a").expect("failed to write");
- std::fs::write(dir.path().join("b.txt"), "b").expect("failed to write");
- let sub = dir.path().join("sub");
- std::fs::create_dir(&sub).expect("failed to create subdir");
- std::fs::write(sub.join("c.md"), "c").expect("failed to write");
-
- let paths = walk_files_paths(
- call(
- "walk_files",
- vec![
- RuntimeValue::String(Shared::new(dir.path().to_string_lossy().into_owned())),
- RuntimeValue::String(Shared::new(pattern.into())),
- ],
- )
- .expect("walk_files should succeed"),
- );
- assert_eq!(paths, expected);
- }
-
- #[cfg(feature = "file-io")]
- #[rstest]
- // Defaults to false: dotfiles/.gitignore matches are kept, same as `collection`.
- #[case(None, vec![".gitignore", ".hidden/secret.txt", "ignored.txt", "kept.txt"])]
- // true: skips dotfiles/dot-directories and any path a .gitignore matches.
- #[case(Some(true), vec!["kept.txt"])]
- fn test_walk_files_respect_gitignore(#[case] respect_gitignore: Option, #[case] expected: Vec<&str>) {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join(".gitignore"), "ignored.txt\n").expect("failed to write");
- std::fs::write(dir.path().join("ignored.txt"), "x").expect("failed to write");
- std::fs::write(dir.path().join("kept.txt"), "x").expect("failed to write");
- let hidden_dir = dir.path().join(".hidden");
- std::fs::create_dir(&hidden_dir).expect("failed to create hidden dir");
- std::fs::write(hidden_dir.join("secret.txt"), "x").expect("failed to write");
-
- let mut args = vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))];
- if let Some(respect_gitignore) = respect_gitignore {
- args.push(RuntimeValue::String(Shared::new("**".into())));
- args.push(walk_files_options(&[("respect_gitignore", respect_gitignore)]));
- }
-
- let paths = walk_files_paths(call("walk_files", args).expect("walk_files should succeed"));
- assert_eq!(paths, expected);
- }
-
- #[cfg(all(feature = "file-io", unix))]
- #[rstest]
- // Defaults to false: a symlinked file is still included, but a symlinked
- // directory is not descended into (so it contributes no entries at all).
- #[case(None, vec!["linked.txt", "real.txt", "real_sub/inside.txt"])]
- // true: descends into symlinked directories too.
- #[case(
- Some(true),
- vec!["linked.txt", "linked_dir/inside.txt", "real.txt", "real_sub/inside.txt"]
- )]
- fn test_walk_files_follow_symlinks(#[case] follow_symlinks: Option, #[case] expected: Vec<&str>) {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("real.txt"), "x").expect("failed to write");
- let real_sub = dir.path().join("real_sub");
- std::fs::create_dir(&real_sub).expect("failed to create subdir");
- std::fs::write(real_sub.join("inside.txt"), "x").expect("failed to write");
- std::os::unix::fs::symlink(dir.path().join("real.txt"), dir.path().join("linked.txt"))
- .expect("failed to create file symlink");
- std::os::unix::fs::symlink(&real_sub, dir.path().join("linked_dir")).expect("failed to create dir symlink");
-
- let mut args = vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))];
- if let Some(follow_symlinks) = follow_symlinks {
- args.push(RuntimeValue::String(Shared::new("**".into())));
- args.push(walk_files_options(&[("follow_symlinks", follow_symlinks)]));
- }
-
- let paths = walk_files_paths(call("walk_files", args).expect("walk_files should succeed"));
- assert_eq!(paths, expected);
- }
-
- #[cfg(all(feature = "file-io", unix))]
- #[test]
- fn test_walk_files_detects_symlink_cycles_even_with_follow_symlinks() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("root.txt"), "x").expect("failed to write");
- let sub = dir.path().join("sub");
- std::fs::create_dir(&sub).expect("failed to create subdir");
- std::os::unix::fs::symlink(dir.path(), sub.join("back_to_root")).expect("failed to create dir symlink");
-
- let paths = walk_files_paths(
- call(
- "walk_files",
- vec![
- RuntimeValue::String(Shared::new(dir.path().to_string_lossy().into_owned())),
- RuntimeValue::String(Shared::new("**".into())),
- walk_files_options(&[("follow_symlinks", true)]),
- ],
- )
- .expect("walk_files should succeed despite the symlink cycle"),
- );
- assert_eq!(paths, vec!["root.txt"]);
- }
-
- #[cfg(all(feature = "file-io", unix))]
- #[test]
- fn test_walk_files_skips_broken_symlinks() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("root.txt"), "x").expect("failed to write");
- std::os::unix::fs::symlink(dir.path().join("does_not_exist"), dir.path().join("broken"))
- .expect("failed to create broken symlink");
-
- let paths = walk_files_paths(
- call(
- "walk_files",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("walk_files should succeed despite the broken symlink"),
- );
- assert_eq!(paths, vec!["root.txt"]);
- }
-
- // macOS/APFS rejects non-UTF-8 filenames outright, so this is only exercisable on Linux.
- #[cfg(all(feature = "file-io", target_os = "linux"))]
- #[test]
- fn test_walk_files_includes_non_utf8_filenames() {
- use std::ffi::OsStr;
- use std::os::unix::ffi::OsStrExt;
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- let name = OsStr::from_bytes(b"report\xff.md");
- std::fs::write(dir.path().join(name), "x").expect("failed to write");
-
- let paths = walk_files_paths(
- call(
- "walk_files",
- vec![RuntimeValue::String(Shared::new(
- dir.path().to_string_lossy().into_owned(),
- ))],
- )
- .expect("walk_files should succeed"),
- );
- assert_eq!(paths.len(), 1, "non-UTF-8 filename should not be dropped");
- }
-
- #[cfg(all(feature = "file-io", unix))]
- #[test]
- fn test_walk_files_rejects_symlink_escape_outside_allowed_paths() {
- let allowed = tempfile::tempdir().expect("failed to create temp dir");
- let outside = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(outside.path().join("secret.md"), "secret").expect("failed to write");
- std::os::unix::fs::symlink(outside.path(), allowed.path().join("escape")).expect("failed to create symlink");
-
- let _guard = io_context::scoped(Shared::new(
- SandboxedIo::new(NativeIo::default()).allow_read(vec![allowed.path().to_path_buf()]),
- ));
-
- assert!(
- call(
- "walk_files",
- vec![
- RuntimeValue::String(Shared::new(allowed.path().to_string_lossy().into_owned())),
- RuntimeValue::String(Shared::new("**".into())),
- walk_files_options(&[("follow_symlinks", true)]),
- ],
- )
- .is_err(),
- "walking into a symlink that resolves outside the allowed paths must be denied"
- );
-
- // Calling walk_files directly on the escaping symlink is denied the same way.
- assert!(
- call(
- "walk_files",
- vec![RuntimeValue::String(Shared::new(
- allowed.path().join("escape").to_string_lossy().into_owned(),
- ))],
- )
- .is_err()
- );
- }
-
- #[cfg(feature = "file-io")]
- #[rstest]
- // Invalid root argument type.
- #[case(|_dir: &str| vec![RuntimeValue::Number(42.into())])]
- // Nonexistent directory.
- #[case(|_dir: &str| vec![RuntimeValue::String(Shared::new("/nonexistent/path/no_such_dir".into()))])]
- // Invalid glob pattern syntax.
- #[case(|dir: &str| vec![
- RuntimeValue::String(Shared::new(dir.to_string())),
- RuntimeValue::String(Shared::new("[".into())),
- ])]
- // Invalid pattern argument type.
- #[case(|dir: &str| vec![
- RuntimeValue::String(Shared::new(dir.to_string())),
- RuntimeValue::Number(42.into()),
- ])]
- fn test_walk_files_errors(#[case] build_args: fn(&str) -> Vec) {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- let args = build_args(&dir.path().to_string_lossy());
- assert!(call("walk_files", args).is_err());
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_write_file_capability_gate_and_success() {
- let tmp = tempfile::NamedTempFile::new().expect("failed to create temp file");
- let path = tmp.path().to_string_lossy().to_string();
- assert!(
- call(
- "write_file",
- vec![
- RuntimeValue::String(Shared::new(path.clone())),
- RuntimeValue::String(Shared::new("hello".into()))
- ]
- )
- .is_err(),
- "write_file should be blocked when write access is not allowed"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_write(true)));
- assert_eq!(
- call(
- "write_file",
- vec![
- RuntimeValue::String(Shared::new(path.clone())),
- RuntimeValue::String(Shared::new("hello".into()))
- ]
- ),
- Ok(RuntimeValue::NONE)
- );
- assert_eq!(std::fs::read_to_string(&path).unwrap(), "hello");
-
- assert_eq!(
- call(
- "write_file",
- vec![
- RuntimeValue::String(Shared::new(path.clone())),
- RuntimeValue::Bytes(Shared::new(vec![1, 2, 3]))
- ]
- ),
- Ok(RuntimeValue::NONE)
- );
- assert_eq!(std::fs::read(&path).unwrap(), vec![1, 2, 3]);
-
- let result = call(
- "write_file",
- vec![
- RuntimeValue::String(Shared::new("/nonexistent/dir/no_such_file.md".into())),
- RuntimeValue::String(Shared::new("hello".into())),
- ],
- );
- assert!(
- result.is_err(),
- "write_file should error when the parent directory doesn't exist"
- );
- }
-
- #[cfg(feature = "process-io")]
- #[test]
- fn test_system_capability_gate_and_success() {
- assert!(
- call("system", vec![RuntimeValue::String(Shared::new("echo".into()))]).is_err(),
- "system should be blocked when run access is not allowed"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_run(true)));
-
- let result = call(
- "system",
- vec![
- RuntimeValue::String(Shared::new("echo".into())),
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new("hello".into()))])),
- ],
- )
- .unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new("hello\n".to_string())));
-
- assert!(
- call(
- "system",
- vec![RuntimeValue::String(Shared::new(
- "mq-this-command-should-not-exist".into()
- ))]
- )
- .is_err(),
- "system should error for a missing command"
- );
-
- assert!(
- call(
- "system",
- vec![
- RuntimeValue::String(Shared::new("echo".into())),
- RuntimeValue::Array(Shared::new(vec![RuntimeValue::Number(1.into())])),
- ],
- )
- .is_err(),
- "system should error when args contains a non-string value"
- );
-
- assert!(call("system", vec![RuntimeValue::Number(42.into())]).is_err());
- }
-
- #[cfg(feature = "file-io")]
- fn image_value(url: &str) -> RuntimeValue {
- RuntimeValue::Markdown(
- Shared::new(mq_markdown::Node::Image(mq_markdown::Image {
- alt: "alt".to_string(),
- url: url.to_string(),
- title: None,
- position: None,
- })),
- None,
- )
- }
-
- #[cfg(feature = "file-io")]
- fn image_url(value: &RuntimeValue) -> String {
- match value {
- RuntimeValue::Markdown(node, _) => match &**node {
- mq_markdown::Node::Image(image) => image.url.clone(),
- other => panic!("expected Image node, got {other:?}"),
- },
- other => panic!("expected Markdown value, got {other:?}"),
- }
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_embed_images_capability_gate_and_success() {
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- std::fs::write(dir.path().join("img.png"), [0x89, 0x50, 0x4e, 0x47]).expect("failed to write");
- let base_dir = dir.path().to_string_lossy().to_string();
-
- assert!(
- call(
- "embed_images",
- vec![
- image_value("img.png"),
- RuntimeValue::String(Shared::new(base_dir.clone()))
- ]
- )
- .is_err(),
- "embed_images should be blocked when read access is not allowed"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_read(true)));
-
- let result = call(
- "embed_images",
- vec![
- image_value("img.png"),
- RuntimeValue::String(Shared::new(base_dir.clone())),
- ],
- )
- .expect("embed_images should succeed");
- assert_eq!(image_url(&result), "data:image/png;base64,iVBORw==");
-
- // Default base_dir (".") combined with an absolute path.
- let absolute_path = dir.path().join("img.png").to_string_lossy().to_string();
- let result = call("embed_images", vec![image_value(&absolute_path)]).expect("embed_images should succeed");
- assert_eq!(image_url(&result), "data:image/png;base64,iVBORw==");
-
- // Already-embedded, remote, and non-image nodes pass through unchanged.
- let already_embedded = "data:image/png;base64,aGVsbG8=";
- assert_eq!(
- call(
- "embed_images",
- vec![
- image_value(already_embedded),
- RuntimeValue::String(Shared::new(base_dir.clone()))
- ]
- ),
- Ok(image_value(already_embedded))
- );
- assert_eq!(
- call(
- "embed_images",
- vec![
- image_value("https://example.com/img.png"),
- RuntimeValue::String(Shared::new(base_dir.clone()))
- ]
- ),
- Ok(image_value("https://example.com/img.png"))
- );
- let text_node = RuntimeValue::Markdown(
- Shared::new(mq_markdown::Node::Text(mq_markdown::Text {
- value: "hello".to_string(),
- position: None,
- })),
- None,
- );
- assert_eq!(
- call(
- "embed_images",
- vec![text_node.clone(), RuntimeValue::String(Shared::new(base_dir.clone()))]
- ),
- Ok(text_node)
- );
-
- let result = call(
- "embed_images",
- vec![
- image_value("no_such_file.png"),
- RuntimeValue::String(Shared::new(base_dir.clone())),
- ],
- );
- assert!(result.is_err(), "embed_images should error for a nonexistent file");
-
- let result = call(
- "embed_images",
- vec![image_value("img.unsupported"), RuntimeValue::String(base_dir.into())],
- );
- assert!(
- result.is_err(),
- "embed_images should error for an unsupported extension"
- );
- }
-
- #[cfg(feature = "file-io")]
- #[test]
- fn test_extract_images_capability_gate_and_success() {
- let bytes = [0x89, 0x50, 0x4e, 0x47];
- let encoded = base64::engine::general_purpose::STANDARD.encode(bytes);
- let data_url = format!("data:image/png;base64,{encoded}");
- let dir = tempfile::tempdir().expect("failed to create temp dir");
- let out_dir = dir.path().to_string_lossy().to_string();
-
- assert!(
- call(
- "extract_images",
- vec![
- image_value(&data_url),
- RuntimeValue::String(Shared::new(out_dir.clone()))
- ]
- )
- .is_err(),
- "extract_images should be blocked when write access is not allowed"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_write(true)));
-
- let result = call(
- "extract_images",
- vec![
- image_value(&data_url),
- RuntimeValue::String(Shared::new(out_dir.clone())),
- ],
- )
- .expect("extract_images should succeed");
- let hash = match convert::md5_bytes(&bytes).unwrap() {
- RuntimeValue::String(s) => s,
- _ => unreachable!(),
- };
- let expected_path = dir.path().join(format!("{hash}.png"));
- assert_eq!(image_url(&result), expected_path.to_string_lossy());
- assert_eq!(std::fs::read(&expected_path).unwrap(), bytes);
-
- // Non-data-URI and non-image nodes pass through unchanged.
- assert_eq!(
- call(
- "extract_images",
- vec![
- image_value("img.png"),
- RuntimeValue::String(Shared::new(out_dir.clone()))
- ]
- ),
- Ok(image_value("img.png"))
- );
-
- let result = call(
- "extract_images",
- vec![
- image_value("data:image/png;base64"),
- RuntimeValue::String(Shared::new(out_dir.clone())),
- ],
- );
- assert!(result.is_err(), "extract_images should error on a malformed data URI");
-
- let result = call(
- "extract_images",
- vec![
- image_value(&format!("data:image/png,{encoded}")),
- RuntimeValue::String(Shared::new(out_dir.clone())),
- ],
- );
- assert!(result.is_err(), "extract_images should error on a non-base64 data URI");
-
- let result = call(
- "extract_images",
- vec![
- image_value(&format!("data:application/pdf;base64,{encoded}")),
- RuntimeValue::String(out_dir.into()),
- ],
- );
- assert!(
- result.is_err(),
- "extract_images should error on an unsupported MIME type"
- );
- }
-
- #[cfg(all(feature = "http", feature = "mock-io"))]
- #[test]
- fn test_mock_fetch_seeds_a_response_the_http_builtin_then_reads_back() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default()).allow_net(true)));
-
- assert_eq!(
- call(
- "mock_fetch",
- vec![
- RuntimeValue::String(Shared::new("https://example.invalid".into())),
- RuntimeValue::String(Shared::new("body".into())),
- ]
- ),
- Ok(RuntimeValue::NONE)
- );
- assert_eq!(
- call(
- "http",
- vec![
- RuntimeValue::Symbol(Ident::new("get")),
- RuntimeValue::String(Shared::new("https://example.invalid".into())),
- ]
- ),
- Ok(RuntimeValue::String(Shared::new("body".into())))
- );
- }
-
- #[test]
- fn test_env_returns_a_dict_restricted_to_allowed_names() {
- let _guard = io_context::scoped(Shared::new(
- SandboxedIo::new(MemIo::default().with_env("MQ_TEST_VAR", "value").with_env("OTHER", "x"))
- .allow_env(vec!["MQ_TEST_VAR".to_string()]),
- ));
-
- assert_eq!(
- call("env", vec![]),
- Ok(RuntimeValue::from(DictMap::from_iter([(
- Ident::new("MQ_TEST_VAR"),
- RuntimeValue::String(Shared::new("value".into()))
- )])))
- );
- }
-
- #[test]
- fn test_env_with_name_returns_the_value_or_none_if_unset() {
- let _guard = io_context::scoped(Shared::new(
- SandboxedIo::new(MemIo::default().with_env("MQ_TEST_VAR", "value")).allow_env(true),
- ));
-
- assert_eq!(
- call("env", vec![RuntimeValue::String(Shared::new("MQ_TEST_VAR".into()))]),
- Ok(RuntimeValue::String(Shared::new("value".into())))
- );
- assert_eq!(
- call("env", vec![RuntimeValue::String(Shared::new("MQ_MISSING".into()))]),
- Ok(RuntimeValue::NONE)
- );
- }
-
- #[test]
- fn test_env_with_name_errors_when_not_in_the_allowlist() {
- let _guard = io_context::scoped(Shared::new(
- SandboxedIo::new(MemIo::default().with_env("MQ_TEST_VAR", "value")).allow_env(vec!["OTHER".to_string()]),
- ));
- assert!(call("env", vec![RuntimeValue::String(Shared::new("MQ_TEST_VAR".into()))]).is_err());
- }
-
- #[test]
- fn test_env_errors_when_env_access_is_denied() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default())));
- assert!(call("env", vec![]).is_err());
- }
-
- #[cfg(all(feature = "http", feature = "mock-io"))]
- #[test]
- fn test_open_http_streams_the_mocked_response_line_by_line() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default()).allow_net(true)));
- call(
- "mock_fetch",
- vec![
- RuntimeValue::String(Shared::new("https://example.invalid".into())),
- RuntimeValue::String(Shared::new("a\nb\nc".into())),
- ],
- )
- .unwrap();
-
- let handle = call(
- "open_http",
- vec![
- RuntimeValue::Symbol(Ident::new("get")),
- RuntimeValue::String(Shared::new("https://example.invalid".into())),
- ],
- )
- .unwrap();
- assert_eq!(call("type", vec![handle.clone()]).unwrap().to_string(), "http");
-
- for line in ["a", "b", "c"] {
- assert_eq!(
- call("read_line", vec![handle.clone()]),
- Ok(RuntimeValue::String(Shared::new(line.into())))
- );
- }
- assert_eq!(call("read_line", vec![handle.clone()]), Ok(RuntimeValue::NONE));
-
- assert!(call("close", vec![handle.clone()]).is_ok());
- assert_eq!(
- call("status", vec![handle]),
- Ok(RuntimeValue::Symbol(Ident::new("closed")))
- );
- }
-
- #[cfg(all(feature = "http", feature = "mock-io"))]
- #[test]
- fn test_http_all_fetches_each_request_in_batch_order() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default()).allow_net(true)));
- for (url, body) in [
- ("https://example.invalid/a", "body-a"),
- ("https://example.invalid/b", "body-b"),
- ("https://example.invalid/c", "body-c"),
- ] {
- call(
- "mock_fetch",
- vec![
- RuntimeValue::String(Shared::new(url.into())),
- RuntimeValue::String(Shared::new(body.into())),
- ],
- )
- .unwrap();
- }
-
- let requests = RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::Dict(Shared::new(DictMap::from_iter([(
- Ident::new("url"),
- RuntimeValue::String(Shared::new("https://example.invalid/a".into())),
- )]))),
- RuntimeValue::Dict(Shared::new(DictMap::from_iter([(
- Ident::new("url"),
- RuntimeValue::String(Shared::new("https://example.invalid/b".into())),
- )]))),
- RuntimeValue::Dict(Shared::new(DictMap::from_iter([
- (Ident::new("method"), RuntimeValue::Symbol(Ident::new("post"))),
- (
- Ident::new("url"),
- RuntimeValue::String(Shared::new("https://example.invalid/c".into())),
- ),
- (Ident::new("body"), RuntimeValue::String(Shared::new("payload".into()))),
- ]))),
- ]));
-
- assert_eq!(
- call("http_all", vec![requests]),
- Ok(RuntimeValue::Array(Shared::new(vec![
- RuntimeValue::String(Shared::new("body-a".into())),
- RuntimeValue::String(Shared::new("body-b".into())),
- RuntimeValue::String(Shared::new("body-c".into())),
- ])))
- );
- }
-
- #[cfg(all(feature = "http", feature = "mock-io"))]
- #[test]
- fn test_http_all_rejects_non_dict_request() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default()).allow_net(true)));
-
- let requests = RuntimeValue::Array(Shared::new(vec![RuntimeValue::String(Shared::new(
- "https://example.invalid".into(),
- ))]));
-
- assert!(call("http_all", vec![requests]).is_err());
- }
-
- #[cfg(all(feature = "http", feature = "mock-io"))]
- #[test]
- fn test_http_all_rejects_request_without_url() {
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(MemIo::default()).allow_net(true)));
-
- let requests = RuntimeValue::Array(Shared::new(vec![RuntimeValue::Dict(Shared::new(DictMap::from_iter(
- [(Ident::new("method"), RuntimeValue::Symbol(Ident::new("get")))],
- )))]));
-
- assert!(call("http_all", vec![requests]).is_err());
- }
-
- #[cfg(all(feature = "http", feature = "mock-io"))]
- #[test]
- fn test_mock_fetch_is_refused_by_non_mock_io() {
- assert!(
- call(
- "mock_fetch",
- vec![
- RuntimeValue::String(Shared::new("https://example.invalid".into())),
- RuntimeValue::String(Shared::new("body".into())),
- ]
- )
- .is_err(),
- "mock_fetch should be refused when the ambient Io isn't a mock"
- );
-
- let _guard = io_context::scoped(Shared::new(SandboxedIo::new(NativeIo::default()).allow_net(true)));
- assert!(
- call(
- "mock_fetch",
- vec![
- RuntimeValue::String(Shared::new("https://example.invalid".into())),
- RuntimeValue::String(Shared::new("body".into())),
- ]
- )
- .is_err(),
- "mock_fetch should be refused against a real, network-backed Io"
- );
- }
-}
+mod tests;
diff --git a/crates/mq-lang/src/runtime/builtin/convert.rs b/crates/mq-lang/src/runtime/builtin/convert.rs
index eb7a78166..888e863cf 100644
--- a/crates/mq-lang/src/runtime/builtin/convert.rs
+++ b/crates/mq-lang/src/runtime/builtin/convert.rs
@@ -935,949 +935,4 @@ pub(super) fn flatten(args: Vec) -> Vec {
}
#[cfg(test)]
-mod tests {
- use super::*;
- use crate::Ident;
- use rstest::rstest;
-
- #[rstest]
- #[case::empty(b"".to_vec(), "00000000")]
- #[case::short(
- b"hi".to_vec(),
- "00000000 68 69 |hi|"
- )]
- #[case::exactly_one_row(
- (0u8..16).collect::>(),
- "00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|"
- )]
- #[case::multi_row(
- (0u8..=0x22).collect::>(),
- "00000000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f |................|\n\
- 00000010 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f |................|\n\
- 00000020 20 21 22 | !\"|"
- )]
- fn test_hexdump(#[case] input: Vec, #[case] expected: &str) {
- assert_eq!(hexdump(&input), Ok(RuntimeValue::String(expected.to_string().into())));
- }
-
- // Test convert::try_from
- #[rstest]
- #[case::h1_symbol(
- RuntimeValue::Symbol(Ident::new("h1")),
- Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H1))
- )]
- #[case::h2_symbol(
- RuntimeValue::Symbol(Ident::new("h2")),
- Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H2))
- )]
- #[case::h3_symbol(
- RuntimeValue::Symbol(Ident::new("h3")),
- Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H3))
- )]
- #[case::h4_symbol(
- RuntimeValue::Symbol(Ident::new("h4")),
- Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H4))
- )]
- #[case::h5_symbol(
- RuntimeValue::Symbol(Ident::new("h5")),
- Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H5))
- )]
- #[case::h6_symbol(
- RuntimeValue::Symbol(Ident::new("h6")),
- Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H6))
- )]
- #[case::html_symbol(RuntimeValue::Symbol(Ident::new("html")), Convert::Html)]
- #[case::md5_symbol(RuntimeValue::Symbol(Ident::new("md5")), Convert::Md5)]
- #[case::sha256_symbol(RuntimeValue::Symbol(Ident::new("sha256")), Convert::Sha256)]
- #[case::text_symbol(RuntimeValue::Symbol(Ident::new("text")), Convert::Text)]
- #[case::sh_symbol(RuntimeValue::Symbol(Ident::new("sh")), Convert::Shell)]
- #[case::h1_string(RuntimeValue::String(Shared::new("#".to_string())), Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H1)))]
- #[case::h2_string(RuntimeValue::String(Shared::new("##".to_string())), Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H2)))]
- #[case::h3_string(RuntimeValue::String(Shared::new("###".to_string())), Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H3)))]
- #[case::h4_string(RuntimeValue::String(Shared::new("####".to_string())), Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H4)))]
- #[case::h5_string(RuntimeValue::String(Shared::new("#####".to_string())), Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H5)))]
- #[case::h6_string(RuntimeValue::String(Shared::new("######".to_string())), Convert::Markdown(ConvertKind::Heading(mq_markdown::HeadingDepth::H6)))]
- #[case::blockquote_string(RuntimeValue::String(Shared::new(">".to_string())), Convert::Markdown(ConvertKind::Blockquote))]
- #[case::list_item_string(RuntimeValue::String(Shared::new("-".to_string())), Convert::Markdown(ConvertKind::ListItem))]
- #[case::strikethrough_string(RuntimeValue::String(Shared::new("~~".to_string())), Convert::Markdown(ConvertKind::Strikethrough))]
- #[case::strong_string(RuntimeValue::String(Shared::new("**".to_string())), Convert::Markdown(ConvertKind::Strong))]
- #[case::horizontal_rule_string(RuntimeValue::String(Shared::new("--".to_string())), Convert::Markdown(ConvertKind::HorizontalRule))]
- fn test_convert_convert_try_from_valid(#[case] input: RuntimeValue, #[case] expected: Convert) {
- let result = Convert::try_from(&input);
- assert!(result.is_ok());
-
- // Compare the enum variants
- match (result.unwrap(), expected) {
- (Convert::Markdown(ConvertKind::Heading(d1)), Convert::Markdown(ConvertKind::Heading(d2))) => {
- assert_eq!(d1, d2);
- }
- (a, b) => assert_eq!(a, b),
- }
- }
-
- #[rstest]
- #[case::invalid_symbol(RuntimeValue::Symbol(Ident::new("invalid")))]
- #[case::invalid_string(RuntimeValue::String(Shared::new("invalid".to_string())))]
- #[case::number(RuntimeValue::Number(42.into()))]
- #[case::boolean(RuntimeValue::Boolean(true))]
- fn test_convert_convert_try_from_invalid(#[case] input: RuntimeValue) {
- let result = Convert::try_from(&input);
- assert!(result.is_err());
- }
-
- // Test base64 functions
- #[rstest]
- #[case("hello", "aGVsbG8=")]
- #[case("world", "d29ybGQ=")]
- #[case("test", "dGVzdA==")]
- #[case("", "")]
- #[case("Hello, ไธ็!", "SGVsbG8sIOS4lueVjCE=")]
- #[case("emoji: ๐", "ZW1vamk6IPCfmIA=")]
- #[case("line\nbreak", "bGluZQpicmVhaw==")]
- fn test_base64_encode(#[case] input: &str, #[case] expected: &str) {
- let result = base64(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- #[rstest]
- #[case("aGVsbG8=", "hello")]
- #[case("d29ybGQ=", "world")]
- #[case("dGVzdA==", "test")]
- #[case("", "")]
- fn test_base64_decode(#[case] input: &str, #[case] expected: &str) {
- let result = base64d(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- #[rstest]
- #[case("not valid base64!@#")]
- #[case("SGVsbG8")] // Missing padding might still work in some implementations
- fn test_base64d_invalid(#[case] input: &str) {
- let result = base64d(input);
- // Some invalid inputs might error
- if result.is_ok() {
- // If it succeeds, it should produce some output
- assert!(matches!(result.unwrap(), RuntimeValue::String(_)));
- }
- }
-
- #[rstest]
- #[case("hello", "aGVsbG8")]
- #[case("world", "d29ybGQ")]
- #[case("test", "dGVzdA")]
- #[case("subjects?", "c3ViamVjdHM_")]
- #[case("subjects>", "c3ViamVjdHM-")]
- fn test_base64url_encode(#[case] input: &str, #[case] expected: &str) {
- let result = base64url(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- #[rstest]
- #[case("aGVsbG8", "hello")]
- #[case("d29ybGQ", "world")]
- #[case("dGVzdA", "test")]
- fn test_base64url_decode(#[case] input: &str, #[case] expected: &str) {
- let result = base64urld(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test URL encoding
- #[rstest]
- #[case("hello world", "hello%20world")]
- #[case("test@example.com", "test%40example%2Ecom")]
- #[case("a+b=c", "a%2Bb%3Dc")]
- #[case("", "")]
- #[case("foo/bar", "foo%2Fbar")]
- #[case("foo?bar=baz", "foo%3Fbar%3Dbaz")]
- #[case("foo&bar", "foo%26bar")]
- #[case("100%", "100%25")]
- #[case("cafรฉ", "caf%C3%A9")]
- fn test_url_encode(#[case] input: &str, #[case] expected: &str) {
- let result = url_encode(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test html_escape
- #[rstest]
- #[case("bold ", "<b>bold</b>")]
- #[case("Tom & Jerry", "Tom & Jerry")]
- #[case(r#"say "hi""#, "say "hi"")]
- #[case("it's", "it's")]
- #[case("plain text", "plain text")]
- #[case("", "")]
- fn test_html_escape(#[case] input: &str, #[case] expected: &str) {
- let result = html_escape(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test html_unescape
- #[rstest]
- #[case("<b>bold</b>", "bold ")]
- #[case("Tom & Jerry", "Tom & Jerry")]
- #[case(""quoted"", "\"quoted\"")]
- #[case("'single'", "'single'")]
- #[case("'apos'", "'apos'")]
- #[case("AB", "AB")]
- #[case("plain text", "plain text")]
- #[case("", "")]
- fn test_html_unescape(#[case] input: &str, #[case] expected: &str) {
- let result = html_unescape(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test html_escape/html_unescape round trip
- #[rstest]
- #[case("")]
- #[case(r#"5 > 3 && 2 < 4, say "hi""#)]
- fn test_html_escape_unescape_round_trip(#[case] input: &str) {
- let escaped = html_escape(input).unwrap();
- let RuntimeValue::String(escaped) = escaped else {
- panic!("Expected String")
- };
- let unescaped = html_unescape(&escaped).unwrap();
- assert_eq!(unescaped, RuntimeValue::String(Shared::new(input.to_string())));
- }
-
- // Test markdown_escape: text context
- #[rstest]
- #[case("plain text", "plain text")]
- #[case("**bold** and _em_", r"\*\*bold\*\* and \_em\_")]
- #[case("a ~b~ `c` ", r"a \~b\~ \`c\` \")]
- #[case(r"back\slash", r"back\\slash")]
- #[case("[label](url)", r"\[label\](url)")]
- #[case("", r"!\[alt\](url)")]
- #[case("# Not a heading", r"\# Not a heading")]
- #[case("###### Not a heading", r"\###### Not a heading")]
- #[case("####### still text", "####### still text")] // 7 hashes: not a valid ATX marker
- #[case("> Not a quote", r"\> Not a quote")]
- #[case("- Not a list item", r"\- Not a list item")]
- #[case("+ Not a list item", r"\+ Not a list item")]
- #[case("1. Not a list item", r"1\. Not a list item")]
- #[case("42) Not a list item", r"42\) Not a list item")]
- #[case("not-a-list.item", "not-a-list.item")]
- #[case("---", r"\---")]
- #[case("===", r"\===")]
- #[case("- - -", r"\- - -")]
- #[case("Title\n===\nBody", "Title\n\\===\nBody")]
- #[case(" - indented list", " \\- indented list")]
- #[case(" four spaces", " four spaces")]
- #[case("\tone tab", " one tab")]
- #[case(" \tspaces then tab", " spaces then tab")]
- fn test_markdown_escape_text(#[case] input: &str, #[case] expected: &str) {
- let result = markdown_escape(input, MarkdownEscapeContext::Text).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- #[rstest]
- #[case(" customer value")]
- #[case("\tcustomer value")]
- #[case(" \tcustomer value")]
- fn test_markdown_escape_text_indentation_does_not_become_code_block(#[case] input: &str) {
- let escaped = match markdown_escape(input, MarkdownEscapeContext::Text).unwrap() {
- RuntimeValue::String(s) => s.to_string(),
- _ => panic!("Expected String"),
- };
- let md = mq_markdown::Markdown::from_markdown_str(&escaped).unwrap();
- assert!(
- !md.nodes.iter().any(|n| matches!(n, mq_markdown::Node::Code(_))),
- "escaped text {:?} parsed back into a code block: {:?}",
- escaped,
- md.nodes
- );
- }
-
- // Test markdown_escape: heading and link_label contexts collapse newlines to spaces
- #[rstest]
- #[case(MarkdownEscapeContext::Heading, "Q1 * Results", r"Q1 \* Results")]
- #[case(MarkdownEscapeContext::Heading, "line1\nline2", "line1 line2")]
- #[case(MarkdownEscapeContext::LinkLabel, "a [nested] link", r"a \[nested\] link")]
- #[case(MarkdownEscapeContext::LinkLabel, "line1\nline2", "line1 line2")]
- fn test_markdown_escape_singleline(
- #[case] context: MarkdownEscapeContext,
- #[case] input: &str,
- #[case] expected: &str,
- ) {
- let result = markdown_escape(input, context).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test markdown_escape: table_cell context
- #[rstest]
- #[case("a | b", r"a \| b")]
- #[case("line1\nline2", "line1 line2")]
- #[case("**bold**", r"\*\*bold\*\*")]
- #[case(r"back\|slash", r"back\\\|slash")]
- #[case("plain", "plain")]
- fn test_markdown_escape_table_cell(#[case] input: &str, #[case] expected: &str) {
- let result = markdown_escape(input, MarkdownEscapeContext::TableCell).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test markdown_escape: code context is left unmodified (no backslash-escape mechanism
- // exists inside a code span/block; the caller must pick a longer fence instead)
- #[rstest]
- #[case("plain")]
- #[case("`nested`")]
- #[case("``` fence ```")]
- #[case("a | b\n* c *")]
- fn test_markdown_escape_code(#[case] input: &str) {
- let result = markdown_escape(input, MarkdownEscapeContext::Code).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(input.to_string())));
- }
-
- #[rstest]
- #[case("text", MarkdownEscapeContext::Text)]
- #[case("heading", MarkdownEscapeContext::Heading)]
- #[case("link_label", MarkdownEscapeContext::LinkLabel)]
- #[case("table_cell", MarkdownEscapeContext::TableCell)]
- #[case("code", MarkdownEscapeContext::Code)]
- fn test_markdown_escape_context_try_from_valid(#[case] input: &str, #[case] expected: MarkdownEscapeContext) {
- assert_eq!(MarkdownEscapeContext::try_from(input).unwrap(), expected);
- }
-
- #[rstest]
- #[case("")]
- #[case("Text")]
- #[case("paragraph")]
- fn test_markdown_escape_context_try_from_invalid(#[case] input: &str) {
- assert!(MarkdownEscapeContext::try_from(input).is_err());
- }
-
- // Round-trip: escaping a value and embedding it in a table row keeps the table's
- // column count and doesn't turn the value into a link/emphasis/heading.
- #[rstest]
- #[case("a | b\nc", "a \\| b c")]
- #[case("no special chars", "no special chars")]
- fn test_markdown_escape_table_cell_round_trip_fixture(#[case] input: &str, #[case] expected_cell: &str) {
- let escaped = markdown_escape(input, MarkdownEscapeContext::TableCell).unwrap();
- let RuntimeValue::String(escaped) = escaped else {
- panic!("Expected String")
- };
- assert_eq!(escaped.as_str(), expected_cell);
- let row = format!("| {} |", escaped);
- let unescaped_pipes = row
- .char_indices()
- .filter(|&(i, c)| c == '|' && !row[..i].ends_with('\\'))
- .count();
- assert_eq!(
- unescaped_pipes, 2,
- "row should have exactly 2 column delimiters: {row:?}"
- );
- }
-
- // Test strip_tags
- #[rstest]
- #[case("bold ", "bold")]
- #[case("Hello world !
", "Hello world!")]
- #[case("no tags here", "no tags here")]
- #[case(r#"link "#, "link")]
- #[case("text
", "text")]
- #[case("", "")]
- // A stray, unterminated `<` is treated as the start of a tag through the
- // end of the input, mirroring common strip_tags implementations.
- #[case("a < b", "a ")]
- #[case("a > b", "a > b")]
- fn test_strip_tags(#[case] input: &str, #[case] expected: &str) {
- let result = strip_tags(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test sanitize_html
- #[rstest]
- #[case("hi
", "hi
")]
- #[case(r#" "#, r#" "#)]
- #[case(
- r#"click "#,
- r#"click "#
- )]
- #[case("bold and italic ", "bold and italic ")]
- #[case("", "")]
- fn test_sanitize_html(#[case] input: &str, #[case] expected: &str) {
- let result = sanitize_html(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test shell escape
- #[rstest]
- #[case("hello", "hello")] // No escaping needed
- #[case("hello world", "'hello world'")] // Space needs quoting
- #[case("hello'world", "'hello'\\''world'")] // Single quote needs escaping
- #[case("hello\"world", "'hello\"world'")] // Double quote in single quotes
- #[case("hello$world", "'hello$world'")] // Dollar sign needs quoting
- #[case("hello`world`", "'hello`world`'")] // Backticks need quoting
- #[case("hello!world", "'hello!world'")] // Exclamation mark needs quoting
- #[case("hello&world", "'hello&world'")] // Ampersand needs quoting
- #[case("hello;world", "'hello;world'")] // Semicolon needs quoting
- #[case("hello|world", "'hello|world'")] // Pipe needs quoting
- #[case("helloworld", "'hello>world'")] // Greater than needs quoting
- #[case("hello(world)", "'hello(world)'")] // Parentheses need quoting
- #[case("hello[world]", "'hello[world]'")] // Brackets need quoting
- #[case("hello{world}", "'hello{world}'")] // Braces need quoting
- #[case("hello*world", "'hello*world'")] // Asterisk needs quoting
- #[case("hello?world", "'hello?world'")] // Question mark needs quoting
- #[case("hello\\world", "'hello\\world'")] // Backslash needs quoting
- #[case("hello\nworld", "'hello\nworld'")] // Newline needs quoting
- #[case("hello\tworld", "'hello\tworld'")] // Tab needs quoting
- #[case("", "''")] // Empty string
- #[case("hello-world", "hello-world")] // Hyphen is safe
- #[case("hello_world", "hello_world")] // Underscore is safe
- #[case("hello.world", "hello.world")] // Dot is safe
- #[case("hello,world", "hello,world")] // Comma is safe
- #[case("hello:world", "hello:world")] // Colon is safe
- #[case("hello@world", "hello@world")] // At sign is safe
- #[case("hello/world", "hello/world")] // Slash is safe
- #[case("hello=world", "hello=world")] // Equals is safe
- #[case("test123", "test123")] // Alphanumeric is safe
- #[case("TEST", "TEST")] // Uppercase is safe
- #[case("it's a test", "'it'\\''s a test'")] // Multiple issues
- #[case("rm -rf /", "'rm -rf /'")] // Dangerous command
- fn test_shell_escape(#[case] input: &str, #[case] expected: &str) {
- let result = shell_escape(input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test to_string
- #[rstest]
- #[case::string(RuntimeValue::String(Shared::new("test".to_string())), "test")]
- #[case::symbol(RuntimeValue::Symbol(Ident::new("test")), "test")]
- #[case::number(RuntimeValue::Number(42.into()), "42")]
- #[case::boolean_true(RuntimeValue::Boolean(true), "true")]
- #[case::boolean_false(RuntimeValue::Boolean(false), "false")]
- fn test_to_string(#[case] input: RuntimeValue, #[case] expected: &str) {
- let result = to_string(&input).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test to_number
- #[rstest]
- #[case::string_int(RuntimeValue::String(Shared::new("42".to_string())), RuntimeValue::Number(42.into()))]
- #[case::number(RuntimeValue::Number(42.into()), RuntimeValue::Number(42.into()))]
- #[case::boolean_true(RuntimeValue::Boolean(true), RuntimeValue::Number(1.into()))]
- #[case::boolean_false(RuntimeValue::Boolean(false), RuntimeValue::Number(0.into()))]
- #[case::none(RuntimeValue::None, RuntimeValue::Number(0.into()))]
- fn test_to_number(#[case] mut input: RuntimeValue, #[case] expected: RuntimeValue) {
- let result = to_number(&mut input).unwrap();
- assert_eq!(result, expected);
- }
-
- #[test]
- fn test_to_number_invalid_string() {
- let mut input = RuntimeValue::String(Shared::new("not a number".to_string()));
- let result = to_number(&mut input);
- assert!(result.is_err());
- }
-
- #[test]
- fn test_to_number_array() {
- let mut input = RuntimeValue::Array(crate::Shared::new(vec![
- RuntimeValue::String(Shared::new("42".to_string())),
- RuntimeValue::Boolean(true),
- ]));
- let result = to_number(&mut input).unwrap();
- match result {
- RuntimeValue::Array(arr) => {
- assert_eq!(arr.len(), 2);
- assert_eq!(arr[0], RuntimeValue::Number(42.into()));
- assert_eq!(arr[1], RuntimeValue::Number(1.into()));
- }
- _ => panic!("Expected Array"),
- }
- }
-
- // Test to_array
- #[rstest]
- #[case::string(
- RuntimeValue::String(Shared::new("abc".to_string())),
- vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::String(Shared::new("b".to_string())),
- RuntimeValue::String(Shared::new("c".to_string())),
- ]
- )]
- #[case::none(RuntimeValue::None, vec![])]
- #[case::number(RuntimeValue::Number(42.into()), vec![RuntimeValue::Number(42.into())])]
- #[case::boolean_true(RuntimeValue::Boolean(true), vec![RuntimeValue::Boolean(true)])]
- #[case::boolean_false(RuntimeValue::Boolean(false), vec![RuntimeValue::Boolean(false)])]
- fn test_to_array(#[case] mut input: RuntimeValue, #[case] expected: Vec) {
- let result = to_array(&mut input).unwrap();
- assert_eq!(result, RuntimeValue::Array(crate::Shared::new(expected)));
- }
-
- #[test]
- fn test_to_array_already_array() {
- let mut input = RuntimeValue::Array(crate::Shared::new(vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- ]));
- let result = to_array(&mut input).unwrap();
- assert_eq!(
- result,
- RuntimeValue::Array(crate::Shared::new(vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- ]))
- );
- }
-
- // Test to_text
- #[rstest]
- #[case::string(RuntimeValue::String(Shared::new("test".to_string())), "test")]
- #[case::number(RuntimeValue::Number(42.into()), "42")]
- #[case::none(RuntimeValue::None, "")]
- #[case::array(
- RuntimeValue::Array(crate::Shared::new(vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::String(Shared::new("b".to_string())),
- RuntimeValue::String(Shared::new("c".to_string())),
- ])),
- "a,b,c"
- )]
- #[case::array_with_none(
- RuntimeValue::Array(crate::Shared::new(vec![
- RuntimeValue::String(Shared::new("a".to_string())),
- RuntimeValue::None,
- RuntimeValue::String(Shared::new("c".to_string())),
- ])),
- "a,,c"
- )]
- #[case::empty_array(RuntimeValue::Array(crate::Shared::new(vec![])), "")]
- fn test_to_text(#[case] input: RuntimeValue, #[case] expected: &str) {
- let result = to_text(&input).unwrap();
- match result {
- RuntimeValue::None => assert_eq!(expected, ""),
- RuntimeValue::String(s) => assert_eq!(s.as_str(), expected),
- _ => panic!("Expected String or None"),
- }
- }
-
- // Test to_html
- #[rstest]
- #[case::plain_text(RuntimeValue::String(Shared::new("hello".to_string())), "hello
")]
- #[case::bold(RuntimeValue::String(Shared::new("**bold**".to_string())), "bold
")]
- #[case::italic(RuntimeValue::String(Shared::new("*italic*".to_string())), "italic
")]
- #[case::none(RuntimeValue::None, "")]
- #[case::heading(RuntimeValue::String(Shared::new("# Heading\n\nParagraph".to_string())), "Heading \nParagraph
")]
- #[case::quote(RuntimeValue::String(Shared::new("> Quote".to_string())), "\nQuote
\n ")]
- #[case::list(RuntimeValue::String(Shared::new("- Item 1\n- Item 2".to_string())), "")]
- fn test_to_html(#[case] input: RuntimeValue, #[case] expected: &str) {
- let result = to_html(&input).unwrap();
- match result {
- RuntimeValue::None => assert_eq!(expected, ""),
- RuntimeValue::String(s) => {
- // Trim trailing newline for comparison
- let trimmed = s.trim_end();
- assert_eq!(trimmed, expected);
- }
- _ => panic!("Expected String or None"),
- }
- }
-
- // Test flatten
- #[rstest]
- #[case::flat_array(
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())],
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]
- )]
- #[case::nested_array(
- vec![
- RuntimeValue::Array(crate::Shared::new(vec![RuntimeValue::Number(1.into())])),
- RuntimeValue::Number(2.into()),
- RuntimeValue::Array(crate::Shared::new(vec![RuntimeValue::Number(3.into()), RuntimeValue::Number(4.into())])),
- ],
- vec![
- RuntimeValue::Number(1.into()),
- RuntimeValue::Number(2.into()),
- RuntimeValue::Number(3.into()),
- RuntimeValue::Number(4.into()),
- ]
- )]
- #[case::deeply_nested(
- vec![
- RuntimeValue::Array(crate::Shared::new(vec![
- RuntimeValue::Array(crate::Shared::new(vec![RuntimeValue::Number(1.into())])),
- RuntimeValue::Number(2.into()),
- ])),
- ],
- vec![RuntimeValue::Number(1.into()), RuntimeValue::Number(2.into())]
- )]
- #[case::empty(vec![], vec![])]
- fn test_flatten(#[case] input: Vec, #[case] expected: Vec) {
- let result = flatten(input);
- assert_eq!(result, expected);
- }
-
- #[test]
- fn test_flatten_with_empty_nested() {
- let input = vec![
- RuntimeValue::Array(crate::Shared::new(vec![])),
- RuntimeValue::Number(1.into()),
- RuntimeValue::Array(crate::Shared::new(vec![])),
- ];
- let result = flatten(input);
- assert_eq!(result, vec![RuntimeValue::Number(1.into())]);
- }
-
- // Test from_date
- #[rstest]
- #[case("2024-01-01T00:00:00Z", 1704067200)]
- #[case("2024-06-15T12:30:45Z", 1718454645)]
- #[case("2024-12-31T23:59:59Z", 1735689599)]
- fn test_from_date_valid(#[case] input: &str, #[case] expected_secs: i64) {
- let result = from_date(input).unwrap();
- assert_eq!(result, RuntimeValue::Number(expected_secs.into()));
- }
-
- #[rstest]
- #[case("invalid date")]
- #[case("2024-13-01T00:00:00Z")]
- #[case("not-a-date")]
- fn test_from_date_invalid(#[case] input: &str) {
- let result = from_date(input);
- assert!(result.is_err());
- }
-
- // Test to_date
- #[rstest]
- #[case(1704067200, None, "2024-01-01T00:00:00Z")]
- #[case(1704067200, Some("%Y-%m-%d"), "2024-01-01")]
- #[case(1718454645, Some("%Y/%m/%d %H:%M"), "2024/06/15 12:30")]
- #[case(1704067200, Some("%d/%m/%Y"), "01/01/2024")]
- #[case(1704067200, Some("%H:%M:%S"), "00:00:00")]
- #[case(1704067200, Some("%Y"), "2024")]
- fn test_to_date(#[case] secs: i64, #[case] convert: Option<&str>, #[case] expected: &str) {
- let result = to_date(secs.into(), convert).unwrap();
- assert_eq!(result, RuntimeValue::String(Shared::new(expected.to_string())));
- }
-
- // Test to_markdown_string
- #[rstest]
- #[case::single_string(
- vec![RuntimeValue::String(Shared::new("hello".to_string()))],
- "hello"
- )]
- #[case::multiple_strings(
- vec![
- RuntimeValue::String(Shared::new("hello".to_string())),
- RuntimeValue::String(Shared::new(" ".to_string())),
- RuntimeValue::String(Shared::new("world".to_string())),
- ],
- "hello\n \nworld" // Each becomes a separate line
- )]
- fn test_to_markdown_string(#[case] input: Vec