diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 32a7d6ba0..0e6fa79fd 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -46,6 +46,17 @@ pub trait Eval { fn eval(self, ctx: &mut EvalContext) -> Self::Output; } +impl<'a, T> Eval for &'a Spanned +where + Spanned<&'a T>: Eval, +{ + type Output = as Eval>::Output; + + fn eval(self, ctx: &mut EvalContext) -> Self::Output { + self.as_ref().eval(ctx) + } +} + impl Eval for &[Spanned] { type Output = (); @@ -163,6 +174,14 @@ impl Eval for Spanned<&Expr> { Expr::Template(v) => Value::Template(v.clone()), Expr::Group(v) => v.as_ref().with_span(self.span).eval(ctx), Expr::Block(v) => v.as_ref().with_span(self.span).eval(ctx), + Expr::Let(v) => { + let value = match &v.expr { + Some(expr) => expr.as_ref().eval(ctx), + None => Value::None, + }; + Rc::make_mut(&mut ctx.state.scope).set(v.pat.v.as_str(), value); + Value::None + } } } } @@ -171,7 +190,7 @@ impl Eval for Spanned<&ExprUnary> { type Output = Value; fn eval(self, ctx: &mut EvalContext) -> Self::Output { - let value = (*self.v.expr).as_ref().eval(ctx); + let value = self.v.expr.as_ref().eval(ctx); if let Value::Error = value { return Value::Error; @@ -189,8 +208,8 @@ impl Eval for Spanned<&ExprBinary> { type Output = Value; fn eval(self, ctx: &mut EvalContext) -> Self::Output { - let lhs = (*self.v.lhs).as_ref().eval(ctx); - let rhs = (*self.v.rhs).as_ref().eval(ctx); + let lhs = self.v.lhs.as_ref().eval(ctx); + let rhs = self.v.rhs.as_ref().eval(ctx); if lhs == Value::Error || rhs == Value::Error { return Value::Error; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index a66660e56..4483ed765 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -82,6 +82,11 @@ fn node(p: &mut Parser, at_start: bool) -> Option { Token::Raw(t) => Node::Raw(raw(p, t)), Token::UnicodeEscape(t) => Node::Text(unicode_escape(p, t)), + // Keywords. + Token::Let => { + return Some(Node::Expr(expr_let(p)?)); + } + // Comments. Token::LineComment(_) | Token::BlockComment(_) => { p.eat(); @@ -329,7 +334,7 @@ fn value(p: &mut Parser) -> Option { Some(Token::Angle(val, unit)) => Expr::Angle(val, unit), Some(Token::Percent(p)) => Expr::Percent(p), Some(Token::Hex(hex)) => Expr::Color(color(p, hex)), - Some(Token::Str(token)) => Expr::Str(str(p, token)), + Some(Token::Str(token)) => Expr::Str(string(p, token)), // No value. _ => { @@ -377,7 +382,7 @@ fn color(p: &mut Parser, hex: &str) -> RgbaColor { } /// Parse a string. -fn str(p: &mut Parser, token: TokenStr) -> String { +fn string(p: &mut Parser, token: TokenStr) -> String { if !token.terminated { p.diag_expected_at("quote", p.peek_span().end); } @@ -385,5 +390,33 @@ fn str(p: &mut Parser, token: TokenStr) -> String { resolve::resolve_string(token.string) } +/// Parse a let expresion. +fn expr_let(p: &mut Parser) -> Option { + p.push_mode(TokenMode::Code); + p.start_group(Group::Terminated); + p.eat_assert(Token::Let); + + 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)); + } + } + } else { + p.diag_expected("identifier"); + } + + while !p.eof() { + p.diag_unexpected(); + } + + p.pop_mode(); + p.end_group(); + pat.map(|pat| Expr::Let(ExprLet { pat, expr: rhs })) +} + #[cfg(test)] mod tests; diff --git a/src/parse/parser.rs b/src/parse/parser.rs index 0d3761df9..2b5fe7206 100644 --- a/src/parse/parser.rs +++ b/src/parse/parser.rs @@ -117,6 +117,7 @@ impl<'s> Parser<'s> { Group::Bracket => self.eat_assert(Token::LeftBracket), Group::Brace => self.eat_assert(Token::LeftBrace), Group::Subheader => {} + Group::Terminated => {} } self.groups.push(group); @@ -139,6 +140,7 @@ impl<'s> Parser<'s> { Group::Bracket => Some(Token::RightBracket), Group::Brace => Some(Token::RightBrace), Group::Subheader => None, + Group::Terminated => Some(Token::Semicolon), }; if let Some(token) = end { @@ -290,6 +292,7 @@ impl<'s> Parser<'s> { Some(Token::RightBracket) => Group::Bracket, Some(Token::RightBrace) => Group::Brace, Some(Token::Pipe) => Group::Subheader, + Some(Token::Semicolon) => Group::Terminated, _ => return, }) { self.peeked = None; @@ -316,4 +319,6 @@ pub enum Group { /// A group ended by a chained subheader or a closing bracket: /// `... >>`, `...]`. Subheader, + /// A group ended by a semicolon: `;`. + Terminated, } diff --git a/src/parse/tests.rs b/src/parse/tests.rs index b9a3d3015..9460db6b0 100644 --- a/src/parse/tests.rs +++ b/src/parse/tests.rs @@ -202,6 +202,21 @@ macro_rules! Block { }; } +macro_rules! Let { + (@$pat:expr $(=> $expr:expr)?) => {{ + #[allow(unused)] + let mut expr = None; + $(expr = Some(Box::new(into!($expr)));)? + Expr::Let(ExprLet { + pat: into!($pat).map(|s: &str| Ident(s.into())), + expr + }) + }}; + ($($tts:tt)*) => { + Node::Expr(Let!(@$($tts)*)) + }; +} + #[test] fn test_parse_comments() { // In markup. @@ -651,3 +666,26 @@ fn test_parse_values() { nodes: [], errors: [S(1..3, "expected expression, found invalid token")]); } + +#[test] +fn test_parse_let_bindings() { + // Basic let. + t!("#let x;" Let!("x")); + t!("#let _y=1;" Let!("_y" => Int(1))); + + // Followed by text. + t!("#let x = 1\n+\n2;\nHi there" + Let!("x" => Binary(Int(1), Add, Int(2))), + Space, Text("Hi"), Space, Text("there")); + + // Missing semicolon. + t!("#let x = a\nHi" + nodes: [Let!("x" => Id("a"))], + errors: [S(11..13, "unexpected identifier"), + S(13..13, "expected semicolon")]); + + // Missing identifier. + t!("#let 1;" + nodes: [], + errors: [S(5..6, "expected identifier, found integer")]) +} diff --git a/src/parse/tokens.rs b/src/parse/tokens.rs index d16cf2cee..32cc11d9c 100644 --- a/src/parse/tokens.rs +++ b/src/parse/tokens.rs @@ -60,7 +60,7 @@ impl<'s> Iterator for Tokens<'s> { loop { // Common elements. return Some(match c { - // Functions and blocks. + // Functions, blocks and terminators. '[' => Token::LeftBracket, ']' => Token::RightBracket, '{' => Token::LeftBrace, @@ -112,6 +112,7 @@ impl<'s> Iterator for Tokens<'s> { // Length one. ',' => Token::Comma, + ';' => Token::Semicolon, ':' => Token::Colon, '|' => Token::Pipe, '+' => Token::Plus, @@ -575,6 +576,7 @@ mod tests { fn test_tokenize_code_symbols() { // Test all symbols. t!(Code: "," => Comma); + t!(Code: ";" => Semicolon); t!(Code: ":" => Colon); t!(Code: "|" => Pipe); t!(Code: "+" => Plus); @@ -682,7 +684,7 @@ mod tests { // Test code symbols in text. t!(Markup[" /"]: "a():\"b" => Text("a():\"b")); - t!(Markup[" /"]: ":,=|/+-" => Text(":,=|/+-")); + t!(Markup[" /"]: ";:,=|/+-" => Text(";:,=|/+-")); // Test text ends. t!(Markup[""]: "hello " => Text("hello"), Space(0)); diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 09472df41..b758a8494 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -44,6 +44,8 @@ pub enum Expr { Group(Box), /// A block expression: `{1 + 2}`. Block(Box), + /// A let expression: `let x = 1`. + Let(ExprLet), } impl Pretty for Expr { @@ -79,6 +81,7 @@ impl Pretty for Expr { v.pretty(p); p.push_str("}"); } + Self::Let(v) => v.pretty(p), } } } @@ -300,6 +303,26 @@ impl Pretty for ExprDict { /// A template expression: `[*Hi* there!]`. pub type ExprTemplate = Tree; +/// A let expression: `let x = 1`. +#[derive(Debug, Clone, PartialEq)] +pub struct ExprLet { + /// The pattern to assign to. + pub pat: Spanned, + /// The expression to assign to the pattern. + pub expr: Option>>, +} + +impl Pretty for ExprLet { + fn pretty(&self, p: &mut Printer) { + p.push_str("#let "); + p.push_str(&self.pat.v); + if let Some(expr) = &self.expr { + p.push_str(" = "); + expr.v.pretty(p); + } + } +} + #[cfg(test)] mod tests { use super::super::tests::test_pretty; @@ -336,6 +359,9 @@ mod tests { // Parens and blocks. test_pretty("{(1)}", "{(1)}"); test_pretty("{{1}}", "{{1}}"); + + // Let binding. + test_pretty("#let x=1+2", "#let x = 1 + 2"); } #[test] diff --git a/src/syntax/token.rs b/src/syntax/token.rs index 7055d61a2..43415198c 100644 --- a/src/syntax/token.rs +++ b/src/syntax/token.rs @@ -27,6 +27,8 @@ pub enum Token<'s> { Backslash, /// A comma: `,`. Comma, + /// A semicolon: `;`. + Semicolon, /// A colon: `:`. Colon, /// A pipe: `|`. @@ -201,6 +203,7 @@ impl<'s> Token<'s> { Self::Tilde => "tilde", Self::Backslash => "backslash", Self::Comma => "comma", + Self::Semicolon => "semicolon", Self::Colon => "colon", Self::Pipe => "pipe", Self::Plus => "plus", diff --git a/tests/typeset.rs b/tests/typeset.rs index 735a46c0a..e586ae1a4 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -137,7 +137,7 @@ fn test( output: frames, feedback: Feedback { mut diags, .. }, } = typeset(&src, Rc::clone(env), state); - diags.sort(); + diags.sort_by_key(|d| d.span); let env = env.borrow(); let canvas = draw(&frames, &env, 2.0); @@ -215,7 +215,7 @@ fn parse_metadata(src: &str, map: &LineMap) -> (SpanVec, bool) { diags.push(Diag::new(level, s.rest().trim()).with_span(start .. end)); } - diags.sort(); + diags.sort_by_key(|d| d.span); (diags, compare_ref) }