Skip to content

Commit

Permalink
Support list and map literals
Browse files Browse the repository at this point in the history
  • Loading branch information
rossmacarthur committed Jan 10, 2025
1 parent cbc56d5 commit d7b8c3c
Show file tree
Hide file tree
Showing 9 changed files with 322 additions and 6 deletions.
84 changes: 82 additions & 2 deletions src/compile/lex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ pub struct Lexer<'engine, 'source> {

/// A buffer to store the next token.
next: Option<(Token, Span)>,

/// The stack of delimiters within a block.
delimiters: Vec<(Span, Token)>,
}

/// The state of the lexer.
Expand Down Expand Up @@ -89,6 +92,14 @@ pub enum Token {
BeginComment,
/// End block tag, e.g. `#}`
EndComment,
/// `[`
OpenBracket,
/// `]`
CloseBracket,
/// `{`
OpenBrace,
/// `}`
CloseBrace,
/// `.`
Dot,
/// `?.`
Expand Down Expand Up @@ -127,6 +138,7 @@ impl<'engine, 'source> Lexer<'engine, 'source> {
state: State::Template,
left_trim: false,
next: None,
delimiters: Vec::new(),
}
}

Expand Down Expand Up @@ -244,6 +256,10 @@ impl<'engine, 'source> Lexer<'engine, 'source> {
if tk != end {
return Err(self.err_unexpected_token(tk, i..j));
}
// Check for unclosed delimiters.
if let Some((open, end)) = self.delimiters.pop() {
return Err(self.err_unclosed(open, end));
}

// A matching end tag! Update the state and
// return the token.
Expand All @@ -270,6 +286,36 @@ impl<'engine, 'source> Lexer<'engine, 'source> {
'+' => (Token::Plus, i + 1),
'-' => (Token::Minus, i + 1),

// Delimiters
'[' => {
let sp = Span::from(i..(i + c.len_utf8()));
self.delimiters.push((sp, Token::CloseBracket));
(Token::OpenBracket, i + 1)
}
']' => {
let (_, close) = self.delimiters.pop().unwrap();
if close != Token::CloseBracket {
return Err(self
.err_unexpected_token(Token::CloseBracket, i..(i + c.len_utf8())));
}
(Token::CloseBracket, i + 1)
}

'{' => {
let sp = Span::from(i..(i + c.len_utf8()));
self.delimiters.push((sp, Token::CloseBrace));
(Token::OpenBrace, i + 1)
}
'}' => {
let (_, close) = self.delimiters.pop().unwrap();
if close != Token::CloseBrace {
return Err(
self.err_unexpected_token(Token::CloseBrace, i..(i + c.len_utf8()))
);
}
(Token::CloseBrace, i + 1)
}

// Multi-character tokens with a distinct start character.
'?' => self.lex_question_dot(iter, i)?,
'"' => self.lex_string(iter, i)?,
Expand Down Expand Up @@ -464,6 +510,10 @@ impl Token {
Self::EndBlock => "end block",
Self::BeginComment => "begin comment",
Self::EndComment => "end comment",
Self::OpenBracket => "open bracket",
Self::CloseBracket => "close bracket",
Self::OpenBrace => "open brace",
Self::CloseBrace => "close brace",
Self::Dot => "member access operator",
Self::QuestionDot => "optional member access operator",
Self::Pipe => "pipe",
Expand All @@ -480,7 +530,7 @@ impl Token {
}
}

/// Returns the corresponding tag if this token is a tag.
/// Returns the corresponding tag if this token is a tag or delimiter.
fn pair(&self) -> Self {
match self {
Self::BeginExpr => Self::EndExpr,
Expand All @@ -489,7 +539,11 @@ impl Token {
Self::EndBlock => Self::BeginBlock,
Self::BeginComment => Self::EndComment,
Self::EndComment => Self::BeginComment,
_ => panic!("not a tag"),
Self::OpenBracket => Self::CloseBracket,
Self::CloseBracket => Self::OpenBracket,
Self::OpenBrace => Self::CloseBrace,
Self::CloseBrace => Self::OpenBrace,
_ => panic!("not a tag or delimiter"),
}
}

Expand Down Expand Up @@ -653,6 +707,32 @@ mod tests {
);
}

#[test]
fn lex_expr_literals() {
let tokens = lex("lorem {{ [1, 3] {.} }} dolor").unwrap();
assert_eq!(
tokens,
[
(Token::Raw, "lorem "),
(Token::BeginExpr, "{{"),
(Token::Whitespace, " "),
(Token::OpenBracket, "["),
(Token::Number, "1"),
(Token::Comma, ","),
(Token::Whitespace, " "),
(Token::Number, "3"),
(Token::CloseBracket, "]"),
(Token::Whitespace, " "),
(Token::OpenBrace, "{"),
(Token::Dot, "."),
(Token::CloseBrace, "}"),
(Token::Whitespace, " "),
(Token::EndExpr, "}}"),
(Token::Raw, " dolor")
]
);
}

#[cfg(feature = "unicode")]
#[test]
fn lex_expr() {
Expand Down
16 changes: 15 additions & 1 deletion src/compile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,20 @@ impl Compiler {
ast::BaseExpr::Literal(literal) => {
self.push(Instr::ExprStartLiteral(literal));
}
ast::BaseExpr::List(list) => {
self.push(Instr::ExprStartList(list.span));
for item in list.items {
self.compile_base_expr(item);
self.push(Instr::ExprListPush);
}
}
ast::BaseExpr::Map(map) => {
self.push(Instr::ExprStartMap(map.span));
for (key, value) in map.items {
self.compile_base_expr(value);
self.push(Instr::ExprMapInsert(key));
}
}
}
}

Expand All @@ -171,7 +185,7 @@ impl Compiler {
Some(Instr::Apply(_, _, _)) => {
let instr = self.instrs.pop().unwrap();
match instr {
Instr::Apply(ident, len, span) => Instr::EmitWith(ident, len, span),
Instr::Apply(ident, arity, span) => Instr::EmitWith(ident, arity, span),
_ => unreachable!(),
}
}
Expand Down
53 changes: 51 additions & 2 deletions src/compile/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,7 @@ impl<'engine, 'source> Parser<'engine, 'source> {
let name = self.parse_ident()?;
let (args, span) = if self.is_next(Token::Colon)? {
let span = self.expect(Token::Colon)?;
let args = self.parse_args(span)?;
let args = self.parse_call_args(span)?;
let span = expr.span().combine(args.span);
(Some(args), span)
} else {
Expand Down Expand Up @@ -516,6 +516,17 @@ impl<'engine, 'source> Parser<'engine, 'source> {
let var = self.parse_var(first)?;
ast::BaseExpr::Var(var)
}

(Token::OpenBracket, span) => {
let list = self.parse_list(span)?;
ast::BaseExpr::List(list)
}

(Token::OpenBrace, span) => {
let map = self.parse_map(span)?;
ast::BaseExpr::Map(map)
}

(tk, span) => {
return Err(self.err_unexpected_token("expression", tk, span));
}
Expand Down Expand Up @@ -599,7 +610,7 @@ impl<'engine, 'source> Parser<'engine, 'source> {
///
/// user.name, "a string", true
///
fn parse_args(&mut self, span: Span) -> Result<ast::Args> {
fn parse_call_args(&mut self, span: Span) -> Result<ast::Args> {
let mut values = Vec::new();
loop {
values.push(self.parse_base_expr()?);
Expand Down Expand Up @@ -763,6 +774,44 @@ impl<'engine, 'source> Parser<'engine, 'source> {
Ok(string)
}

/// Parses a list literal.
fn parse_list(&mut self, span: Span) -> Result<ast::List> {
let mut items = Vec::new();
loop {
if self.is_next(Token::CloseBracket)? {
break;
}
let item = self.parse_base_expr()?;
items.push(item);
if !self.is_next(Token::Comma)? {
break;
}
self.expect(Token::Comma)?;
}
let span = span.combine(self.expect(Token::CloseBracket)?);
Ok(ast::List { items, span })
}

/// Parses a map literal.
fn parse_map(&mut self, span: Span) -> Result<ast::Map> {
let mut items = Vec::new();
loop {
if self.is_next(Token::CloseBrace)? {
break;
}
let key = self.parse_ident()?;
self.expect(Token::Colon)?;
let value = self.parse_base_expr()?;
items.push((key, value));
if !self.is_next(Token::Comma)? {
break;
}
self.expect(Token::Comma)?;
}
let span = span.combine(self.expect(Token::CloseBrace)?);
Ok(ast::Map { items, span })
}

/// Expects the given keyword.
fn expect_keyword(&mut self, exp: Keyword) -> Result<Span> {
let (kw, span) = self.parse_keyword()?;
Expand Down
33 changes: 32 additions & 1 deletion src/render/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::types::ast;
use crate::types::program::{Instr, Template};
use crate::types::span::Span;
use crate::value::ValueCow;
use crate::{EngineBoxFn, Error, Result};
use crate::{EngineBoxFn, Error, Result, Value};

#[cfg_attr(internal_debug, derive(Debug))]
pub struct RendererImpl<'render, 'stack> {
Expand Down Expand Up @@ -225,6 +225,37 @@ where
exprs.push((value, literal.span));
}

Instr::ExprStartList(span) => {
let value = ValueCow::Owned(crate::Value::new_list());
exprs.push((value, *span));
}

Instr::ExprStartMap(span) => {
let value = ValueCow::Owned(crate::Value::new_map());
exprs.push((value, *span));
}

Instr::ExprListPush => {
let (item, _) = exprs.pop().unwrap();
match exprs.last_mut().unwrap() {
(ValueCow::Owned(Value::List(l)), _) => {
l.push(item.to_owned());
}
_ => panic!("expected owned list"),
}
}

Instr::ExprMapInsert(key) => {
let key = t.source[key.span].to_owned();
let (value, _) = exprs.pop().unwrap();
match exprs.last_mut().unwrap() {
(ValueCow::Owned(Value::Map(m)), _) => {
m.insert(key, value.to_owned());
}
_ => panic!("expected owned map"),
}
}

Instr::Apply(name, _arity, _span) => {
let name_raw = &t.source[name.span];
match self.inner.engine.functions.get(name_raw) {
Expand Down
10 changes: 10 additions & 0 deletions src/render/value.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::collections::BTreeMap;

use crate::types::ast;
use crate::value::ValueCow;
use crate::{Error, Result, Value};
Expand Down Expand Up @@ -27,6 +29,14 @@ impl Value {
Value::Map(_) => "map",
}
}

pub(crate) const fn new_map() -> Self {
Self::Map(BTreeMap::new())
}

pub(crate) const fn new_list() -> Self {
Self::List(Vec::new())
}
}

/// Lookup the given path.
Expand Down
16 changes: 16 additions & 0 deletions src/types/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ pub struct Args {
pub enum BaseExpr {
Var(Var),
Literal(Literal),
List(List),
Map(Map),
}

#[cfg_attr(internal_debug, derive(Debug))]
Expand Down Expand Up @@ -145,6 +147,18 @@ pub struct Literal {
pub span: Span,
}

#[cfg_attr(internal_debug, derive(Debug))]
pub struct List {
pub items: Vec<BaseExpr>,
pub span: Span,
}

#[cfg_attr(internal_debug, derive(Debug))]
pub struct Map {
pub items: Vec<(Ident, BaseExpr)>,
pub span: Span,
}

impl Scope {
pub const fn new() -> Self {
Self { stmts: Vec::new() }
Expand All @@ -171,6 +185,8 @@ impl BaseExpr {
match self {
BaseExpr::Var(var) => var.span(),
BaseExpr::Literal(lit) => lit.span,
BaseExpr::List(list) => list.span,
BaseExpr::Map(map) => map.span,
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions src/types/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,18 @@ pub enum Instr {
/// Start building an expression using a literal
ExprStartLiteral(ast::Literal),

/// Start building a list expression
ExprStartList(Span),

/// Start building a map expression
ExprStartMap(Span),

/// Append an item to the current list expression
ExprListPush,

/// Insert an item to the current map expression
ExprMapInsert(ast::Ident),

/// Apply the filter using the value and args on the top of the stack.
///
/// The second value is the number of arguments to pop from the stack
Expand Down
Loading

0 comments on commit d7b8c3c

Please sign in to comment.