diff --git a/src/ast/mod.rs b/src/ast/mod.rs index fa78ccc..1d483a7 100644 --- a/src/ast/mod.rs +++ b/src/ast/mod.rs @@ -1,9 +1,12 @@ use thiserror::Error; use winnow::Parser; use winnow::RecoverableParser; +use winnow::combinator::Infix::Left; use winnow::combinator::alt; use winnow::combinator::cut_err; use winnow::combinator::delimited; +use winnow::combinator::dispatch; +use winnow::combinator::expression; use winnow::combinator::not; use winnow::combinator::opt; use winnow::combinator::peek; @@ -24,7 +27,9 @@ use winnow::token::any; use crate::SourceSpan; use crate::parser::TemplateToken; use crate::parser::TokenKind; +use crate::parser::TokenOperator; use crate::resume_after_cut; +use crate::value::NomoValue; #[derive(Debug, Clone)] pub struct TemplateAst<'input> { @@ -253,7 +258,16 @@ pub enum TemplateAstExpr<'input> { expression: Option>>, }, Invalid(&'input [TemplateToken]), + Operation { + op: TokenOperator, + lhs: Box>, + rhs: Box>, + }, EndBlock, + Literal { + source: TemplateToken, + value: NomoValue, + }, } fn parse_asts<'input>(input: &mut Input<'input>) -> Result>, AstError> { @@ -275,7 +289,7 @@ fn parse_interpolation<'input>( input: &mut Input<'input>, ) -> Result, AstError> { let expr_parser = resume_after_cut( - parse_value_expression, + parse_expression, repeat_till( 0.., any, @@ -397,7 +411,7 @@ fn parse_for_loop<'input>(input: &mut Input<'input>) -> Result( preceded( TokenKind::ConditionalIf, cut_err( - surrounded(ws, parse_value_expression) + surrounded(ws, parse_expression) .map(Box::new) .context(AstError::ctx().msg("Expected an expression after 'if'")), ), @@ -510,7 +524,7 @@ fn parse_conditional_else<'input>( surrounded(ws, TokenKind::ConditionalElse), opt(preceded( TokenKind::ConditionalIf, - cut_err(surrounded(ws, parse_value_expression)).map(Box::new), + cut_err(surrounded(ws, parse_expression)).map(Box::new), )), ) .map(|else_expr| TemplateAstExpr::ElseConditional { @@ -545,12 +559,6 @@ fn parse_end<'input>(input: &mut Input<'input>) -> Result( - input: &mut Input<'input>, -) -> Result, AstError> { - trace("value_expression", alt((parse_variable_access,))).parse_next(input) -} - fn parse_variable_access<'input>( input: &mut Input<'input>, ) -> Result, AstError> { @@ -611,6 +619,55 @@ where ) } +fn parse_operand<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace("operand", alt((parse_variable_access, parse_literal))).parse_next(input) +} + +fn parse_literal<'input>(input: &mut Input<'input>) -> Result, AstError> { + trace( + "literal", + any.verify_map(|token: &TemplateToken| match token.kind() { + TokenKind::Literal(literal) => Some(TemplateAstExpr::Literal { + source: token.clone(), + value: match literal { + crate::parser::TokenLiteral::Bool(bool) => NomoValue::Bool { value: bool }, + crate::parser::TokenLiteral::Integer(int) => NomoValue::Integer { value: int }, + }, + }), + _ => None, + }), + ) + .parse_next(input) +} + +fn parse_operator<'input>(input: &mut Input<'input>) -> Result { + trace( + "operator", + any.verify_map(|t: &'input TemplateToken| match t.kind() { + TokenKind::Operator(op) => Some(op), + _ => None, + }), + ) + .parse_next(input) +} + +fn parse_expression<'input>( + input: &mut Input<'input>, +) -> Result, AstError> { + trace( + "expression", + expression(surrounded(ws, parse_operand)).infix(dispatch! { surrounded(ws, parse_operator); + TokenOperator::Plus => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Plus, lhs: Box::new(lhs), rhs: Box::new(rhs) })), + TokenOperator::Minus => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Minus, lhs: Box::new(lhs), rhs: Box::new(rhs) })), + TokenOperator::Times => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Times, lhs: Box::new(lhs), rhs: Box::new(rhs) })), + TokenOperator::Divide => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Divide, lhs: Box::new(lhs), rhs: Box::new(rhs) })), + TokenOperator::And => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::And, lhs: Box::new(lhs), rhs: Box::new(rhs) })), + TokenOperator::Or => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Or, lhs: Box::new(lhs), rhs: Box::new(rhs) })), + }), + ) + .parse_next(input) +} + fn ws<'input>(input: &mut Input<'input>) -> Result<(), AstError> { repeat(.., TokenKind::Whitespace).parse_next(input) } @@ -912,4 +969,15 @@ mod tests { insta::assert_debug_snapshot!(ast); } + + #[test] + fn check_math_expression() { + let input = "{{= 5 * 3 + 2 / 3 }}"; + + let parsed = crate::parser::parse(input.into()).unwrap(); + + let ast = panic_pretty(input, parse(parsed.tokens())); + + insta::assert_debug_snapshot!(ast); + } } diff --git a/src/ast/snapshots/nomo__ast__tests__check_math_expression.snap b/src/ast/snapshots/nomo__ast__tests__check_math_expression.snap new file mode 100644 index 0000000..7265e0a --- /dev/null +++ b/src/ast/snapshots/nomo__ast__tests__check_math_expression.snap @@ -0,0 +1,45 @@ +--- +source: src/ast/mod.rs +expression: ast +--- +TemplateAst { + root: [ + Interpolation { + prev_whitespace_content: None, + expression: Operation { + op: Plus, + lhs: Operation { + op: Times, + lhs: Literal { + source: [Literal(Integer(5))]"5" (4..5), + value: Integer { + value: 5, + }, + }, + rhs: Literal { + source: [Literal(Integer(3))]"3" (8..9), + value: Integer { + value: 3, + }, + }, + }, + rhs: Operation { + op: Divide, + lhs: Literal { + source: [Literal(Integer(2))]"2" (12..13), + value: Integer { + value: 2, + }, + }, + rhs: Literal { + source: [Literal(Integer(3))]"3" (16..17), + value: Integer { + value: 3, + }, + }, + }, + }, + post_whitespace_content: None, + }, + ], +} diff --git a/src/emit/mod.rs b/src/emit/mod.rs index b34d4cf..afaa4cd 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -402,6 +402,8 @@ fn emit_ast_expr( | TemplateAstExpr::For { .. } | TemplateAstExpr::ForElse | TemplateAstExpr::Invalid { .. } + | TemplateAstExpr::Literal { .. } + | TemplateAstExpr::Operation { .. } | TemplateAstExpr::VariableAccess { .. } => eval.push(Instruction::Abort), } } @@ -430,6 +432,8 @@ fn emit_expr_load( TemplateAstExpr::ForChain { .. } => todo!(), TemplateAstExpr::For { .. } => todo!(), TemplateAstExpr::ForElse => todo!(), + TemplateAstExpr::Operation { .. } => todo!(), + TemplateAstExpr::Literal { .. } => todo!(), TemplateAstExpr::IfConditional { .. } => todo!(), TemplateAstExpr::ConditionalContent { .. } => todo!(), diff --git a/src/parser/mod.rs b/src/parser/mod.rs index de0ba97..a5474c4 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8,11 +8,16 @@ use thiserror::Error; use winnow::LocatingSlice; use winnow::Parser; use winnow::RecoverableParser; +use winnow::ascii::alpha1; +use winnow::ascii::digit1; use winnow::ascii::multispace0; use winnow::ascii::multispace1; use winnow::combinator::alt; use winnow::combinator::cut_err; +use winnow::combinator::dispatch; +use winnow::combinator::empty; use winnow::combinator::eof; +use winnow::combinator::fail; use winnow::combinator::not; use winnow::combinator::opt; use winnow::combinator::peek; @@ -28,6 +33,7 @@ use winnow::stream::Location; use winnow::stream::Recoverable; use winnow::stream::Stream; use winnow::token::any; +use winnow::token::literal; use winnow::token::one_of; use winnow::token::rest; use winnow::token::take_until; @@ -213,11 +219,23 @@ pub enum TokenKind { In, End, Literal(TokenLiteral), + Operator(TokenOperator), +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum TokenOperator { + Plus, + Minus, + Times, + Divide, + And, + Or, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum TokenLiteral { Bool(bool), + Integer(u64), } impl PartialEq for TemplateToken { @@ -309,6 +327,13 @@ impl TemplateToken { } } + pub fn operator(operator: TokenOperator, source: NomoInput) -> Self { + TemplateToken { + kind: TokenKind::Operator(operator), + source, + } + } + pub fn kind(&self) -> TokenKind { self.kind } @@ -385,7 +410,7 @@ fn parse_interpolate<'input>(input: &mut Input<'input>) -> PResult<'input, Vec(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { trace( "parse_block_token", - alt((parse_ident, parse_keyword, parse_whitespace)), + alt((parse_ident, parse_keyword, parse_whitespace, parse_operator)), ) .parse_next(input) } @@ -393,13 +418,20 @@ fn parse_block_token<'input>(input: &mut Input<'input>) -> PResult<'input, Templ fn parse_literal<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { trace( "parse_literal", - alt((parse_boolean,)) + alt((parse_boolean, parse_number)) .with_taken() .map(|(lit, span)| TemplateToken::literal(lit, span)), ) .parse_next(input) } +fn parse_number<'input>(input: &mut Input<'input>) -> PResult<'input, TokenLiteral> { + digit1 + .verify_map(|digits: NomoInput| digits.parse::().ok()) + .map(TokenLiteral::Integer) + .parse_next(input) +} + fn parse_boolean<'input>(input: &mut Input<'input>) -> PResult<'input, TokenLiteral> { alt(( "true".value(TokenLiteral::Bool(true)), @@ -474,12 +506,44 @@ fn parse_ident<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateTok .parse_next(input) } +fn parse_operator<'input>(input: &mut Input<'input>) -> PResult<'input, TemplateToken> { + let (operator, source) = trace( + "operator", + dispatch! {any; + '+' => empty.value(TokenOperator::Plus), + '-' => empty.value(TokenOperator::Minus), + '*' => empty.value(TokenOperator::Times), + '/' => empty.value(TokenOperator::Divide), + '&' => alt(( + "&".value(TokenOperator::And), + cut_err(fail), + )), + '|' => alt(( + "|".value(TokenOperator::Or), + cut_err(fail), + )), + _ => fail, + }, + ) + .with_taken() + .parse_next(input)?; + + Ok(TemplateToken::operator(operator, source)) +} + fn ident<'input>(input: &mut Input<'input>) -> PResult<'input, NomoInput> { peek(not(parse_keyword)) .context(ParseError::ctx().msg("Expected an ident, but found a literal instead")) .parse_next(input)?; - take_while(1.., |c: char| c.is_alphanumeric() || "_".contains(c)).parse_next(input) + let literal_start = alt((alpha1, "_")); + + ( + literal_start, + take_while(0.., |c: char| c.is_alphanumeric() || "_".contains(c)), + ) + .take() + .parse_next(input) } fn bad_ident<'input>(input: &mut Input<'input>) -> PResult<'input, ()> { @@ -680,4 +744,61 @@ mod tests { ) "#); } + + #[test] + fn parse_operations() { + let input = "{{= 5 * 14 + 3 / 2 - 1 }}{{ if foo && bar || baz }}{{ end }}"; + let output = parse(input.into()); + + insta::assert_debug_snapshot!(output, @r#" + Ok( + ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [WantsOutput]"=" (2..3), + [Whitespace]" " (3..4), + [Literal(Integer(5))]"5" (4..5), + [Whitespace]" " (5..6), + [Operator(Times)]"*" (6..7), + [Whitespace]" " (7..8), + [Literal(Integer(14))]"14" (8..10), + [Whitespace]" " (10..11), + [Operator(Plus)]"+" (11..12), + [Whitespace]" " (12..13), + [Literal(Integer(3))]"3" (13..14), + [Whitespace]" " (14..15), + [Operator(Divide)]"/" (15..16), + [Whitespace]" " (16..17), + [Literal(Integer(2))]"2" (17..18), + [Whitespace]" " (18..19), + [Operator(Minus)]"-" (19..20), + [Whitespace]" " (20..21), + [Literal(Integer(1))]"1" (21..22), + [Whitespace]" " (22..23), + [RightDelim]"}}" (23..25), + [LeftDelim]"{{" (25..27), + [Whitespace]" " (27..28), + [ConditionalIf]"if" (28..30), + [Whitespace]" " (30..31), + [Ident]"foo" (31..34), + [Whitespace]" " (34..35), + [Operator(And)]"&&" (35..37), + [Whitespace]" " (37..38), + [Ident]"bar" (38..41), + [Whitespace]" " (41..42), + [Operator(Or)]"||" (42..44), + [Whitespace]" " (44..45), + [Ident]"baz" (45..48), + [Whitespace]" " (48..49), + [RightDelim]"}}" (49..51), + [LeftDelim]"{{" (51..53), + [Whitespace]" " (53..54), + [End]"end" (54..57), + [Whitespace]" " (57..58), + [RightDelim]"}}" (58..60), + ], + }, + ) + "#); + } } diff --git a/tests/cases/1-parsed@identifiers.snap b/tests/cases/1-parsed@identifiers.snap index 66a2da0..31760f1 100644 --- a/tests/cases/1-parsed@identifiers.snap +++ b/tests/cases/1-parsed@identifiers.snap @@ -1,6 +1,15 @@ --- source: tests/file_tests.rs expression: parsed +info: + input: "{{= _name }}\n{{= a_name }}\n{{= name }}\n{{= _name1 }}\n{{= _namE }}\n{{= name1 }}" + context: + _namE: Foo + name1: Foo + name: Foo + _name: Foo + a_name: Foo + _name1: Foo input_file: tests/cases/identifiers.nomo --- ParsedTemplate { @@ -22,29 +31,29 @@ ParsedTemplate { [LeftDelim]"{{" (27..29), [WantsOutput]"=" (29..30), [Whitespace]" " (30..31), - [Ident]"1name" (31..36), - [Whitespace]" " (36..37), - [RightDelim]"}}" (37..39), - [Whitespace]"\n" (39..40), - [LeftDelim]"{{" (40..42), - [WantsOutput]"=" (42..43), - [Whitespace]" " (43..44), - [Ident]"_name1" (44..50), - [Whitespace]" " (50..51), - [RightDelim]"}}" (51..53), - [Whitespace]"\n" (53..54), - [LeftDelim]"{{" (54..56), - [WantsOutput]"=" (56..57), - [Whitespace]" " (57..58), - [Ident]"_namE" (58..63), - [Whitespace]" " (63..64), - [RightDelim]"}}" (64..66), - [Whitespace]"\n" (66..67), - [LeftDelim]"{{" (67..69), - [WantsOutput]"=" (69..70), - [Whitespace]" " (70..71), - [Ident]"name1" (71..76), - [Whitespace]" " (76..77), - [RightDelim]"}}" (77..79), + [Ident]"name" (31..35), + [Whitespace]" " (35..36), + [RightDelim]"}}" (36..38), + [Whitespace]"\n" (38..39), + [LeftDelim]"{{" (39..41), + [WantsOutput]"=" (41..42), + [Whitespace]" " (42..43), + [Ident]"_name1" (43..49), + [Whitespace]" " (49..50), + [RightDelim]"}}" (50..52), + [Whitespace]"\n" (52..53), + [LeftDelim]"{{" (53..55), + [WantsOutput]"=" (55..56), + [Whitespace]" " (56..57), + [Ident]"_namE" (57..62), + [Whitespace]" " (62..63), + [RightDelim]"}}" (63..65), + [Whitespace]"\n" (65..66), + [LeftDelim]"{{" (66..68), + [WantsOutput]"=" (68..69), + [Whitespace]" " (69..70), + [Ident]"name1" (70..75), + [Whitespace]" " (75..76), + [RightDelim]"}}" (76..78), ], } diff --git a/tests/cases/2-ast@identifiers.snap b/tests/cases/2-ast@identifiers.snap index ddcc251..4333d9d 100644 --- a/tests/cases/2-ast@identifiers.snap +++ b/tests/cases/2-ast@identifiers.snap @@ -1,6 +1,15 @@ --- source: tests/file_tests.rs expression: ast +info: + input: "{{= _name }}\n{{= a_name }}\n{{= name }}\n{{= _name1 }}\n{{= _namE }}\n{{= name1 }}" + context: + _namE: Foo + name1: Foo + name: Foo + _name: Foo + a_name: Foo + _name1: Foo input_file: tests/cases/identifiers.nomo --- TemplateAst { @@ -26,34 +35,34 @@ TemplateAst { Interpolation { prev_whitespace_content: None, expression: VariableAccess( - [Ident]"1name" (31..36), + [Ident]"name" (31..35), ), post_whitespace_content: Some( - [Whitespace]"\n" (39..40), + [Whitespace]"\n" (38..39), ), }, Interpolation { prev_whitespace_content: None, expression: VariableAccess( - [Ident]"_name1" (44..50), + [Ident]"_name1" (43..49), ), post_whitespace_content: Some( - [Whitespace]"\n" (53..54), + [Whitespace]"\n" (52..53), ), }, Interpolation { prev_whitespace_content: None, expression: VariableAccess( - [Ident]"_namE" (58..63), + [Ident]"_namE" (57..62), ), post_whitespace_content: Some( - [Whitespace]"\n" (66..67), + [Whitespace]"\n" (65..66), ), }, Interpolation { prev_whitespace_content: None, expression: VariableAccess( - [Ident]"name1" (71..76), + [Ident]"name1" (70..75), ), post_whitespace_content: None, }, diff --git a/tests/cases/3-instructions@identifiers.snap b/tests/cases/3-instructions@identifiers.snap index 01ec3a8..9556d23 100644 --- a/tests/cases/3-instructions@identifiers.snap +++ b/tests/cases/3-instructions@identifiers.snap @@ -1,6 +1,15 @@ --- source: tests/file_tests.rs expression: emit +info: + input: "{{= _name }}\n{{= a_name }}\n{{= name }}\n{{= _name1 }}\n{{= _namE }}\n{{= name1 }}" + context: + _namE: Foo + name1: Foo + name: Foo + _name: Foo + a_name: Foo + _name1: Foo input_file: tests/cases/identifiers.nomo --- VMInstructions { @@ -35,7 +44,7 @@ VMInstructions { content: "\n" (26..27), }, LoadFromContextToSlot { - name: "1name" (31..36), + name: "name" (31..35), slot: VariableSlot { index: 2, }, @@ -46,10 +55,10 @@ VMInstructions { }, }, AppendContent { - content: "\n" (39..40), + content: "\n" (38..39), }, LoadFromContextToSlot { - name: "_name1" (44..50), + name: "_name1" (43..49), slot: VariableSlot { index: 3, }, @@ -60,10 +69,10 @@ VMInstructions { }, }, AppendContent { - content: "\n" (53..54), + content: "\n" (52..53), }, LoadFromContextToSlot { - name: "_namE" (58..63), + name: "_namE" (57..62), slot: VariableSlot { index: 4, }, @@ -74,10 +83,10 @@ VMInstructions { }, }, AppendContent { - content: "\n" (66..67), + content: "\n" (65..66), }, LoadFromContextToSlot { - name: "name1" (71..76), + name: "name1" (70..75), slot: VariableSlot { index: 5, }, diff --git a/tests/cases/identifiers.nomo b/tests/cases/identifiers.nomo index 570a7b2..da49414 100644 --- a/tests/cases/identifiers.nomo +++ b/tests/cases/identifiers.nomo @@ -1,7 +1,7 @@ { "_name": "Foo", "a_name": "Foo", - "1name": "Foo", + "name": "Foo", "_name1": "Foo", "_namE": "Foo", "name1": "Foo" @@ -9,7 +9,7 @@ --- {{= _name }} {{= a_name }} -{{= 1name }} +{{= name }} {{= _name1 }} {{= _namE }} {{= name1 }} \ No newline at end of file