From 84ba547c7c80e45cc8edafcde8714973bb2a3a2f Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 20 Jan 2021 21:33:13 +0100 Subject: [PATCH] =?UTF-8?q?If=20expressions=20=F0=9F=94=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/env.rs | 3 + src/eval/call.rs | 2 +- src/eval/mod.rs | 47 ++++++++++++--- src/eval/scope.rs | 2 +- src/layout/par.rs | 1 - src/layout/stack.rs | 1 - src/library/style.rs | 4 +- src/parse/collection.rs | 4 +- src/parse/mod.rs | 96 +++++++++++++++++++----------- src/parse/parser.rs | 102 ++++++++++++++++++++------------ src/parse/tokens.rs | 5 ++ src/syntax/expr.rs | 32 +++++++++- tests/lang/ref/if.png | Bin 0 -> 4352 bytes tests/lang/ref/let.png | Bin 1563 -> 3347 bytes tests/lang/typ/bracket-call.typ | 2 +- tests/lang/typ/headings.typ | 2 +- tests/lang/typ/if.typ | 66 +++++++++++++++++++++ tests/lang/typ/let.typ | 77 +++++++++++++----------- tests/library/typ/font.typ | 2 +- tests/library/typ/rgb.typ | 4 +- 20 files changed, 319 insertions(+), 133 deletions(-) create mode 100644 tests/lang/ref/if.png create mode 100644 tests/lang/typ/if.typ diff --git a/src/env.rs b/src/env.rs index 5746dd2f8..aaa04aa27 100644 --- a/src/env.rs +++ b/src/env.rs @@ -84,6 +84,9 @@ pub struct ImageResource { } impl ImageResource { + /// Parse an image resource from raw data in a supported format. + /// + /// The image format is determined automatically. pub fn parse(data: Vec) -> Option { let reader = ImageReader::new(Cursor::new(data)).with_guessed_format().ok()?; let format = reader.format()?; diff --git a/src/eval/call.rs b/src/eval/call.rs index 5b0628a8a..d57ed1441 100644 --- a/src/eval/call.rs +++ b/src/eval/call.rs @@ -20,7 +20,7 @@ impl Eval for Spanned<&ExprCall> { return returned; } else { let ty = value.type_name(); - ctx.diag(error!(span, "a value of type {} is not callable", ty)); + ctx.diag(error!(span, "expected function, found {}", ty)); } } else if !name.is_empty() { ctx.diag(error!(span, "unknown function")); diff --git a/src/eval/mod.rs b/src/eval/mod.rs index c7b87aefd..500436980 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -180,14 +180,8 @@ impl Eval for Spanned<&Expr> { Expr::Binary(v) => v.with_span(self.span).eval(ctx), Expr::Group(v) => v.as_ref().eval(ctx), Expr::Block(v) => v.as_ref().eval(ctx), - Expr::Let(v) => { - let value = match &v.expr { - Some(expr) => expr.as_ref().eval(ctx), - None => Value::None, - }; - ctx.scopes.define(v.pat.v.as_str(), value); - Value::None - } + Expr::Let(v) => v.with_span(self.span).eval(ctx), + Expr::If(v) => v.with_span(self.span).eval(ctx), } } } @@ -249,3 +243,40 @@ impl Eval for Spanned<&ExprBinary> { } } } + +impl Eval for Spanned<&ExprLet> { + type Output = Value; + + fn eval(self, ctx: &mut EvalContext) -> Self::Output { + let value = match &self.v.expr { + Some(expr) => expr.as_ref().eval(ctx), + None => Value::None, + }; + ctx.scopes.define(self.v.pat.v.as_str(), value); + Value::None + } +} + +impl Eval for Spanned<&ExprIf> { + type Output = Value; + + fn eval(self, ctx: &mut EvalContext) -> Self::Output { + let condition = self.v.condition.eval(ctx); + if let Value::Bool(boolean) = condition { + if boolean { + self.v.if_body.eval(ctx) + } else if let Some(expr) = &self.v.else_body { + expr.eval(ctx) + } else { + Value::None + } + } else { + ctx.diag(error!( + self.v.condition.span, + "expected boolean, found {}", + condition.type_name() + )); + Value::Error + } + } +} diff --git a/src/eval/scope.rs b/src/eval/scope.rs index 62ee7e402..a93de2695 100644 --- a/src/eval/scope.rs +++ b/src/eval/scope.rs @@ -42,7 +42,7 @@ pub struct Scope { } impl Scope { - // Create a new empty scope. + /// Create a new empty scope. pub fn new() -> Self { Self::default() } diff --git a/src/layout/par.rs b/src/layout/par.rs index 2a1ad941a..3f971e126 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -95,7 +95,6 @@ impl<'a> ParLayouter<'a> { while !self.areas.current.fits(frame.size) { if self.areas.in_full_last() { // TODO: Diagnose once the necessary spans exist. - let _ = warning!("cannot fit frame into any area"); break; } else { self.finish_area(); diff --git a/src/layout/stack.rs b/src/layout/stack.rs index bfb93a941..7d1d7a127 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -80,7 +80,6 @@ impl<'a> StackLayouter<'a> { while !self.areas.current.fits(frame.size) { if self.areas.in_full_last() { // TODO: Diagnose once the necessary spans exist. - let _ = warning!("cannot fit frame into any area"); break; } else { self.finish_area(); diff --git a/src/library/style.rs b/src/library/style.rs index 2e3484409..ee52c97c2 100644 --- a/src/library/style.rs +++ b/src/library/style.rs @@ -158,7 +158,7 @@ impl_type! { FontWeight: "font weight", Value::Int(number) => { let [min, max] = [Self::THIN, Self::BLACK]; - let message = || format!("must be between {:#?} and {:#?}", min, max); + let message = || format!("should be between {:#?} and {:#?}", min, max); return if number < i64::from(min.to_number()) { CastResult::Warn(min, message()) } else if number > i64::from(max.to_number()) { @@ -189,7 +189,7 @@ pub fn rgb(ctx: &mut EvalContext, args: &mut Args) -> Value { let mut clamp = |component: Option>, default| { component.map_or(default, |c| { if c.v < 0.0 || c.v > 1.0 { - ctx.diag(warning!(c.span, "must be between 0.0 and 1.0")); + ctx.diag(warning!(c.span, "should be between 0.0 and 1.0")); } (c.v.max(0.0).min(1.0) * 255.0).round() as u8 }) diff --git a/src/parse/collection.rs b/src/parse/collection.rs index 9addcef09..58fd91aee 100644 --- a/src/parse/collection.rs +++ b/src/parse/collection.rs @@ -11,7 +11,7 @@ pub fn arguments(p: &mut Parser) -> ExprArgs { /// - Dictionary literal /// - Parenthesized expression pub fn parenthesized(p: &mut Parser) -> Expr { - p.start_group(Group::Paren); + p.start_group(Group::Paren, TokenMode::Code); let state = if p.eat_if(Token::Colon) { collection(p, State::Dict(vec![])) } else { @@ -30,7 +30,7 @@ fn collection(p: &mut Parser, mut collection: T) -> T { collection.push_arg(p, arg); if let Some(pos) = missing_coma.take() { - p.diag_expected_at("comma", pos); + p.expected_at("comma", pos); } if p.eof() { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 0a6563667..622223fa3 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -78,9 +78,8 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { Token::UnicodeEscape(t) => Node::Text(unicode_escape(p, t)), // Keywords. - Token::Let => { - return Some(Node::Expr(expr_let(p)?)); - } + Token::Let => return Some(Node::Expr(stmt_let(p)?)), + Token::If => return Some(Node::Expr(expr_if(p)?)), // Comments. Token::LineComment(_) | Token::BlockComment(_) => { @@ -89,7 +88,7 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { } _ => { - p.diag_unexpected(); + p.unexpected(); return None; } }; @@ -111,7 +110,7 @@ fn heading(p: &mut Parser) -> NodeHeading { }); if level.v > 5 { - p.diag(warning!(level.span, "section depth should not exceed 6")); + p.diag(warning!(level.span, "should not exceed depth 6")); level.v = 5; } @@ -155,8 +154,7 @@ fn unicode_escape(p: &mut Parser, token: TokenUnicodeEscape) -> String { /// Parse a bracketed function call. fn bracket_call(p: &mut Parser) -> Expr { - p.push_mode(TokenMode::Code); - p.start_group(Group::Bracket); + p.start_group(Group::Bracket, TokenMode::Code); // One header is guaranteed, but there may be more (through chaining). let mut outer = vec![]; @@ -167,7 +165,6 @@ fn bracket_call(p: &mut Parser) -> Expr { inner = p.span(bracket_subheader); } - p.pop_mode(); p.end_group(); if p.peek() == Some(Token::LeftBracket) { @@ -189,15 +186,15 @@ fn bracket_call(p: &mut Parser) -> Expr { /// Parse one subheader of a bracketed function call. fn bracket_subheader(p: &mut Parser) -> ExprCall { - p.start_group(Group::Subheader); + p.start_group(Group::Subheader, TokenMode::Code); let start = p.next_start(); let name = p.span_if(ident).unwrap_or_else(|| { let what = "function name"; if p.eof() { - p.diag_expected_at(what, start); + p.expected_at(what, start); } else { - p.diag_expected(what); + p.expected(what); } Ident(String::new()).with_span(start) }); @@ -210,23 +207,19 @@ fn bracket_subheader(p: &mut Parser) -> ExprCall { /// Parse the body of a bracketed function call. fn bracket_body(p: &mut Parser) -> Tree { - p.push_mode(TokenMode::Markup); - p.start_group(Group::Bracket); + p.start_group(Group::Bracket, TokenMode::Markup); let tree = tree(p); - p.pop_mode(); p.end_group(); tree } /// Parse a block expression: `{...}`. fn block(p: &mut Parser) -> Option { - p.push_mode(TokenMode::Code); - p.start_group(Group::Brace); + p.start_group(Group::Brace, TokenMode::Code); let expr = p.span_if(expr); while !p.eof() { - p.diag_unexpected(); + p.unexpected(); } - p.pop_mode(); p.end_group(); Some(Expr::Block(Box::new(expr?))) } @@ -333,7 +326,7 @@ fn value(p: &mut Parser) -> Option { // No value. _ => { - p.diag_expected("expression"); + p.expected("expression"); return None; } }; @@ -343,17 +336,15 @@ fn value(p: &mut Parser) -> Option { // Parse a template value: `[...]`. fn template(p: &mut Parser) -> Expr { - p.push_mode(TokenMode::Markup); - p.start_group(Group::Bracket); + p.start_group(Group::Bracket, TokenMode::Markup); let tree = tree(p); - p.pop_mode(); p.end_group(); Expr::Template(tree) } /// Parse a parenthesized function call. fn paren_call(p: &mut Parser, name: Spanned) -> Expr { - p.start_group(Group::Paren); + p.start_group(Group::Paren, TokenMode::Code); let args = p.span(arguments); p.end_group(); Expr::Call(ExprCall { name, args }) @@ -379,36 +370,71 @@ fn color(p: &mut Parser, hex: &str) -> RgbaColor { /// Parse a string. fn string(p: &mut Parser, token: TokenStr) -> String { if !token.terminated { - p.diag_expected_at("quote", p.peek_span().end); + p.expected_at("quote", p.peek_span().end); } resolve::resolve_string(token.string) } -/// Parse a let expresion. -fn expr_let(p: &mut Parser) -> Option { - p.push_mode(TokenMode::Code); +/// Parse a let statement. +fn stmt_let(p: &mut Parser) -> Option { + p.start_group(Group::Stmt, TokenMode::Code); p.eat_assert(Token::Let); - p.start_group(Group::Expr); let pat = p.span_if(ident); let mut rhs = None; if pat.is_some() { if p.eat_if(Token::Eq) { - if let Some(expr) = p.span_if(expr) { - rhs = Some(Box::new(expr)); - } + rhs = p.span_if(expr); } } else { - p.diag_expected("identifier"); + p.expected("identifier"); } - p.pop_mode(); if !p.eof() { - p.diag_expected("semicolon or line break"); + p.expected_at("semicolon or line break", p.last_end()); } p.end_group(); - pat.map(|pat| Expr::Let(ExprLet { pat, expr: rhs })) + + Some(Expr::Let(ExprLet { pat: pat?, expr: rhs.map(Box::new) })) +} + +/// Parse an if expresion. +fn expr_if(p: &mut Parser) -> Option { + p.start_group(Group::Expr, TokenMode::Code); + p.eat_assert(Token::If); + let condition = p.span_if(expr); + p.end_group(); + + let condition = Box::new(condition?); + let if_body = Box::new(control_body(p)?); + let end = p.last_end(); + p.skip_white(); + + let else_body = if p.eat_if(Token::Else) { + control_body(p).map(Box::new) + } else { + p.jump(end); + None + }; + + Some(Expr::If(ExprIf { condition, if_body, else_body })) +} + +/// Parse a control flow body. +fn control_body(p: &mut Parser) -> Option> { + let start = p.last_end(); + p.skip_white(); + + match p.peek() { + Some(Token::LeftBracket) => Some(p.span(template)), + Some(Token::LeftBrace) => p.span_if(block), + _ => { + p.expected_at("body", start); + p.jump(start); + None + } + } } diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 8c27c8f7a..f9ced34f5 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -55,7 +55,7 @@ impl<'s> Parser<'s> { /// Eat the next token and add a diagnostic that it is not the expected /// `thing`. - pub fn diag_expected(&mut self, what: &str) { + pub fn expected(&mut self, what: &str) { let before = self.next_start; if let Some(found) = self.eat() { let after = self.last_end; @@ -66,17 +66,17 @@ impl<'s> Parser<'s> { found.name(), )); } else { - self.diag_expected_at(what, self.next_start); + self.expected_at(what, self.next_start); } } - /// Add a diagnostic that the `thing` was expected at the given position. - pub fn diag_expected_at(&mut self, what: &str, pos: Pos) { + /// Add a diagnostic that `what` was expected at the given position. + pub fn expected_at(&mut self, what: &str, pos: Pos) { self.diag(error!(pos, "expected {}", what)); } /// Eat the next token and add a diagnostic that it is unexpected. - pub fn diag_unexpected(&mut self) { + pub fn unexpected(&mut self) { let before = self.next_start; if let Some(found) = self.eat() { let after = self.last_end; @@ -89,21 +89,7 @@ impl<'s> Parser<'s> { self.feedback.decos.push(deco); } - /// Update the token mode and push the previous mode onto a stack. - pub fn push_mode(&mut self, mode: TokenMode) { - self.modes.push(self.tokens.mode()); - self.tokens.set_mode(mode); - } - - /// Pop the topmost token mode from the stack. - /// - /// # Panics - /// This panics if there is no mode on the stack. - pub fn pop_mode(&mut self) { - self.tokens.set_mode(self.modes.pop().expect("no pushed mode")); - } - - /// Continues parsing in a group. + /// Continue parsing in a group. /// /// When the end delimiter of the group is reached, all subsequent calls to /// `eat()` and `peek()` return `None`. Parsing can only continue with @@ -111,37 +97,55 @@ impl<'s> Parser<'s> { /// /// # Panics /// 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, mode: TokenMode) { + self.modes.push(self.tokens.mode()); + self.tokens.set_mode(mode); + self.groups.push(group); + self.repeek(); match group { Group::Paren => self.eat_assert(Token::LeftParen), Group::Bracket => self.eat_assert(Token::LeftBracket), Group::Brace => self.eat_assert(Token::LeftBrace), - Group::Expr => self.repeek(), - Group::Subheader => self.repeek(), + Group::Subheader => {} + Group::Stmt => {} + Group::Expr => {} } } - /// Ends the parsing of a group and returns the span of the whole group. + /// End the parsing of a group. /// /// # Panics /// This panics if no group was started. pub fn end_group(&mut self) { + let prev_mode = self.tokens.mode(); + self.tokens.set_mode(self.modes.pop().expect("no pushed mode")); + let group = self.groups.pop().expect("no started group"); self.repeek(); - let (end, required) = match group { - Group::Paren => (Token::RightParen, true), - Group::Bracket => (Token::RightBracket, true), - Group::Brace => (Token::RightBrace, true), - Group::Expr => (Token::Semicolon, false), - Group::Subheader => return, - }; + // Eat the end delimiter if there is one. + if let Some((end, required)) = match group { + Group::Paren => Some((Token::RightParen, true)), + Group::Bracket => Some((Token::RightBracket, true)), + Group::Brace => Some((Token::RightBrace, true)), + Group::Subheader => None, + Group::Stmt => Some((Token::Semicolon, false)), + Group::Expr => None, + } { + if self.next == Some(end) { + // Bump the delimeter and return. No need to rescan in this case. + self.bump(); + return; + } else if required { + self.diag(error!(self.next_start, "expected {}", end.name())); + } + } - if self.next == Some(end) { + // Rescan the peeked token if the mode changed. + if self.tokens.mode() != prev_mode { + self.tokens.jump(self.last_end); self.bump(); - } else if required { - self.diag(error!(self.next_start, "expected {}", end.name())); } } @@ -201,6 +205,18 @@ impl<'s> Parser<'s> { debug_assert_eq!(next, Some(t)); } + /// Skip whitespace and comment tokens. + pub fn skip_white(&mut self) { + while matches!( + self.peek(), + Some(Token::Space(_)) | + Some(Token::LineComment(_)) | + Some(Token::BlockComment(_)) + ) { + self.eat(); + } + } + /// Peek at the next token without consuming it. pub fn peek(&self) -> Option> { self.peeked @@ -243,6 +259,12 @@ impl<'s> Parser<'s> { self.last_end } + /// Jump to a position in the source string. + pub fn jump(&mut self, pos: Pos) { + self.tokens.jump(pos); + self.bump(); + } + /// Slice a part out of the source string. pub fn get(&self, span: impl Into) -> &'s str { self.tokens.scanner().get(span.into().to_range()) @@ -265,7 +287,7 @@ impl<'s> Parser<'s> { TokenMode::Code => loop { match self.next { Some(Token::Space(n)) => { - if n >= 1 && self.groups.last() == Some(&Group::Expr) { + if n >= 1 && self.groups.last() == Some(&Group::Stmt) { break; } } @@ -293,8 +315,8 @@ impl<'s> Parser<'s> { 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::Semicolon if self.groups.contains(&Group::Stmt) => {} + Token::Space(n) if n >= 1 && self.groups.last() == Some(&Group::Stmt) => {} Token::Pipe if self.groups.contains(&Group::Subheader) => {} _ => return, } @@ -319,9 +341,11 @@ pub enum Group { Bracket, /// A curly-braced group: `{...}`. Brace, - /// A group ended by a semicolon or a line break: `;`, `\n`. - Expr, /// A group ended by a chained subheader or a closing bracket: /// `... >>`, `...]`. Subheader, + /// A group ended by a semicolon or a line break: `;`, `\n`. + Stmt, + /// A group for a single expression. Not ended by something specific. + Expr, } diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index 68b31a876..312e941b9 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -42,6 +42,11 @@ impl<'s> Tokens<'s> { self.s.index().into() } + /// Jump to the given position. + pub fn jump(&mut self, pos: Pos) { + self.s.jump(pos.to_usize()); + } + /// The underlying scanner. pub fn scanner(&self) -> &Scanner<'s> { &self.s diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 79713cf11..29e143b24 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -46,6 +46,8 @@ pub enum Expr { Block(ExprBlock), /// A let expression: `let x = 1`. Let(ExprLet), + /// An if expression: `if x { y } else { z }`. + If(ExprIf), } impl Pretty for Expr { @@ -82,6 +84,7 @@ impl Pretty for Expr { p.push_str("}"); } Self::Let(v) => v.pretty(p), + Self::If(v) => v.pretty(p), } } } @@ -329,6 +332,30 @@ impl Pretty for ExprLet { } } +/// An if expression: `if x { y } else { z }`. +#[derive(Debug, Clone, PartialEq)] +pub struct ExprIf { + /// The pattern to assign to. + pub condition: Box>, + /// The expression to evaluate if the condition is true. + pub if_body: Box>, + /// The expression to evaluate if the condition is false. + pub else_body: Option>>, +} + +impl Pretty for ExprIf { + fn pretty(&self, p: &mut Printer) { + p.push_str("#if "); + self.condition.v.pretty(p); + p.push_str(" "); + self.if_body.v.pretty(p); + if let Some(expr) = &self.else_body { + p.push_str(" #else "); + expr.v.pretty(p); + } + } +} + #[cfg(test)] mod tests { use super::super::tests::test_pretty; @@ -366,8 +393,9 @@ mod tests { test_pretty("{(1)}", "{(1)}"); test_pretty("{{1}}", "{{1}}"); - // Let binding. - test_pretty("#let x=1+2", "#let x = 1 + 2"); + // Control flow. + test_pretty("#let x = 1+2", "#let x = 1 + 2"); + test_pretty("#if x [y] #else [z]", "#if x [y] #else [z]"); } #[test] diff --git a/tests/lang/ref/if.png b/tests/lang/ref/if.png new file mode 100644 index 0000000000000000000000000000000000000000..da99e185f1ed934c5d57d7906dcfc0e6818fb18a GIT binary patch literal 4352 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 literal 0 HcmV?d00001 diff --git a/tests/lang/ref/let.png b/tests/lang/ref/let.png index d9833391044b13db8e6a2c6f1db5339721813502..4b8a1131a7fe1d30a419d2aac0fefa84328a8786 100644 GIT binary patch literal 3347 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 Y+xjN?6O(Vh|CM&^uueAhRz9i!0@;KZ8UO$Q literal 1563 zcmai!e>l?#9LG2NvZX1^Vg1msB{2C`lvBT<+XI_t*8@Kkw)Ly#M<=&*$|zv49)hX)oSu-XFQL)zN(9Ihae`DqMz#ra~JbJy~Nd z zEKDZbz>bj9)fC9fRb8K*wmigMR3Nl8@g${p&uipWl+O0Qdi1ulsN?RJE+6=lIr zt1xB(K4vjX6-p9|t%A*`EYq;6&9wuIdq0A+`>bohg4DTBh9%WRxwh8QIXdL!y<*`< z^%rtKx<{LwdE<^-BSN?jbq+pNbXI$mf8Ca!F+E1t`d;q+`b|@IFfGLb_uKG86Wb7c zGeTgW6@LsYe6~N2lhst4l6}C#OnIvoTSe~lrIC3Zrzs^3&Eb^|j3smWI*s(Gft{|o zc)Y_99lqN!nX+u0461ci)|(5@RkFY%IcP5h=#r+S2F#7K5s0>z^jAPk?eW%x{3kwx zKR-Na;X^o;N=#I=@`nT8&b(e1n(On_>jb)cHyTM(g3}uZZW51IKF*6*6nrSbY%R!m zp>DY_qFHG%!=t^t>6uj7iBd%riLe-AOL%_Ls#&4@j{P{krh&D8dkR zj8;?s2gwMgy~r$_G=-bp)r(HcPkp1uG9vh8lRJv zDi-c4H_Q!td|QtyXp;-mk5t+#E&%_hvwJx0X>KJgt+i$ffkbWtB$nvo#^p~ z>JRpX8MOPd4CIlx;;`lAl(r6%y9i=JNH{OI0IX9_M&*y_Z_AwWDQg}aOLiw_aUEqa z=GBV6&>l!JoVgRnG^+F!6`OuvZ4-#Dp78g416kXFWlA?b0c<;m{@NWX{^Tms)Hn&c zW~%hJ4w{#`ih^7MLMFTVttHKFz0zA(6KYFYAt=AYhc~~bRv92|a~14x|aAJpi8OEIHHSOA0*;_}&uOw{TPlRT~7_R2csbgsv-cec+{ zkHybasSqcZeO*GCQ0JFxKEd0A^01iLZ3Iddf25;ZcRRpWF(20kC*)^z#<rYL~hQRlf+bplFGnQHR;E=wm3?I!&hC9b&mSpYoZ6MuLH6A;-8o5cxA-IqAQ zqAfXC7!Gwpo(dEuz#Yg-&Da;LFr7*`N(ql7f+n>|NOdOC;C^%n$OVY VNd7awrZ(_D)+dAz@p2qh^f#biwfO)5 diff --git a/tests/lang/typ/bracket-call.typ b/tests/lang/typ/bracket-call.typ index 7bbaeac18..4b92a4b8f 100644 --- a/tests/lang/typ/bracket-call.typ +++ b/tests/lang/typ/bracket-call.typ @@ -70,7 +70,7 @@ --- // Ref: false -// Error: 2:2-2:3 a value of type string is not callable +// Error: 2:2-2:3 expected function, found string #let x = "string" [x] diff --git a/tests/lang/typ/headings.typ b/tests/lang/typ/headings.typ index 765d03770..e63574ca3 100644 --- a/tests/lang/typ/headings.typ +++ b/tests/lang/typ/headings.typ @@ -6,7 +6,7 @@ ###### Six // Too many hashtags. -// Warning: 1:1-1:8 section depth should not exceed 6 +// Warning: 1:1-1:8 should not exceed depth 6 ####### Seven --- diff --git a/tests/lang/typ/if.typ b/tests/lang/typ/if.typ new file mode 100644 index 000000000..5f4c986a7 --- /dev/null +++ b/tests/lang/typ/if.typ @@ -0,0 +1,66 @@ +#let x = true + +// The two different bodies. +#if true [_1_,] #if x {"2"} + +// Braced condition is fine. +#if {true} {"3"} + +// Newline between body and else-clause. +#if false [] +#else [4] + +// Multiline (condition needs parens because it's terminated by the line break, +// just like the right-hand side of a let-binding). +#if + x +{ + "Fi" + "ve" +} + +// Spacing is somewhat delicated. We only want to have spacing in the output if +// there was whitespace before/after the full if-else statement. In particular, +// spacing after a simple if should be retained, but spacing between the first +// body and the else should be ignored. +a#if true[b]c \ +a#if true[b] c \ +a #if true{"b"}c \ +a #if true{"b"} c \ +a#if false [?] #else [b]c \ +a#if true {"b"} #else {"?"} c \ + +// Body not evaluated at all if condition is false. +#if false { dont-care-about-undefined-variables } + +--- +#let x = true + +// Needs condition. +// Error: 1:6-1:7 expected expression, found closing brace +a#if } + +// Needs if-body. +// Error: 2:7-2:7 expected body +// Error: 1:16-1:16 expected body +a#if x b#if (x)c + +// Needs if-body expression. +// Error: 1:12-1:12 expected expression +a#if true {} + +// Needs else-body. +// Error: 1:20-1:20 expected body +a#if true [b] #else c + +// Lone else. +// Error: 2:1-2:6 unexpected else keyword +// Error: 1:8-1:8 expected function name +#else [] + +// Condition must be boolean. If it isn't, neither branch is evaluated. +// Error: 1:5-1:14 expected boolean, found string +#if "a" + "b" { "nope" } #else { "nope" } + +// No coercing from empty array or or stuff like that. +// Error: 1:5-1:7 expected boolean, found array +#if () { "nope" } #else { "nope" } diff --git a/tests/lang/typ/let.typ b/tests/lang/typ/let.typ index 28fc6649b..3f8f5e0fa 100644 --- a/tests/lang/typ/let.typ +++ b/tests/lang/typ/let.typ @@ -1,5 +1,3 @@ -// Ref: false - // Automatically initialized with `none`. #let x [eq x, none] @@ -8,40 +6,7 @@ #let y = 1 [eq y, 1] -// Multiple bindings in one line. -#let x = "a"; #let y = "b"; [eq x + y, "ab"] - -// No name. -// Error: 1:6-1:7 expected identifier, found integer -#let 1 - ---- -// Terminated with just a line break. -#let v = "a" -First -[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. +// Initialize with template, not terminated by semicolon in template. #let v = [Hello; there] // Not terminated by line break due to parens. @@ -51,3 +16,43 @@ Third 3, ) [eq x, (1, 2, 3)] + +// Multiple bindings in one line. +#let x = "a"; #let y = "b"; [eq x + y, "ab"] + +// Invalid name. +// Error: 1:6-1:7 expected identifier, found integer +#let 1 + +// Terminated by end of line before binding name. +// Error: 1:5-1:5 expected identifier +#let +x = 5 + +// No name at all. +// Error: 1:11-1:11 expected identifier +The Fi#let;rst + +// Terminated with just a line break. +#let v = "a" +The Second [eq v, "a"] + +// Terminated with semicolon + line break. +#let v = "a"; +The Third [eq v, "a"] + +// Terminated with just a semicolon. +The#let v = "a"; Fourth [eq v, "a"] + +// Terminated by semicolon even though we are in a paren group. +// Error: 2:25-2:25 expected expression +// Error: 1:25-1:25 expected closing paren +The#let array = (1, 2 + ;Fifth [eq array, (1, 2)] + +// Not terminated. +// Error: 1:16-1:16 expected semicolon or line break +The#let v = "a"Sixth [eq v, "a"] + +// Not terminated. +// Error: 1:16-1:16 expected semicolon or line break +The#let v = "a" [eq v, "a"] Seventh diff --git a/tests/library/typ/font.typ b/tests/library/typ/font.typ index beaddd4e6..4be98941c 100644 --- a/tests/library/typ/font.typ +++ b/tests/library/typ/font.typ @@ -27,7 +27,7 @@ // Error: 1:43-1:44 expected font family or array of font families, found integer [font style: bold, weight: "thin", serif: 0] -// Warning: 1:15-1:19 must be between 100 and 900 +// Warning: 1:15-1:19 should be between 100 and 900 [font weight: 2700] // Error: 1:7-1:27 unexpected argument diff --git a/tests/library/typ/rgb.typ b/tests/library/typ/rgb.typ index 66d63d0de..f778b2d1f 100644 --- a/tests/library/typ/rgb.typ +++ b/tests/library/typ/rgb.typ @@ -4,8 +4,8 @@ // Alpha channel. [rgb 1.0, 0.0, 0.0, 0.5] -// Warning: 2:6-2:9 must be between 0.0 and 1.0 -// Warning: 1:11-1:15 must be between 0.0 and 1.0 +// Warning: 2:6-2:9 should be between 0.0 and 1.0 +// Warning: 1:11-1:15 should be between 0.0 and 1.0 [rgb -30, 15.5, 0.5] // Error: 1:6-1:10 missing argument: blue component