summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLaurenz <laurmaedje@gmail.com>2021-01-13 11:54:50 +0100
committerLaurenz <laurmaedje@gmail.com>2021-01-13 11:54:50 +0100
commit539735e668f601058c2c71a847335e17fac107e8 (patch)
tree32ce04a99fd92a089625e4abdc09d2373687f0ab
parentd2ba1b705ed7a532266294aa100f19423bb07f4d (diff)
Basic let bindings 🎞
-rw-r--r--src/eval/mod.rs25
-rw-r--r--src/parse/mod.rs37
-rw-r--r--src/parse/parser.rs5
-rw-r--r--src/parse/tests.rs38
-rw-r--r--src/parse/tokens.rs6
-rw-r--r--src/syntax/expr.rs26
-rw-r--r--src/syntax/token.rs3
-rw-r--r--tests/typeset.rs4
8 files changed, 135 insertions, 9 deletions
diff --git a/src/eval/mod.rs b/src/eval/mod.rs
index 32a7d6ba..0e6fa79f 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<T>
+where
+ Spanned<&'a T>: Eval,
+{
+ type Output = <Spanned<&'a T> as Eval>::Output;
+
+ fn eval(self, ctx: &mut EvalContext) -> Self::Output {
+ self.as_ref().eval(ctx)
+ }
+}
+
impl Eval for &[Spanned<Node>] {
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 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<Node> {
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<Expr> {
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<Expr> {
+ 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));
diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs
index 09472df4..b758a849 100644
--- a/src/syntax/expr.rs
+++ b/src/syntax/expr.rs
@@ -44,6 +44,8 @@ pub enum Expr {
Group(Box<Expr>),
/// A block expression: `{1 + 2}`.
Block(Box<Expr>),
+ /// 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<Ident>,
+ /// The expression to assign to the pattern.
+ pub expr: Option<Box<Spanned<Expr>>>,
+}
+
+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 7055d61a..43415198 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 735a46c0..e586ae1a 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<Diag>, 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)
}