From 539735e668f601058c2c71a847335e17fac107e8 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 13 Jan 2021 11:54:50 +0100 Subject: =?UTF-8?q?Basic=20let=20bindings=20=F0=9F=8E=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/parse/mod.rs | 37 +++++++++++++++++++++++++++++++++++-- src/parse/parser.rs | 5 +++++ src/parse/tests.rs | 38 ++++++++++++++++++++++++++++++++++++++ src/parse/tokens.rs | 6 ++++-- 4 files changed, 82 insertions(+), 4 deletions(-) (limited to 'src/parse') diff --git a/src/parse/mod.rs b/src/parse/mod.rs index a66660e5..4483ed76 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 0d3761df..2b5fe720 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 b9a3d301..9460db6b 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 d16cf2ce..32cc11d9 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)); -- cgit v1.2.3