Assertions with [eq] + better tests 🩺

This commit is contained in:
Laurenz 2021-01-17 13:53:22 +01:00
parent cc5f14193c
commit 29be90bf95
31 changed files with 284 additions and 135 deletions

View File

@ -73,7 +73,7 @@ impl Args {
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
{ {
self.pos.iter_mut().find_map(move |slot| try_cast(ctx, slot)) (0 .. self.pos.len()).find_map(move |i| try_cast(ctx, &mut self.pos, i))
} }
/// Find and remove the first convertible positional argument, producing an /// Find and remove the first convertible positional argument, producing an
@ -97,7 +97,16 @@ impl Args {
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
{ {
self.pos.iter_mut().filter_map(move |slot| try_cast(ctx, slot)) let mut i = 0;
std::iter::from_fn(move || {
while i < self.pos.len() {
if let Some(val) = try_cast(ctx, &mut self.pos, i) {
return Some(val);
}
i += 1;
}
None
})
} }
/// Convert and remove the value for the given named argument, producing an /// Convert and remove the value for the given named argument, producing an
@ -163,17 +172,26 @@ where
/// Try to cast the value in the slot into `T`, putting it back if the /// Try to cast the value in the slot into `T`, putting it back if the
/// conversion fails. /// conversion fails.
fn try_cast<T>(ctx: &mut EvalContext, slot: &mut Spanned<Value>) -> Option<T> fn try_cast<T>(
ctx: &mut EvalContext,
vec: &mut Vec<Spanned<Value>>,
i: usize,
) -> Option<T>
where where
T: Cast<Spanned<Value>>, T: Cast<Spanned<Value>>,
{ {
// Replace with error placeholder when conversion works since error values // Replace with error placeholder when conversion works since error values
// are ignored when generating "unexpected argument" errors. // are ignored when generating "unexpected argument" errors.
let value = std::mem::replace(slot, Spanned::zero(Value::Error)); let slot = &mut vec[i];
let value = std::mem::replace(slot, Spanned::zero(Value::None));
let span = value.span; let span = value.span;
match T::cast(value) { match T::cast(value) {
CastResult::Ok(t) => Some(t), CastResult::Ok(t) => {
vec.remove(i);
Some(t)
}
CastResult::Warn(t, m) => { CastResult::Warn(t, m) => {
vec.remove(i);
ctx.diag(warning!(span, "{}", m)); ctx.diag(warning!(span, "{}", m));
Some(t) Some(t)
} }

View File

@ -117,7 +117,7 @@ fn heading(p: &mut Parser) -> NodeHeading {
// Parse the heading contents. // Parse the heading contents.
let mut contents = vec![]; let mut contents = vec![];
while p.check(|t| !matches!(t, Token::Space(n) if n > 0)) { while p.check(|t| !matches!(t, Token::Space(n) if n >= 1)) {
if let Some(node) = p.span_if(|p| node(p, &mut false)) { if let Some(node) = p.span_if(|p| node(p, &mut false)) {
contents.push(node); contents.push(node);
} }
@ -388,8 +388,8 @@ fn string(p: &mut Parser, token: TokenStr) -> String {
/// Parse a let expresion. /// Parse a let expresion.
fn expr_let(p: &mut Parser) -> Option<Expr> { fn expr_let(p: &mut Parser) -> Option<Expr> {
p.push_mode(TokenMode::Code); p.push_mode(TokenMode::Code);
p.start_group(Group::Terminated);
p.eat_assert(Token::Let); p.eat_assert(Token::Let);
p.start_group(Group::Expr);
let pat = p.span_if(ident); let pat = p.span_if(ident);
let mut rhs = None; let mut rhs = None;
@ -404,11 +404,11 @@ fn expr_let(p: &mut Parser) -> Option<Expr> {
p.diag_expected("identifier"); p.diag_expected("identifier");
} }
while !p.eof() { p.pop_mode();
p.diag_unexpected(); if !p.eof() {
p.diag_expected("semicolon or line break");
} }
p.pop_mode();
p.end_group(); p.end_group();
pat.map(|pat| Expr::Let(ExprLet { pat, expr: rhs })) pat.map(|pat| Expr::Let(ExprLet { pat, expr: rhs }))
} }

View File

@ -112,16 +112,14 @@ impl<'s> Parser<'s> {
/// # Panics /// # Panics
/// This panics if the next token does not start the given group. /// This panics if the next token does not start the given group.
pub fn start_group(&mut self, group: Group) { pub fn start_group(&mut self, group: Group) {
self.groups.push(group);
match group { match group {
Group::Paren => self.eat_assert(Token::LeftParen), Group::Paren => self.eat_assert(Token::LeftParen),
Group::Bracket => self.eat_assert(Token::LeftBracket), Group::Bracket => self.eat_assert(Token::LeftBracket),
Group::Brace => self.eat_assert(Token::LeftBrace), Group::Brace => self.eat_assert(Token::LeftBrace),
Group::Subheader => {} Group::Expr => self.repeek(),
Group::Terminated => {} Group::Subheader => self.repeek(),
} }
self.groups.push(group);
self.repeek();
} }
/// Ends the parsing of a group and returns the span of the whole group. /// Ends the parsing of a group and returns the span of the whole group.
@ -129,26 +127,21 @@ impl<'s> Parser<'s> {
/// # Panics /// # Panics
/// This panics if no group was started. /// This panics if no group was started.
pub fn end_group(&mut self) { pub fn end_group(&mut self) {
// Check that we are indeed at the end of the group.
debug_assert_eq!(self.peek(), None, "unfinished group");
let group = self.groups.pop().expect("no started group"); let group = self.groups.pop().expect("no started group");
self.repeek(); self.repeek();
let end = match group { let (end, required) = match group {
Group::Paren => Some(Token::RightParen), Group::Paren => (Token::RightParen, true),
Group::Bracket => Some(Token::RightBracket), Group::Bracket => (Token::RightBracket, true),
Group::Brace => Some(Token::RightBrace), Group::Brace => (Token::RightBrace, true),
Group::Subheader => None, Group::Expr => (Token::Semicolon, false),
Group::Terminated => Some(Token::Semicolon), Group::Subheader => return,
}; };
if let Some(token) = end { if self.next == Some(end) {
if self.next == Some(token) { self.bump();
self.bump(); } else if required {
} else { self.diag(error!(self.next_start, "expected {}", end.name()));
self.diag(error!(self.next_start, "expected {}", token.name()));
}
} }
} }
@ -169,7 +162,7 @@ impl<'s> Parser<'s> {
where where
F: FnOnce(&mut Self) -> Option<T>, F: FnOnce(&mut Self) -> Option<T>,
{ {
self.span(|p| f(p)).transpose() self.span(f).transpose()
} }
/// Consume the next token. /// Consume the next token.
@ -269,17 +262,21 @@ impl<'s> Parser<'s> {
match self.tokens.mode() { match self.tokens.mode() {
TokenMode::Markup => {} TokenMode::Markup => {}
TokenMode::Code => { TokenMode::Code => loop {
while matches!( match self.next {
self.next, Some(Token::Space(n)) => {
Some(Token::Space(_)) | if n >= 1 && self.groups.last() == Some(&Group::Expr) {
Some(Token::LineComment(_)) | break;
Some(Token::BlockComment(_)) }
) { }
self.next_start = self.tokens.pos(); Some(Token::LineComment(_)) => {}
self.next = self.tokens.next(); Some(Token::BlockComment(_)) => {}
_ => break,
} }
}
self.next_start = self.tokens.pos();
self.next = self.tokens.next();
},
} }
self.repeek(); self.repeek();
@ -287,16 +284,22 @@ impl<'s> Parser<'s> {
fn repeek(&mut self) { fn repeek(&mut self) {
self.peeked = self.next; self.peeked = self.next;
if self.groups.contains(&match self.next { let token = match self.next {
Some(Token::RightParen) => Group::Paren, Some(token) => token,
Some(Token::RightBracket) => Group::Bracket, None => return,
Some(Token::RightBrace) => Group::Brace, };
Some(Token::Pipe) => Group::Subheader,
Some(Token::Semicolon) => Group::Terminated, match token {
Token::RightParen if self.groups.contains(&Group::Paren) => {}
Token::RightBracket if self.groups.contains(&Group::Bracket) => {}
Token::RightBrace if self.groups.contains(&Group::Brace) => {}
Token::Semicolon if self.groups.contains(&Group::Expr) => {}
Token::Space(n) if n >= 1 && self.groups.last() == Some(&Group::Expr) => {}
Token::Pipe if self.groups.contains(&Group::Subheader) => {}
_ => return, _ => return,
}) {
self.peeked = None;
} }
self.peeked = None;
} }
} }
@ -316,9 +319,9 @@ pub enum Group {
Bracket, Bracket,
/// A curly-braced group: `{...}`. /// A curly-braced group: `{...}`.
Brace, Brace,
/// A group ended by a semicolon or a line break: `;`, `\n`.
Expr,
/// A group ended by a chained subheader or a closing bracket: /// A group ended by a chained subheader or a closing bracket:
/// `... >>`, `...]`. /// `... >>`, `...]`.
Subheader, Subheader,
/// A group ended by a semicolon: `;`.
Terminated,
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

BIN
tests/lang/ref/nbsp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
tests/lang/ref/text.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 731 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.3 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

View File

@ -1,6 +1,9 @@
// Empty. // Empty.
{()} {()}
// Not an array, just a parenthesized expression.
{(1)}
// One item and trailing comma. // One item and trailing comma.
{(-1,)} {(-1,)}
@ -13,21 +16,31 @@
, #003 , #003
,)} ,)}
// Missing closing paren.
// Error: 1:3-1:3 expected closing paren // Error: 1:3-1:3 expected closing paren
{(} {(}
// Error: 2:4-2:6 expected expression, found end of block comment // Not an array.
// Error: 1:4-1:4 expected comma // Error: 1:2-1:3 expected expression, found closing paren
{)}
// Missing comma and bad expression.
// Error: 2:4-2:4 expected comma
// Error: 1:4-1:6 expected expression, found end of block comment
{(1*/2)} {(1*/2)}
// Bad expression.
// Error: 1:6-1:8 expected expression, found invalid token // Error: 1:6-1:8 expected expression, found invalid token
{(1, 1u 2)} {(1, 1u 2)}
// Leading comma is not an expression.
// Error: 1:3-1:4 expected expression, found comma // Error: 1:3-1:4 expected expression, found comma
{(,1)} {(,1)}
// Missing expression makes named pair incomplete, making this an empty array.
// Error: 1:5-1:5 expected expression // Error: 1:5-1:5 expected expression
{(a:)} {(a:)}
// Named pair after this is already identified as an array.
// Error: 1:6-1:10 expected expression, found named pair // Error: 1:6-1:10 expected expression, found named pair
{(1, b: 2)} {(1, b: 2)}

View File

@ -1,14 +0,0 @@
Hello 🌏!
_Emph_ and *strong*!
The non-breaking~space does not work.
// Directly after word.
Line\ Break
// Spaces around.
Line \ Break
// Directly before word does not work.
No \Break

View File

@ -1,21 +1,26 @@
// Basic expression.
{1} {1}
// Function calls.
{f(1)}
{[[f 1]]}
// Error: 1:2-1:2 expected expression // Error: 1:2-1:2 expected expression
{} {}
// Bad expression.
// Error: 1:2-1:4 expected expression, found invalid token // Error: 1:2-1:4 expected expression, found invalid token
{1u} {1u}
// Two expressions are not allowed.
// Error: 1:4-1:5 unexpected integer
{2 3}
// Missing closing brace in nested block.
// Error: 1:5-1:5 expected closing brace // Error: 1:5-1:5 expected closing brace
{({1) + 2} {({1) + 2}
// Error: 1:12-1:12 expected closing bracket // Missing closing bracket in template expression.
{[*] + [ok*} // Error: 1:11-1:11 expected closing bracket
{[_] + [4_}
// Opening brace is ignored after one expression is parsed.
// Error: 2:4-2:5 unexpected hex value // Error: 2:4-2:5 unexpected hex value
// Error: 1:5-1:6 unexpected opening brace // Error: 1:5-1:6 unexpected opening brace
{1 #{} _end_ {5 #{}*.*

View File

@ -1,18 +1,19 @@
// Test whether line comment acts as spacing. // Line comment acts as spacing.
A// you A// you
B B
// Test whether block comment acts as spacing. // Block comment does not act as spacing.
C/* C/*
/* */ /* */
*/D */D
// Test in expressions. // Works in headers.
[f /*1*/ a: "b" // [f /*1*/ a: "b" //
, 1] , 1]
// End should not appear without start.
// Error: 1:7-1:9 unexpected end of block comment // Error: 1:7-1:9 unexpected end of block comment
/* */ */ /* */ */
// Unterminated block comment is okay. // Unterminated is okay.
/* /*

View File

@ -4,9 +4,11 @@
// Two pairs. // Two pairs.
{(one: 1, two: 2)} {(one: 1, two: 2)}
// Simple expression after this is already identified as a dictionary.
// Error: 1:9-1:10 expected named pair, found expression // Error: 1:9-1:10 expected named pair, found expression
{(a: 1, b)} {(a: 1, b)}
// Identified as dictionary by initial colon.
// Error: 4:4-4:5 expected named pair, found expression // Error: 4:4-4:5 expected named pair, found expression
// Error: 3:5-3:5 expected comma // Error: 3:5-3:5 expected comma
// Error: 2:12-2:16 expected identifier // Error: 2:12-2:16 expected identifier

View File

@ -0,0 +1,11 @@
// Basic.
_Emph_ and *strong*!
// Inside of words.
Pa_rtl_y emphasized or str*ength*ened.
// Scoped to body.
[box][*Sco_ped] to body.
// Unterminated is fine.
_The End

View File

@ -18,8 +18,10 @@
// Escaped escape sequence. // Escaped escape sequence.
\u{41} vs. \\u\{41\} \u{41} vs. \\u\{41\}
// Unicode codepoint does not exist.
// Error: 1:1-1:11 invalid unicode escape sequence // Error: 1:1-1:11 invalid unicode escape sequence
\u{FFFFFF} \u{FFFFFF}
// Unterminated.
// Error: 1:6-1:6 expected closing brace // Error: 1:6-1:6 expected closing brace
\u{41*Bold* \u{41*Bold*

View File

@ -1,37 +1,43 @@
// Ref: false
#let a = 2; #let a = 2;
#let b = 4; #let b = 4;
// Paren call.
[eq f(1), "f(1)"]
[eq type(1), "integer"]
// Unary operations. // Unary operations.
{+1} [eq +1, 1]
{-1} [eq -1, 1-2]
{--1} [eq --1, 1]
// Binary operations. // Binary operations.
{"a"+"b"} [eq "a" + "b", "ab"]
{1-2} [eq 1-4, 3*-1]
{a * b} [eq a * b, 8]
{12pt/.4} [eq 12pt/.4, 30pt]
// Associativity. // Associativity.
{1+2+3} [eq 1+2+3, 6]
{1/2*3} [eq 1/2*3, 1.5]
// Precedence. // Precedence.
{1+2*-3} [eq 1+2*-3, -5]
// Parentheses. // Parentheses.
{(a)} [eq (a), 2]
{(2)} [eq (2), 2]
{(1+2)*3} [eq (1+2)*3, 9]
// Confusion with floating-point literal. // Confusion with floating-point literal.
{1e+2-1e-2} [eq 1e+2-1e-2, 99.99]
// Error: 1:3-1:3 expected expression // Error: 1:3-1:3 expected expression
{-} {-}
// Error: 1:4-1:4 expected expression // Error: 1:8-1:8 expected expression
{1+} [eq {1+}, 1]
// Error: 1:4-1:4 expected expression // Error: 1:8-1:8 expected expression
{2*} [eq {2*}, 2]

View File

@ -1,38 +1,48 @@
// Test different numbers of hashtags. // Test different numbers of hashtags.
// Valid levels.
# One # One
### Three ### Three
###### Six ###### Six
// Too many hashtags.
// Warning: 1:1-1:8 section depth should not exceed 6 // Warning: 1:1-1:8 section depth should not exceed 6
####### Seven ####### Seven
--- ---
// Test heading vs. no heading. // Test heading vs. no heading.
// Parsed as headings if at start of the context.
/**/ # Heading /**/ # Heading
{[## Heading]} {[## Heading]}
[box][### Heading] [box][### Heading]
\# No heading // Not at the start of the context.
Text with # hashtag Text with # hashtag
Nr#1 // Escaped.
\# No heading
// Keyword.
// Error: 1:1-1:6 unexpected invalid token // Error: 1:1-1:6 unexpected invalid token
#nope #nope
// Not parsed as a keyword, but neither as a heading.
Nr#1
--- ---
// Heading continuation over linebreak. // Heading continuation over linebreak.
// Code blocks continue heading.
# This { # This {
"works" "continues"
} }
// Function call continues heading.
# [box][ # [box][
This This,
] too ] too
// Without some kind of block, headings end at a line break.
# This # This
does not not

View File

@ -1,20 +1,53 @@
// Ref: false
// Automatically initialized with `none`. // Automatically initialized with `none`.
#let x; #let x
{(x,)} [eq x, none]
// Can start with underscore. // Initialized with `1`.
#let _y=1; #let y = 1;
{_y} [eq y, 1]
// Multiline. // Multiple bindings in one line.
#let z = "world" #let x = "a"; #let y = "b"; [eq x + y, "ab"]
+ " 🌏"; Hello, {z}!
// No name.
// Error: 1:6-1:7 expected identifier, found integer // Error: 1:6-1:7 expected identifier, found integer
#let 1; #let 1
// Error: 4:1-4:3 unexpected identifier ---
// Error: 3:4-3:9 unexpected identifier // Terminated with just a line break.
// Error: 3:1-3:1 expected semicolon #let v = "a"
#let x = "" First
Hi there [eq v, "a"]
// Terminated with just a semicolon.
#let v = "a"; Second
[eq v, "a"]
// Terminated with semicolon + line break.
#let v = "a";
Third
[eq v, "a"]
// Terminated by semicolon even though we are in a paren group.
// Error: 2:22-2:22 expected expression
// Error: 1:22-1:22 expected closing paren
#let array = (1, 2 + ;Fourth
[eq array, (1, 2)]
// Not terminated.
// Error: 1:14-1:20 expected semicolon or line break, found identifier
#let v = "a" Unseen Fifth
[eq v, "a"]
// Not terminated by semicolon in template.
#let v = [Hello; there]
// Not terminated by line break due to parens.
#let x = (
1,
2,
3,
)
[eq x, (1, 2, 3)]

View File

@ -0,0 +1,19 @@
// Leading line break.
\ Leading
// Directly after word.
Line\ Break
// Spaces around.
Line \ Break
// Directly before word does not work.
No \Break
// Trailing before paragraph break.
Paragraph 1 \
Paragraph 2
// Trailing before end of document.
Paragraph 3 \

2
tests/lang/typ/nbsp.typ Normal file
View File

@ -0,0 +1,2 @@
// Parsed correctly, but not actually doing anything at the moment.
The non-breaking~space does not work.

View File

@ -1,23 +1,28 @@
[font 8pt]
// Typst syntax inside.
`#let x = 1``[f 1]`
// Space between "rust" and "let" is trimmed.
The keyword ``rust let``. The keyword ``rust let``.
`#let x = 1` // Trimming depends on number backticks.
`[f 1]` <` untrimmed `> \
<`` trimmed ``>
---
[font 6pt]
// Multiline trimming.
``py ``py
import this import this
def say_hi(): def say_hi():
print("Hello World!") print("Hi!")
`` ``
--- // Lots of backticks inside.
```` ````
```backticks``` ```backticks```
```` ````
--- // Unterminated.
// Error: 2:1-2:1 expected backtick(s) // Error: 2:1-2:1 expected backtick(s)
`endless `endless

1
tests/lang/typ/text.typ Normal file
View File

@ -0,0 +1 @@
Hello 🌏!

View File

@ -1,13 +1,24 @@
// Test representation of values in the document.
#let name = "Typst"; #let name = "Typst";
#let ke-bab = "Kebab!"; #let ke-bab = "Kebab!";
#let α = "Alpha"; #let α = "Alpha";
// Variables.
{name} \ {name} \
{ke-bab} \ {ke-bab} \
{α} \ {α} \
// Error: 1:1-1:4 unknown variable
{_}
// Literal values.
{none} (empty) \ {none} (empty) \
{true} \ {true} \
{false} \ {false} \
// Numerical values.
{1} \
{1.0e-4} \ {1.0e-4} \
{3.15} \ {3.15} \
{1e-10} \ {1e-10} \
@ -17,17 +28,16 @@
{12e1pt} \ {12e1pt} \
{2.5rad} \ {2.5rad} \
{45deg} \ {45deg} \
{"hi"} \
{"a\n[]\"\u{1F680}string"} \
{#f7a20500} \
{[*{"Hi"} [f 1]*]} \
{{1}}
// Error: 1:1-1:4 unknown variable // Colors.
{_} \ {#f7a20500} \
// Error: 1:2-1:5 invalid color // Error: 1:2-1:5 invalid color
{#a5} {#a5}
// Error: 1:2-1:4 expected expression, found invalid token // Strings and escaping.
{1u} {"hi"} \
{"a\n[]\"\u{1F680}string"} \
// Templates.
{[*{"Hi"} [f 1]*]}

View File

@ -1,7 +1,9 @@
use std::cell::RefCell;
use std::env; use std::env;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::rc::Rc;
use fontdock::fs::FsIndex; use fontdock::fs::FsIndex;
use image::{GenericImageView, Rgba}; use image::{GenericImageView, Rgba};
@ -180,7 +182,8 @@ fn test_part(i: usize, src: &str, env: &mut Env) -> (bool, Vec<Frame>) {
let (compare_ref, ref_diags) = parse_metadata(src, &map); let (compare_ref, ref_diags) = parse_metadata(src, &map);
let mut scope = library::new(); let mut scope = library::new();
register_helpers(&mut scope); let panicked = Rc::new(RefCell::new(false));
register_helpers(&mut scope, Rc::clone(&panicked));
// We want to have "unbounded" pages, so we allow them to be infinitely // We want to have "unbounded" pages, so we allow them to be infinitely
// large and fit them to match their content. // large and fit them to match their content.
@ -201,6 +204,10 @@ fn test_part(i: usize, src: &str, env: &mut Env) -> (bool, Vec<Frame>) {
diags.sort_by_key(|d| d.span); diags.sort_by_key(|d| d.span);
let mut ok = true; let mut ok = true;
if *panicked.borrow() {
ok = false;
}
if diags != ref_diags { if diags != ref_diags {
println!(" Subtest {} does not match expected diagnostics. ❌", i); println!(" Subtest {} does not match expected diagnostics. ❌", i);
ok = false; ok = false;
@ -259,7 +266,7 @@ fn parse_metadata(src: &str, map: &LineMap) -> (bool, SpanVec<Diag>) {
(compare_ref, diags) (compare_ref, diags)
} }
fn register_helpers(scope: &mut Scope) { fn register_helpers(scope: &mut Scope, panicked: Rc<RefCell<bool>>) {
pub fn f(_: &mut EvalContext, args: &mut Args) -> Value { pub fn f(_: &mut EvalContext, args: &mut Args) -> Value {
let (array, dict) = args.drain(); let (array, dict) = args.drain();
let iter = array let iter = array
@ -281,7 +288,22 @@ fn register_helpers(scope: &mut Scope) {
Value::Str(p.finish()) Value::Str(p.finish())
} }
let eq = move |ctx: &mut EvalContext, args: &mut Args| -> Value {
let lhs = args.require::<Value>(ctx, "left-hand side");
let rhs = args.require::<Value>(ctx, "right-hand side");
if lhs != rhs {
*panicked.borrow_mut() = true;
println!(" Assertion failed ❌");
println!(" left: {:?}", lhs);
println!(" right: {:?}", rhs);
Value::Str(format!("(panic)"))
} else {
Value::None
}
};
scope.set("f", ValueFunc::new("f", f)); scope.set("f", ValueFunc::new("f", f));
scope.set("eq", ValueFunc::new("eq", eq));
} }
fn print_diag(diag: &Spanned<Diag>, map: &LineMap) { fn print_diag(diag: &Spanned<Diag>, map: &LineMap) {