diff options
| author | Laurenz <laurmaedje@gmail.com> | 2020-01-16 17:51:04 +0100 |
|---|---|---|
| committer | Laurenz <laurmaedje@gmail.com> | 2020-01-16 17:51:04 +0100 |
| commit | 08b91a265fcda74f5463473938ec33873b49a7f7 (patch) | |
| tree | 747ac6a0b385a14a4aa5adbc3f21ef7b9653bd78 /tests | |
| parent | 15ad30555bdad8e7b192fdcf7d4543c0d3fb18ce (diff) | |
Powerful parser testing 🐱👤
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/layouter/coma.typ (renamed from tests/layouts/coma.typ) | 0 | ||||
| -rw-r--r-- | tests/layouter/stack.typ (renamed from tests/layouts/stack.typ) | 0 | ||||
| -rw-r--r-- | tests/parse.rs | 236 | ||||
| -rw-r--r-- | tests/parser/tokens.rs (renamed from tests/parsing/tokens.rs) | 8 | ||||
| -rw-r--r-- | tests/parser/trees.rs | 33 | ||||
| -rw-r--r-- | tests/parsing/trees.rs | 20 | ||||
| -rw-r--r-- | tests/src/layouter.rs (renamed from tests/layout.rs) | 9 | ||||
| -rw-r--r-- | tests/src/parser.rs | 311 | ||||
| -rw-r--r-- | tests/src/render.py (renamed from tests/render.py) | 14 | ||||
| -rw-r--r-- | tests/src/spanless.rs | 62 |
10 files changed, 423 insertions, 270 deletions
diff --git a/tests/layouts/coma.typ b/tests/layouter/coma.typ index 14f639b5..14f639b5 100644 --- a/tests/layouts/coma.typ +++ b/tests/layouter/coma.typ diff --git a/tests/layouts/stack.typ b/tests/layouter/stack.typ index cbca41dc..cbca41dc 100644 --- a/tests/layouts/stack.typ +++ b/tests/layouter/stack.typ diff --git a/tests/parse.rs b/tests/parse.rs deleted file mode 100644 index 616f4d70..00000000 --- a/tests/parse.rs +++ /dev/null @@ -1,236 +0,0 @@ -#![allow(unused_imports)] -#![allow(dead_code)] -#![allow(non_snake_case)] - -use typstc::func::Scope; -use typstc::size::Size; -use typstc::syntax::*; -use typstc::{function, parse}; - - -mod token_shorthands { - pub use super::Token::{ - Whitespace as W, - LineComment as LC, BlockComment as BC, StarSlash as SS, - LeftBracket as LB, RightBracket as RB, - LeftParen as LP, RightParen as RP, - LeftBrace as LBR, RightBrace as RBR, - Colon as CL, Comma as CM, Equals as EQ, - ExprIdent as ID, ExprStr as STR, ExprSize as SIZE, - ExprNumber as NUM, ExprBool as BOOL, - Star as ST, Underscore as U, Backtick as B, Text as T, - }; -} - -mod node_shorthands { - use super::Node; - pub use Node::{ - Space as S, Newline as N, Text, - ToggleItalic as I, ToggleBolder as B, ToggleMonospace as M, - Func, - }; - pub fn T(text: &str) -> Node { Node::Text(text.to_string()) } -} - -macro_rules! F { - (@body None) => (None); - (@body Some([$($tts:tt)*])) => ({ - let nodes = vec![$($tts)*].into_iter() - .map(|v| Spanned { v, span: Span::ZERO }) - .collect(); - - Some(SyntaxTree { nodes }) - }); - - ($($body:tt)*) => ({ - Func(FuncCall(Box::new(DebugFn { - pos: vec![], - key: vec![], - body: F!(@body $($body)*), - }))) - }); -} - -function! { - #[derive(Debug, PartialEq)] - pub struct DebugFn { - pos: Vec<Spanned<Expression>>, - key: Vec<Pair>, - body: Option<SyntaxTree>, - } - - parse(args, body, ctx) { - DebugFn { - pos: args.iter_pos().collect(), - key: args.iter_keys().collect(), - body: parse!(optional: body, ctx), - } - } - - layout() { vec![] } -} - -impl DebugFn { - fn compare(&self, other: &DebugFn) -> bool { - self.pos.iter().zip(&other.pos).all(|(a, b)| a.v == b.v) - && self.key.iter().zip(&other.key) - .all(|(a, b)| a.key.v == b.key.v && a.value.v == b.value.v) - && match (&self.body, &other.body) { - (Some(a), Some(b)) => compare(a, b), - (None, None) => true, - _ => false, - } - } -} - -fn downcast(func: &FuncCall) -> &DebugFn { - func.0.downcast::<DebugFn>().expect("not a debug fn") -} - -fn compare(a: &SyntaxTree, b: &SyntaxTree) -> bool { - for (x, y) in a.nodes.iter().zip(&b.nodes) { - use node_shorthands::*; - let same = match (&x.v, &y.v) { - (S, S) | (N, N) | (I, I) | (B, B) | (M, M) => true, - (Text(t1), Text(t2)) => t1 == t2, - (Func(f1), Func(f2)) => { - downcast(f1).compare(downcast(f2)) - } - _ => false, - }; - - if !same { return false; } - } - true -} - -/// Parses the test syntax. -macro_rules! tokens { - ($($task:ident $src:expr =>($line:expr)=> [$($tts:tt)*])*) => ({ - #[allow(unused_mut)] - let mut cases = Vec::new(); - $(cases.push(($line, $src, tokens!(@$task [$($tts)*])));)* - cases - }); - - (@t [$($tts:tt)*]) => ({ - use token_shorthands::*; - Target::Tokenize(vec![$($tts)*]) - }); - - (@ts [$($tts:tt)*]) => ({ - use token_shorthands::*; - Target::TokenizeSpanned(tokens!(@__spans [$($tts)*])) - }); - - (@p [$($tts:tt)*]) => ({ - use node_shorthands::*; - - let nodes = vec![$($tts)*].into_iter() - .map(|v| Spanned { v, span: Span::ZERO }) - .collect(); - - Target::Parse(SyntaxTree { nodes }) - }); - - (@ps [$($tts:tt)*]) => ({ - use node_shorthands::*; - Target::ParseSpanned(tokens!(@__spans [$($tts)*])) - }); - - (@__spans [$(($sl:tt:$sc:tt, $el:tt:$ec:tt, $v:expr)),* $(,)?]) => ({ - vec![ - $(Spanned { v: $v, span: Span { - start: Position { line: $sl, column: $sc }, - end: Position { line: $el, column: $ec }, - }}),* - ] - }); -} - -#[derive(Debug)] -enum Target { - Tokenize(Vec<Token<'static>>), - TokenizeSpanned(Vec<Spanned<Token<'static>>>), - Parse(SyntaxTree), - ParseSpanned(SyntaxTree), -} - -fn main() { - let tests = include!("cache/parse"); - let mut errors = false; - - let len = tests.len(); - println!(); - println!("Running {} test{}", len, if len > 1 { "s" } else { "" }); - - // Go through all test files. - for (file, cases) in tests.into_iter() { - print!("Testing: {}. ", file); - - let mut okay = 0; - let mut failed = 0; - - // Go through all tests in a test file. - for (line, src, target) in cases.into_iter() { - let (correct, expected, found) = test_case(src, target); - - // Check whether the tokenization works correctly. - if correct { - okay += 1; - } else { - if failed == 0 { - println!(); - } - - println!(" - Case failed in file {}.rs in line {}.", file, line); - println!(" - Source: {:?}", src); - println!(" - Expected: {:?}", expected); - println!(" - Found: {:?}", found); - println!(); - - failed += 1; - errors = true; - } - } - - // Print a small summary. - print!("{} okay, {} failed.", okay, failed); - if failed == 0 { - print!(" ✔") - } - println!(); - } - - println!(); - - if errors { - std::process::exit(-1); - } -} - -fn test_case(src: &str, target: Target) -> (bool, String, String) { - match target { - Target::Tokenize(tokens) => { - let found: Vec<_> = tokenize(src).map(Spanned::value).collect(); - (found == tokens, format!("{:?}", tokens), format!("{:?}", found)) - } - - Target::TokenizeSpanned(tokens) => { - let found: Vec<_> = tokenize(src).collect(); - (found == tokens, format!("{:?}", tokens), format!("{:?}", found)) - } - - Target::Parse(tree) => { - let scope = Scope::with_debug::<DebugFn>(); - let (found, _, errs) = parse(src, ParseContext { scope: &scope }); - (compare(&tree, &found), format!("{:?}", tree), format!("{:?}", found)) - } - - Target::ParseSpanned(tree) => { - let scope = Scope::with_debug::<DebugFn>(); - let (found, _, _) = parse(src, ParseContext { scope: &scope }); - (tree == found, format!("{:?}", tree), format!("{:?}", found)) - } - } -} diff --git a/tests/parsing/tokens.rs b/tests/parser/tokens.rs index 14f4e521..fb48b32e 100644 --- a/tests/parsing/tokens.rs +++ b/tests/parser/tokens.rs @@ -41,13 +41,13 @@ t "[a: true, x=1]" => [LB, ID("a"), CL, W(0), BOOL(true), CM, W(0), t "[120%]" => [LB, NUM(1.2), RB] // Body only tokens. -t "_*`" => [U, ST, B] -t "[func]*bold*" => [LB, ID("func"), RB, ST, T("bold"), ST] +t "_*`" => [U, S, B] +t "[func]*bold*" => [LB, ID("func"), RB, S, T("bold"), S] t "[_*`]" => [LB, T("_"), T("*"), T("`"), RB] t "hi_you_ there" => [T("hi"), U, T("you"), U, W(0), T("there")] // Nested functions. -t "[f: [=][*]]" => [LB, ID("f"), CL, W(0), LB, EQ, RB, LB, ST, RB, RB] +t "[f: [=][*]]" => [LB, ID("f"), CL, W(0), LB, EQ, RB, LB, S, RB, RB] t "[_][[,],]," => [LB, T("_"), RB, LB, LB, CM, RB, T(","), RB, T(",")] t "[=][=][=]" => [LB, EQ, RB, LB, T("="), RB, LB, EQ, RB] t "[=][[=][=][=]]" => [LB, EQ, RB, LB, LB, EQ, RB, LB, T("="), RB, LB, EQ, RB, RB] @@ -75,6 +75,6 @@ ts "[a=10]" => [(0:0, 0:1, LB), (0:1, 0:2, ID("a")), (0:2, 0:3, EQ), (0:3, 0:5, NUM(10.0)), (0:5, 0:6, RB)] ts r#"[x = "(1)"]*"# => [(0:0, 0:1, LB), (0:1, 0:2, ID("x")), (0:2, 0:3, W(0)), (0:3, 0:4, EQ), (0:4, 0:5, W(0)), (0:5, 0:10, STR("(1)")), - (0:10, 0:11, RB), (0:11, 0:12, ST)] + (0:10, 0:11, RB), (0:11, 0:12, S)] ts "// ab\r\n\nf" => [(0:0, 0:5, LC(" ab")), (0:5, 2:0, W(2)), (2:0, 2:1, T("f"))] ts "/*b*/_" => [(0:0, 0:5, BC("b")), (0:5, 0:6, U)] diff --git a/tests/parser/trees.rs b/tests/parser/trees.rs new file mode 100644 index 00000000..442f71dd --- /dev/null +++ b/tests/parser/trees.rs @@ -0,0 +1,33 @@ +p "" => [] +p "hi" => [T("hi")] +p "hi you" => [T("hi"), S, T("you")] +p "❤\n\n 🌍" => [T("❤"), N, T("🌍")] + +p "[func]" => [func!("func"; None)] +p "[tree][hi *you*]" => [func!("tree"; Some([T("hi"), S, B, T("you"), B]))] + +p "from [align: left] to" => [ + T("from"), S, func!("align", pos: [ID("left")]; None), S, T("to"), +] + +p "[box: x=1.2pt, false][a b c] bye" => [ + func!( + "box", + pos: [BOOL(false)], + key: ["x" => SIZE(Size::pt(1.2))]; + Some([T("a"), S, T("b"), S, T("c")]) + ), + S, T("bye"), +] + +c "hi" => [] +c "[align: left][\n _body_\n]" => [ + (0:0, 0:1, B), + (0:1, 0:6, FN), + (0:6, 0:7, CL), + (0:8, 0:12, ID), + (0:12, 0:13, B), + (0:13, 0:14, B), + (1:4, 1:10, IT), + (2:0, 2:2, B), +] diff --git a/tests/parsing/trees.rs b/tests/parsing/trees.rs deleted file mode 100644 index 78b16828..00000000 --- a/tests/parsing/trees.rs +++ /dev/null @@ -1,20 +0,0 @@ -p "" => [] -p "hi" => [T("hi")] -p "hi you" => [T("hi"), S, T("you")] -p "❤\n\n 🌍" => [T("❤"), N, T("🌍")] -p "[func]" => [F!(None)] -p "[tree][hi *you*]" => [F!(Some([T("hi"), S, B, T("you"), B]))] -// p "from [align: left] to" => [ -// T("from"), S, -// F!("align", pos=[ID("left")], None), -// S, T("to"), -// ] -// p "[box: x=1.2pt, false][a b c] bye" => [ -// F!( -// "box", -// pos=[BOOL(false)], -// key=["x": SIZE(Size::pt(1.2))], -// Some([T("a"), S, T("b"), S, T("c")]), -// ), -// S, T("bye"), -// ] diff --git a/tests/layout.rs b/tests/src/layouter.rs index 007b3c3f..6d38666b 100644 --- a/tests/layout.rs +++ b/tests/src/layouter.rs @@ -15,16 +15,17 @@ use typstc::style::PageStyle; use typstc::toddle::query::FileSystemFontProvider; use typstc::export::pdf::PdfExporter; -type Result<T> = std::result::Result<T, Box<dyn Error>>; -fn main() -> Result<()> { +type DynResult<T> = Result<T, Box<dyn Error>>; + +fn main() -> DynResult<()> { let opts = Options::parse(); create_dir_all("tests/cache/serial")?; create_dir_all("tests/cache/render")?; create_dir_all("tests/cache/pdf")?; - let tests: Vec<_> = read_dir("tests/layouts/")?.collect(); + let tests: Vec<_> = read_dir("tests/layouter/")?.collect(); let mut filtered = Vec::new(); for entry in tests { @@ -62,7 +63,7 @@ fn main() -> Result<()> { } /// Create a _PDF_ with a name from the source code. -fn test(name: &str, src: &str) -> Result<()> { +fn test(name: &str, src: &str) -> DynResult<()> { println!("Testing: {}.", name); let mut typesetter = Typesetter::new(); diff --git a/tests/src/parser.rs b/tests/src/parser.rs new file mode 100644 index 00000000..ecf1544c --- /dev/null +++ b/tests/src/parser.rs @@ -0,0 +1,311 @@ +use std::fmt::Debug; + +use typstc::func::Scope; +use typstc::size::Size; +use typstc::syntax::*; +use typstc::{function, parse}; + +mod spanless; +use spanless::SpanlessEq; + + +/// The result of a single test case. +enum Case { + Okay, + Failed { + line: usize, + src: &'static str, + expected: String, + found: String, + } +} + +/// Test all tests. +fn test(tests: Vec<(&str, Vec<Case>)>) { + println!(); + + let mut errors = false; + + let len = tests.len(); + println!("Running {} test{}", len, if len > 1 { "s" } else { "" }); + + for (file, cases) in tests { + print!("Testing: {}. ", file); + + let mut okay = 0; + let mut failed = 0; + + for case in cases { + match case { + Case::Okay => okay += 1, + Case::Failed { line, src, expected, found } => { + println!(); + println!(" - Case failed in file {}.rs in line {}.", file, line); + println!(" - Source: {:?}", src); + println!(" - Expected: {}", expected); + println!(" - Found: {}", found); + + failed += 1; + } + } + } + + // Print a small summary. + print!("{} okay, {} failed.", okay, failed); + if failed == 0 { + print!(" ✔") + } else { + errors = true; + } + + println!(); + } + + println!(); + + if errors { + std::process::exit(-1); + } +} + +/// The main test macro. +macro_rules! tokens { + ($($task:ident $src:expr =>($line:expr)=> [$($e:tt)*])*) => ({ + vec![$({ + let (okay, expected, found) = case!($task $src, [$($e)*]); + if okay { + Case::Okay + } else { + Case::Failed { + line: $line, + src: $src, + expected: format(expected), + found: format(found), + } + } + }),*] + }); +} + +//// Indented formatting for failed cases. +fn format(thing: impl Debug) -> String { + format!("{:#?}", thing).replace('\n', "\n ") +} + +/// Evaluates a single test. +macro_rules! case { + (t $($rest:tt)*) => (case!(@tokenize SpanlessEq::spanless_eq, $($rest)*)); + (ts $($rest:tt)*) => (case!(@tokenize PartialEq::eq, $($rest)*)); + + (@tokenize $cmp:expr, $src:expr, [$($e:tt)*]) => ({ + let expected = list!(tokens [$($e)*]); + let found = tokenize($src).collect::<Vec<_>>(); + ($cmp(&found, &expected), expected, found) + }); + + (p $($rest:tt)*) => (case!(@parse SpanlessEq::spanless_eq, $($rest)*)); + (ps $($rest:tt)*) => (case!(@parse PartialEq::eq, $($rest)*)); + + (@parse $cmp:expr, $src:expr, [$($e:tt)*]) => ({ + let expected = SyntaxTree { nodes: list!(nodes [$($e)*]) }; + let found = parse($src, ParseContext { scope: &scope() }).0; + ($cmp(&found, &expected), expected, found) + }); + + (c $src:expr, [$($e:tt)*]) => ({ + let expected = Colorization { tokens: list!(colors [$($e)*]) }; + let found = parse($src, ParseContext { scope: &scope() }).1; + (expected == found, expected, found) + }); + + (e $src:expr, [$($e:tt)*]) => ({ + let expected = ErrorMap { errors: list!([$($e)*]) }; + let found = parse($src, ParseContext { scope: &scope() }).2; + (expected == found, expected, found) + }); +} + +/// A scope containing the `DebugFn` as a fallback. +fn scope() -> Scope { + Scope::with_debug::<DebugFn>() +} + +/// Parses possibly-spanned lists of token or node expressions. +macro_rules! list { + (expr [$($item:expr),* $(,)?]) => ({ + #[allow(unused_imports)] + use cuts::expr::*; + Tuple { items: vec![$(zspan($item)),*] } + }); + + (expr [$($key:expr =>($_:expr)=> $value:expr),* $(,)?]) => ({ + #[allow(unused_imports)] + use cuts::expr::*; + Object { + pairs: vec![$(Pair { + key: zspan(Ident($key.to_string())), + value: zspan($value), + }),*] + } + }); + + ($cut:ident [$($e:tt)*]) => ({ + #[allow(unused_imports)] + use cuts::$cut::*; + list!([$($e)*]) + }); + + ([$(($sl:tt:$sc:tt, $el:tt:$ec:tt, $v:expr)),* $(,)?]) => ({ + vec![ + $(Spanned { v: $v, span: Span { + start: Position { line: $sl, column: $sc }, + end: Position { line: $el, column: $ec }, + }}),* + ] + }); + + ([$($e:tt)*]) => (vec![$($e)*].into_iter().map(zspan).collect::<Vec<_>>()); +} + +/// Composes a function expression. +macro_rules! func { + ($name:expr $(,pos: [$($p:tt)*])? $(,key: [$($k:tt)*])?; $($b:tt)*) => ({ + #![allow(unused_mut, unused_assignments)] + + let mut positional = Tuple::new(); + let mut keyword = Object::new(); + + $(positional = list!(expr [$($p)*]);)? + $(keyword = list!(expr [$($k)*]);)? + + Node::Func(FuncCall(Box::new(DebugFn { + header: FuncHeader { + name: zspan(Ident($name.to_string())), + args: FuncArgs { + positional, + keyword, + }, + }, + body: func!(@body $($b)*), + }))) + }); + + (@body Some($($b:tt)*)) => (Some(SyntaxTree { nodes: list!(nodes $($b)*) })); + (@body None) => (None); +} + +function! { + /// Most functions in the tests are parsed into the debug function for easy + /// inspection of arguments and body. + #[derive(Debug, PartialEq)] + pub struct DebugFn { + header: FuncHeader, + body: Option<SyntaxTree>, + } + + parse(header, body, ctx) { + DebugFn { + header: header.clone(), + body: parse!(optional: body, ctx), + } + } + + layout() { vec![] } +} + +/// Span an element with a zero span. +fn zspan<T>(v: T) -> Spanned<T> { + Spanned { v, span: Span::ZERO } +} + +/// Abbreviations for tokens, nodes, colors and expressions. +#[allow(non_snake_case, dead_code)] +mod cuts { + pub mod tokens { + pub use typstc::syntax::Token::{ + Whitespace as W, + LineComment as LC, + BlockComment as BC, + StarSlash as SS, + LeftBracket as LB, + RightBracket as RB, + LeftParen as LP, + RightParen as RP, + LeftBrace as LBR, + RightBrace as RBR, + Colon as CL, + Comma as CM, + Equals as EQ, + ExprIdent as ID, + ExprStr as STR, + ExprSize as SIZE, + ExprNumber as NUM, + ExprBool as BOOL, + Star as S, + Underscore as U, + Backtick as B, + Text as T, + }; + } + + pub mod nodes { + use typstc::syntax::Node; + + pub use Node::{ + Space as S, + Newline as N, + ToggleItalic as I, + ToggleBolder as B, + ToggleMonospace as M, + }; + + pub fn T(text: &str) -> Node { + Node::Text(text.to_string()) + } + } + + pub mod colors { + pub use typstc::syntax::ColorToken::{ + Comment as C, + Bracket as B, + FuncName as FN, + Colon as CL, + Key as K, + Equals as EQ, + Comma as CM, + Paren as P, + Brace as BR, + ExprIdent as ID, + ExprStr as STR, + ExprNumber as NUM, + ExprSize as SIZE, + ExprBool as BOOL, + Bold as BD, + Italic as IT, + Monospace as MS, + Invalid as INV, + }; + } + + pub mod expr { + use typstc::syntax::{Expression, Ident}; + + pub use Expression::{ + Number as NUM, + Size as SIZE, + Bool as BOOL, + }; + + pub fn ID(text: &str) -> Expression { + Expression::Ident(Ident(text.to_string())) + } + + pub fn STR(text: &str) -> Expression { + Expression::Str(text.to_string()) + } + } +} + +fn main() { + test(include!("../cache/parser-tests.rs")) +} diff --git a/tests/render.py b/tests/src/render.py index 1387ed53..bb27e973 100644 --- a/tests/render.py +++ b/tests/src/render.py @@ -7,7 +7,7 @@ from PIL import Image, ImageDraw, ImageFont BASE = os.path.dirname(__file__) -CACHE = os.path.join(BASE, 'cache/') +CACHE = os.path.join(BASE, '../cache/') SERIAL = os.path.join(CACHE, 'serial/') RENDER = os.path.join(CACHE, 'render/') @@ -98,16 +98,18 @@ class MultiboxRenderer: class BoxRenderer: - def __init__(self, fonts, width, height): + def __init__(self, fonts, width, height, grid=False): self.fonts = fonts self.size = (pix(width), pix(height)) img = Image.new('RGBA', self.size, (255, 255, 255, 255)) pixels = numpy.array(img) - # for i in range(0, int(height)): - # for j in range(0, int(width)): - # if ((i // 2) % 2 == 0) == ((j // 2) % 2 == 0): - # pixels[4*i:4*(i+1), 4*j:4*(j+1)] = (225, 225, 225, 255) + + if grid: + for i in range(0, int(height)): + for j in range(0, int(width)): + if ((i // 2) % 2 == 0) == ((j // 2) % 2 == 0): + pixels[4*i:4*(i+1), 4*j:4*(j+1)] = (225, 225, 225, 255) self.img = Image.fromarray(pixels, 'RGBA') self.draw = ImageDraw.Draw(self.img) diff --git a/tests/src/spanless.rs b/tests/src/spanless.rs new file mode 100644 index 00000000..fde5a2ed --- /dev/null +++ b/tests/src/spanless.rs @@ -0,0 +1,62 @@ +use super::*; + + +/// Compares elements by only looking at values and ignoring spans. +pub trait SpanlessEq<T> { + fn spanless_eq(&self, other: &T) -> bool; +} + +impl SpanlessEq<Vec<Spanned<Token<'_>>>> for Vec<Spanned<Token<'_>>> { + fn spanless_eq(&self, other: &Vec<Spanned<Token>>) -> bool { + self.len() == other.len() + && self.iter().zip(other).all(|(x, y)| x.v == y.v) + } +} + +impl SpanlessEq<SyntaxTree> for SyntaxTree { + fn spanless_eq(&self, other: &SyntaxTree) -> bool { + fn downcast(func: &FuncCall) -> &DebugFn { + func.0.downcast::<DebugFn>().expect("not a debug fn") + } + + self.nodes.len() == other.nodes.len() + && self.nodes.iter().zip(&other.nodes).all(|(x, y)| match (&x.v, &y.v) { + (Node::Func(a), Node::Func(b)) => downcast(a).spanless_eq(downcast(b)), + (a, b) => a == b, + }) + } +} + +impl SpanlessEq<DebugFn> for DebugFn { + fn spanless_eq(&self, other: &DebugFn) -> bool { + self.header.name.v == other.header.name.v + && self.header.args.positional.spanless_eq(&other.header.args.positional) + && self.header.args.keyword.spanless_eq(&other.header.args.keyword) + } +} + +impl SpanlessEq<Expression> for Expression { + fn spanless_eq(&self, other: &Expression) -> bool { + match (self, other) { + (Expression::Tuple(a), Expression::Tuple(b)) => a.spanless_eq(b), + (Expression::Object(a), Expression::Object(b)) => a.spanless_eq(b), + (a, b) => a == b, + } + } +} + +impl SpanlessEq<Tuple> for Tuple { + fn spanless_eq(&self, other: &Tuple) -> bool { + self.items.len() == other.items.len() + && self.items.iter().zip(&other.items) + .all(|(x, y)| x.v.spanless_eq(&y.v)) + } +} + +impl SpanlessEq<Object> for Object { + fn spanless_eq(&self, other: &Object) -> bool { + self.pairs.len() == other.pairs.len() + && self.pairs.iter().zip(&other.pairs) + .all(|(x, y)| x.key.v == y.key.v && x.value.v.spanless_eq(&y.value.v)) + } +} |
