Compare commits
5 commits
8c02dbd672
...
605798674f
| Author | SHA1 | Date | |
|---|---|---|---|
| 605798674f | |||
| 722e61cc85 | |||
| 437584c844 | |||
| d222573a3a | |||
| 05c095ccfe |
16 changed files with 856 additions and 19 deletions
|
|
@ -654,15 +654,32 @@ fn parse_operator<'input>(input: &mut Input<'input>) -> Result<TokenOperator, As
|
||||||
fn parse_expression<'input>(
|
fn parse_expression<'input>(
|
||||||
input: &mut Input<'input>,
|
input: &mut Input<'input>,
|
||||||
) -> Result<TemplateAstExpr<'input>, AstError> {
|
) -> Result<TemplateAstExpr<'input>, AstError> {
|
||||||
|
macro_rules! infix {
|
||||||
|
($parser:expr => [ $($side:tt $val:tt => $prec:expr),* $(,)? ]) => {
|
||||||
|
dispatch! { surrounded(ws, parse_operator);
|
||||||
|
$(
|
||||||
|
TokenOperator::$val => $side($prec, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::$val, lhs: Box::new(lhs), rhs: Box::new(rhs) }))
|
||||||
|
),*
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
trace(
|
trace(
|
||||||
"expression",
|
"expression",
|
||||||
expression(surrounded(ws, parse_operand)).infix(dispatch! { surrounded(ws, parse_operator);
|
expression(surrounded(ws, parse_operand)).infix(infix! {
|
||||||
TokenOperator::Plus => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Plus, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
|
surrounded(ws, parse_operator) => [
|
||||||
TokenOperator::Minus => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Minus, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
|
Left Plus => 18,
|
||||||
TokenOperator::Times => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Times, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
|
Left Minus => 18,
|
||||||
TokenOperator::Divide => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Divide, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
|
Left Times => 20,
|
||||||
TokenOperator::And => Left(7, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::And, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
|
Left Divide => 20,
|
||||||
TokenOperator::Or => Left(5, |_, lhs, rhs| Ok(TemplateAstExpr::Operation { op: TokenOperator::Or, lhs: Box::new(lhs), rhs: Box::new(rhs) })),
|
Left And => 10,
|
||||||
|
Left Or => 7,
|
||||||
|
Left Equal => 12,
|
||||||
|
Left NotEqual => 12,
|
||||||
|
Left Greater => 15,
|
||||||
|
Left GreaterOrEqual => 15,
|
||||||
|
Left Lesser => 15,
|
||||||
|
Left LesserOrEqual => 15,
|
||||||
|
]
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.parse_next(input)
|
.parse_next(input)
|
||||||
|
|
@ -980,4 +997,15 @@ mod tests {
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(ast);
|
insta::assert_debug_snapshot!(ast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn check_logical_expression() {
|
||||||
|
let input = "{{= true && false || 3 >= 2 && 5 == 2 }}";
|
||||||
|
|
||||||
|
let parsed = crate::parser::parse(input.into()).unwrap();
|
||||||
|
|
||||||
|
let ast = panic_pretty(input, parse(parsed.tokens()));
|
||||||
|
|
||||||
|
insta::assert_debug_snapshot!(ast);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
---
|
||||||
|
source: src/ast/mod.rs
|
||||||
|
expression: ast
|
||||||
|
---
|
||||||
|
TemplateAst {
|
||||||
|
root: [
|
||||||
|
Interpolation {
|
||||||
|
prev_whitespace_content: None,
|
||||||
|
expression: Operation {
|
||||||
|
op: Or,
|
||||||
|
lhs: Operation {
|
||||||
|
op: And,
|
||||||
|
lhs: Literal {
|
||||||
|
source: [Literal(Bool(true))]"true" (4..8),
|
||||||
|
value: Bool {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Bool(false))]"false" (12..17),
|
||||||
|
value: Bool {
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Operation {
|
||||||
|
op: And,
|
||||||
|
lhs: Operation {
|
||||||
|
op: GreaterOrEqual,
|
||||||
|
lhs: Literal {
|
||||||
|
source: [Literal(Integer(3))]"3" (21..22),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Integer(2))]"2" (26..27),
|
||||||
|
value: Integer {
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Operation {
|
||||||
|
op: Equal,
|
||||||
|
lhs: Literal {
|
||||||
|
source: [Literal(Integer(5))]"5" (31..32),
|
||||||
|
value: Integer {
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Integer(2))]"2" (36..37),
|
||||||
|
value: Integer {
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
post_whitespace_content: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,9 @@ use std::collections::BTreeMap;
|
||||||
|
|
||||||
use crate::ast::TemplateAstExpr;
|
use crate::ast::TemplateAstExpr;
|
||||||
use crate::input::NomoInput;
|
use crate::input::NomoInput;
|
||||||
|
use crate::parser::TemplateToken;
|
||||||
|
use crate::parser::TokenOperator;
|
||||||
|
use crate::value::NomoValue;
|
||||||
|
|
||||||
pub struct EmitMachine {
|
pub struct EmitMachine {
|
||||||
current_index: usize,
|
current_index: usize,
|
||||||
|
|
@ -88,6 +91,17 @@ pub enum Instruction {
|
||||||
value_ident: NomoInput,
|
value_ident: NomoInput,
|
||||||
value_slot: VariableSlot,
|
value_slot: VariableSlot,
|
||||||
},
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: TemplateToken,
|
||||||
|
value: NomoValue,
|
||||||
|
slot: VariableSlot,
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: TokenOperator,
|
||||||
|
left_slot: VariableSlot,
|
||||||
|
right_slot: VariableSlot,
|
||||||
|
result_slot: VariableSlot,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -409,7 +423,7 @@ fn emit_ast_expr(
|
||||||
}
|
}
|
||||||
|
|
||||||
fn emit_expr_load(
|
fn emit_expr_load(
|
||||||
_machine: &mut EmitMachine,
|
machine: &mut EmitMachine,
|
||||||
eval: &mut Vec<Instruction>,
|
eval: &mut Vec<Instruction>,
|
||||||
emit_slot: VariableSlot,
|
emit_slot: VariableSlot,
|
||||||
expression: &TemplateAstExpr<'_>,
|
expression: &TemplateAstExpr<'_>,
|
||||||
|
|
@ -421,6 +435,26 @@ fn emit_expr_load(
|
||||||
slot: emit_slot,
|
slot: emit_slot,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
TemplateAstExpr::Literal { source, value } => {
|
||||||
|
eval.push(Instruction::LoadLiteralToSlot {
|
||||||
|
source: source.clone(),
|
||||||
|
value: value.clone(),
|
||||||
|
slot: emit_slot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
TemplateAstExpr::Operation { op, lhs, rhs } => {
|
||||||
|
let left_slot = machine.reserve_slot();
|
||||||
|
emit_expr_load(machine, eval, left_slot, lhs);
|
||||||
|
let right_slot = machine.reserve_slot();
|
||||||
|
emit_expr_load(machine, eval, right_slot, rhs);
|
||||||
|
|
||||||
|
eval.push(Instruction::MathOperate {
|
||||||
|
op: *op,
|
||||||
|
left_slot,
|
||||||
|
right_slot,
|
||||||
|
result_slot: emit_slot,
|
||||||
|
});
|
||||||
|
}
|
||||||
TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort),
|
TemplateAstExpr::Invalid { .. } => eval.push(Instruction::Abort),
|
||||||
TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
|
TemplateAstExpr::StaticContent { .. } | TemplateAstExpr::Interpolation { .. } => {
|
||||||
unreachable!("Invalid AST here")
|
unreachable!("Invalid AST here")
|
||||||
|
|
@ -432,8 +466,6 @@ fn emit_expr_load(
|
||||||
TemplateAstExpr::ForChain { .. } => todo!(),
|
TemplateAstExpr::ForChain { .. } => todo!(),
|
||||||
TemplateAstExpr::For { .. } => todo!(),
|
TemplateAstExpr::For { .. } => todo!(),
|
||||||
TemplateAstExpr::ForElse => todo!(),
|
TemplateAstExpr::ForElse => todo!(),
|
||||||
TemplateAstExpr::Operation { .. } => todo!(),
|
|
||||||
TemplateAstExpr::Literal { .. } => todo!(),
|
|
||||||
|
|
||||||
TemplateAstExpr::IfConditional { .. } => todo!(),
|
TemplateAstExpr::IfConditional { .. } => todo!(),
|
||||||
TemplateAstExpr::ConditionalContent { .. } => todo!(),
|
TemplateAstExpr::ConditionalContent { .. } => todo!(),
|
||||||
|
|
|
||||||
|
|
@ -96,8 +96,9 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
|
||||||
scopes.insert_into_slot(*slot, value.clone());
|
scopes.insert_into_slot(*slot, value.clone());
|
||||||
}
|
}
|
||||||
Instruction::EmitFromSlot { slot } => {
|
Instruction::EmitFromSlot { slot } => {
|
||||||
let value = scopes.get(slot).as_str().unwrap();
|
let value = scopes.get(slot).try_to_string().unwrap();
|
||||||
output.push_str(value);
|
|
||||||
|
output.push_str(&value);
|
||||||
}
|
}
|
||||||
Instruction::PushScope { inherit_parent: _ } => {
|
Instruction::PushScope { inherit_parent: _ } => {
|
||||||
scopes.push_scope();
|
scopes.push_scope();
|
||||||
|
|
@ -179,6 +180,34 @@ pub fn execute(vm: &VMInstructions, global_context: &Context) -> Result<String,
|
||||||
|
|
||||||
scopes.insert_into_scope(value_ident, value);
|
scopes.insert_into_scope(value_ident, value);
|
||||||
}
|
}
|
||||||
|
Instruction::LoadLiteralToSlot {
|
||||||
|
source: _,
|
||||||
|
value,
|
||||||
|
slot,
|
||||||
|
} => {
|
||||||
|
scopes.insert_into_slot(*slot, value.clone());
|
||||||
|
}
|
||||||
|
Instruction::MathOperate {
|
||||||
|
op,
|
||||||
|
left_slot,
|
||||||
|
right_slot,
|
||||||
|
result_slot,
|
||||||
|
} => {
|
||||||
|
let left_value = scopes.get(left_slot);
|
||||||
|
let right_value = scopes.get(right_slot);
|
||||||
|
|
||||||
|
let result = match op {
|
||||||
|
crate::parser::TokenOperator::Plus => left_value.try_add(right_value),
|
||||||
|
crate::parser::TokenOperator::Minus => left_value.try_sub(right_value),
|
||||||
|
crate::parser::TokenOperator::Times => left_value.try_mul(right_value),
|
||||||
|
crate::parser::TokenOperator::Divide => left_value.try_div(right_value),
|
||||||
|
crate::parser::TokenOperator::And => left_value.try_and(right_value),
|
||||||
|
crate::parser::TokenOperator::Or => left_value.try_or(right_value),
|
||||||
|
_ => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
scopes.insert_into_slot(*result_slot, result.unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ip += 1;
|
ip += 1;
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ use winnow::stream::Location;
|
||||||
use winnow::stream::Recoverable;
|
use winnow::stream::Recoverable;
|
||||||
use winnow::stream::Stream;
|
use winnow::stream::Stream;
|
||||||
use winnow::token::any;
|
use winnow::token::any;
|
||||||
use winnow::token::literal;
|
|
||||||
use winnow::token::one_of;
|
use winnow::token::one_of;
|
||||||
use winnow::token::rest;
|
use winnow::token::rest;
|
||||||
use winnow::token::take_until;
|
use winnow::token::take_until;
|
||||||
|
|
@ -230,6 +229,12 @@ pub enum TokenOperator {
|
||||||
Divide,
|
Divide,
|
||||||
And,
|
And,
|
||||||
Or,
|
Or,
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterOrEqual,
|
||||||
|
Lesser,
|
||||||
|
LesserOrEqual,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
|
|
@ -522,6 +527,22 @@ fn parse_operator<'input>(input: &mut Input<'input>) -> PResult<'input, Template
|
||||||
"|".value(TokenOperator::Or),
|
"|".value(TokenOperator::Or),
|
||||||
cut_err(fail),
|
cut_err(fail),
|
||||||
)),
|
)),
|
||||||
|
'<' => alt((
|
||||||
|
"=".value(TokenOperator::LesserOrEqual),
|
||||||
|
empty.value(TokenOperator::Lesser),
|
||||||
|
)),
|
||||||
|
'>' => alt((
|
||||||
|
"=".value(TokenOperator::GreaterOrEqual),
|
||||||
|
empty.value(TokenOperator::Greater),
|
||||||
|
)),
|
||||||
|
'!' => alt((
|
||||||
|
"=".value(TokenOperator::NotEqual),
|
||||||
|
cut_err(fail),
|
||||||
|
)),
|
||||||
|
'=' => alt((
|
||||||
|
"=".value(TokenOperator::Equal),
|
||||||
|
cut_err(fail),
|
||||||
|
)),
|
||||||
_ => fail,
|
_ => fail,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
@ -747,7 +768,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_operations() {
|
fn parse_operations() {
|
||||||
let input = "{{= 5 * 14 + 3 / 2 - 1 }}{{ if foo && bar || baz }}{{ end }}";
|
let input = "{{= 5 * 14 + 3 / 2 - 1 }}{{ if foo && bar || baz && 2 != 3 || 4 > 2 || 43 <= 5 }}{{ end }}";
|
||||||
let output = parse(input.into());
|
let output = parse(input.into());
|
||||||
|
|
||||||
insta::assert_debug_snapshot!(output, @r#"
|
insta::assert_debug_snapshot!(output, @r#"
|
||||||
|
|
@ -790,12 +811,36 @@ mod tests {
|
||||||
[Whitespace]" " (44..45),
|
[Whitespace]" " (44..45),
|
||||||
[Ident]"baz" (45..48),
|
[Ident]"baz" (45..48),
|
||||||
[Whitespace]" " (48..49),
|
[Whitespace]" " (48..49),
|
||||||
[RightDelim]"}}" (49..51),
|
[Operator(And)]"&&" (49..51),
|
||||||
[LeftDelim]"{{" (51..53),
|
[Whitespace]" " (51..52),
|
||||||
|
[Literal(Integer(2))]"2" (52..53),
|
||||||
[Whitespace]" " (53..54),
|
[Whitespace]" " (53..54),
|
||||||
[End]"end" (54..57),
|
[Operator(NotEqual)]"!=" (54..56),
|
||||||
[Whitespace]" " (57..58),
|
[Whitespace]" " (56..57),
|
||||||
[RightDelim]"}}" (58..60),
|
[Literal(Integer(3))]"3" (57..58),
|
||||||
|
[Whitespace]" " (58..59),
|
||||||
|
[Operator(Or)]"||" (59..61),
|
||||||
|
[Whitespace]" " (61..62),
|
||||||
|
[Literal(Integer(4))]"4" (62..63),
|
||||||
|
[Whitespace]" " (63..64),
|
||||||
|
[Operator(Greater)]">" (64..65),
|
||||||
|
[Whitespace]" " (65..66),
|
||||||
|
[Literal(Integer(2))]"2" (66..67),
|
||||||
|
[Whitespace]" " (67..68),
|
||||||
|
[Operator(Or)]"||" (68..70),
|
||||||
|
[Whitespace]" " (70..71),
|
||||||
|
[Literal(Integer(43))]"43" (71..73),
|
||||||
|
[Whitespace]" " (73..74),
|
||||||
|
[Operator(LesserOrEqual)]"<=" (74..76),
|
||||||
|
[Whitespace]" " (76..77),
|
||||||
|
[Literal(Integer(5))]"5" (77..78),
|
||||||
|
[Whitespace]" " (78..79),
|
||||||
|
[RightDelim]"}}" (79..81),
|
||||||
|
[LeftDelim]"{{" (81..83),
|
||||||
|
[Whitespace]" " (83..84),
|
||||||
|
[End]"end" (84..87),
|
||||||
|
[Whitespace]" " (87..88),
|
||||||
|
[RightDelim]"}}" (88..90),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
141
src/value.rs
141
src/value.rs
|
|
@ -97,6 +97,147 @@ impl NomoValue {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_add(&self, right_value: &NomoValue) -> Option<NomoValue> {
|
||||||
|
match (self, right_value) {
|
||||||
|
(NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => {
|
||||||
|
Some(NomoValue::Integer {
|
||||||
|
value: *lval + *rval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
NomoValue::SignedInteger { value: lval },
|
||||||
|
NomoValue::SignedInteger { value: rval },
|
||||||
|
) => Some(NomoValue::SignedInteger {
|
||||||
|
value: *lval + *rval,
|
||||||
|
}),
|
||||||
|
(NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval })
|
||||||
|
| (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => {
|
||||||
|
Some(NomoValue::SignedInteger {
|
||||||
|
value: (i64::try_from(*val).ok()?) + *sval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(NomoValue::String { value: rstr }, NomoValue::String { value: lstr }) => {
|
||||||
|
Some(NomoValue::String {
|
||||||
|
value: Cow::Owned(format!("{rstr}{lstr}")),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_sub(&self, right_value: &NomoValue) -> Option<NomoValue> {
|
||||||
|
macro_rules! op {
|
||||||
|
($left:expr, $right:expr) => {{ ($left) - ($right) }};
|
||||||
|
}
|
||||||
|
match (self, right_value) {
|
||||||
|
(NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => {
|
||||||
|
Some(NomoValue::Integer {
|
||||||
|
value: op!(*lval, *rval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
NomoValue::SignedInteger { value: lval },
|
||||||
|
NomoValue::SignedInteger { value: rval },
|
||||||
|
) => Some(NomoValue::SignedInteger {
|
||||||
|
value: op!(*lval, *rval),
|
||||||
|
}),
|
||||||
|
(NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval })
|
||||||
|
| (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => {
|
||||||
|
Some(NomoValue::SignedInteger {
|
||||||
|
value: op!(i64::try_from(*val).ok()?, *sval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_mul(&self, right_value: &NomoValue) -> Option<NomoValue> {
|
||||||
|
macro_rules! op {
|
||||||
|
($left:expr, $right:expr) => {{ ($left) * ($right) }};
|
||||||
|
}
|
||||||
|
match (self, right_value) {
|
||||||
|
(NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => {
|
||||||
|
Some(NomoValue::Integer {
|
||||||
|
value: op!(*lval, *rval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
NomoValue::SignedInteger { value: lval },
|
||||||
|
NomoValue::SignedInteger { value: rval },
|
||||||
|
) => Some(NomoValue::SignedInteger {
|
||||||
|
value: op!(*lval, *rval),
|
||||||
|
}),
|
||||||
|
(NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval })
|
||||||
|
| (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => {
|
||||||
|
Some(NomoValue::SignedInteger {
|
||||||
|
value: op!(i64::try_from(*val).ok()?, *sval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_div(&self, right_value: &NomoValue) -> Option<NomoValue> {
|
||||||
|
macro_rules! op {
|
||||||
|
($left:expr, $right:expr) => {{ ($left) / ($right) }};
|
||||||
|
}
|
||||||
|
match (self, right_value) {
|
||||||
|
(NomoValue::Integer { value: lval }, NomoValue::Integer { value: rval }) => {
|
||||||
|
Some(NomoValue::Integer {
|
||||||
|
value: op!(*lval, *rval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(
|
||||||
|
NomoValue::SignedInteger { value: lval },
|
||||||
|
NomoValue::SignedInteger { value: rval },
|
||||||
|
) => Some(NomoValue::SignedInteger {
|
||||||
|
value: op!(*lval, *rval),
|
||||||
|
}),
|
||||||
|
(NomoValue::Integer { value: val }, NomoValue::SignedInteger { value: sval })
|
||||||
|
| (NomoValue::SignedInteger { value: sval }, NomoValue::Integer { value: val }) => {
|
||||||
|
Some(NomoValue::SignedInteger {
|
||||||
|
value: op!(i64::try_from(*val).ok()?, *sval),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_and(&self, right_value: &NomoValue) -> Option<NomoValue> {
|
||||||
|
match (self, right_value) {
|
||||||
|
(NomoValue::Bool { value: lval }, NomoValue::Bool { value: rval }) => {
|
||||||
|
Some(NomoValue::Bool {
|
||||||
|
value: *lval && *rval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_or(&self, right_value: &NomoValue) -> Option<NomoValue> {
|
||||||
|
match (self, right_value) {
|
||||||
|
(NomoValue::Bool { value: lval }, NomoValue::Bool { value: rval }) => {
|
||||||
|
Some(NomoValue::Bool {
|
||||||
|
value: *lval || *rval,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn try_to_string(&self) -> Option<String> {
|
||||||
|
match self {
|
||||||
|
NomoValue::String { value } => Some(value.to_string()),
|
||||||
|
NomoValue::Array { .. } => None,
|
||||||
|
NomoValue::Bool { value } => Some(value.to_string()),
|
||||||
|
NomoValue::Object { .. } => None,
|
||||||
|
NomoValue::Integer { value } => Some(value.to_string()),
|
||||||
|
NomoValue::SignedInteger { value } => Some(value.to_string()),
|
||||||
|
NomoValue::Float { value } => Some(value.to_string()),
|
||||||
|
NomoValue::Iterator { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CloneIterator: Iterator<Item = NomoValue> {
|
pub trait CloneIterator: Iterator<Item = NomoValue> {
|
||||||
|
|
|
||||||
27
tests/cases/1-parsed@literals.snap
Normal file
27
tests/cases/1-parsed@literals.snap
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: parsed
|
||||||
|
info:
|
||||||
|
input: "{{ if true }}\n Hello World!\n{{ end }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/literals.nomo
|
||||||
|
---
|
||||||
|
ParsedTemplate {
|
||||||
|
tokens: [
|
||||||
|
[LeftDelim]"{{" (0..2),
|
||||||
|
[Whitespace]" " (2..3),
|
||||||
|
[ConditionalIf]"if" (3..5),
|
||||||
|
[Whitespace]" " (5..6),
|
||||||
|
[Literal(Bool(true))]"true" (6..10),
|
||||||
|
[Whitespace]" " (10..11),
|
||||||
|
[RightDelim]"}}" (11..13),
|
||||||
|
[Whitespace]"\n " (13..18),
|
||||||
|
[Content]"Hello World!" (18..30),
|
||||||
|
[Whitespace]"\n" (30..31),
|
||||||
|
[LeftDelim]"{{" (31..33),
|
||||||
|
[Whitespace]" " (33..34),
|
||||||
|
[End]"end" (34..37),
|
||||||
|
[Whitespace]" " (37..38),
|
||||||
|
[RightDelim]"}}" (38..40),
|
||||||
|
],
|
||||||
|
}
|
||||||
56
tests/cases/1-parsed@maths.snap
Normal file
56
tests/cases/1-parsed@maths.snap
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: parsed
|
||||||
|
info:
|
||||||
|
input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/maths.nomo
|
||||||
|
---
|
||||||
|
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(3))]"3" (8..9),
|
||||||
|
[Whitespace]" " (9..10),
|
||||||
|
[RightDelim]"}}" (10..12),
|
||||||
|
[Whitespace]"\n" (12..13),
|
||||||
|
[LeftDelim]"{{" (13..15),
|
||||||
|
[WantsOutput]"=" (15..16),
|
||||||
|
[Whitespace]" " (16..17),
|
||||||
|
[Literal(Integer(2))]"2" (17..18),
|
||||||
|
[Whitespace]" " (18..19),
|
||||||
|
[Operator(Times)]"*" (19..20),
|
||||||
|
[Whitespace]" " (20..21),
|
||||||
|
[Literal(Integer(3))]"3" (21..22),
|
||||||
|
[Whitespace]" " (22..23),
|
||||||
|
[Operator(Plus)]"+" (23..24),
|
||||||
|
[Whitespace]" " (24..25),
|
||||||
|
[Literal(Integer(4))]"4" (25..26),
|
||||||
|
[Whitespace]" " (26..27),
|
||||||
|
[Operator(Times)]"*" (27..28),
|
||||||
|
[Whitespace]" " (28..29),
|
||||||
|
[Literal(Integer(3))]"3" (29..30),
|
||||||
|
[Whitespace]" " (30..31),
|
||||||
|
[RightDelim]"}}" (31..33),
|
||||||
|
[Whitespace]"\n" (33..34),
|
||||||
|
[LeftDelim]"{{" (34..36),
|
||||||
|
[WantsOutput]"=" (36..37),
|
||||||
|
[Whitespace]" " (37..38),
|
||||||
|
[Literal(Integer(3))]"3" (38..39),
|
||||||
|
[Whitespace]" " (39..40),
|
||||||
|
[Operator(Divide)]"/" (40..41),
|
||||||
|
[Whitespace]" " (41..42),
|
||||||
|
[Literal(Integer(3))]"3" (42..43),
|
||||||
|
[Whitespace]" " (43..44),
|
||||||
|
[Operator(Plus)]"+" (44..45),
|
||||||
|
[Whitespace]" " (45..46),
|
||||||
|
[Literal(Integer(3))]"3" (46..47),
|
||||||
|
[Whitespace]" " (47..48),
|
||||||
|
[RightDelim]"}}" (48..50),
|
||||||
|
],
|
||||||
|
}
|
||||||
44
tests/cases/2-ast@literals.snap
Normal file
44
tests/cases/2-ast@literals.snap
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: ast
|
||||||
|
info:
|
||||||
|
input: "{{ if true }}\n Hello World!\n{{ end }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/literals.nomo
|
||||||
|
---
|
||||||
|
TemplateAst {
|
||||||
|
root: [
|
||||||
|
ConditionalChain {
|
||||||
|
chain: [
|
||||||
|
Block {
|
||||||
|
prev_whitespace_content: None,
|
||||||
|
expression: IfConditional {
|
||||||
|
expression: Literal {
|
||||||
|
source: [Literal(Bool(true))]"true" (6..10),
|
||||||
|
value: Bool {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
post_whitespace_content: Some(
|
||||||
|
[Whitespace]"\n " (13..18),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
ConditionalContent {
|
||||||
|
content: [
|
||||||
|
StaticContent(
|
||||||
|
[Content]"Hello World!" (18..30),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
Block {
|
||||||
|
prev_whitespace_content: Some(
|
||||||
|
[Whitespace]"\n" (30..31),
|
||||||
|
),
|
||||||
|
expression: EndBlock,
|
||||||
|
post_whitespace_content: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
100
tests/cases/2-ast@maths.snap
Normal file
100
tests/cases/2-ast@maths.snap
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: ast
|
||||||
|
info:
|
||||||
|
input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/maths.nomo
|
||||||
|
---
|
||||||
|
TemplateAst {
|
||||||
|
root: [
|
||||||
|
Interpolation {
|
||||||
|
prev_whitespace_content: None,
|
||||||
|
expression: 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
post_whitespace_content: Some(
|
||||||
|
[Whitespace]"\n" (12..13),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Interpolation {
|
||||||
|
prev_whitespace_content: None,
|
||||||
|
expression: Operation {
|
||||||
|
op: Plus,
|
||||||
|
lhs: Operation {
|
||||||
|
op: Times,
|
||||||
|
lhs: Literal {
|
||||||
|
source: [Literal(Integer(2))]"2" (17..18),
|
||||||
|
value: Integer {
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Integer(3))]"3" (21..22),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Operation {
|
||||||
|
op: Times,
|
||||||
|
lhs: Literal {
|
||||||
|
source: [Literal(Integer(4))]"4" (25..26),
|
||||||
|
value: Integer {
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Integer(3))]"3" (29..30),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
post_whitespace_content: Some(
|
||||||
|
[Whitespace]"\n" (33..34),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
Interpolation {
|
||||||
|
prev_whitespace_content: None,
|
||||||
|
expression: Operation {
|
||||||
|
op: Plus,
|
||||||
|
lhs: Operation {
|
||||||
|
op: Divide,
|
||||||
|
lhs: Literal {
|
||||||
|
source: [Literal(Integer(3))]"3" (38..39),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Integer(3))]"3" (42..43),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rhs: Literal {
|
||||||
|
source: [Literal(Integer(3))]"3" (46..47),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
post_whitespace_content: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
55
tests/cases/3-instructions@literals.snap
Normal file
55
tests/cases/3-instructions@literals.snap
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: emit
|
||||||
|
info:
|
||||||
|
input: "{{ if true }}\n Hello World!\n{{ end }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/literals.nomo
|
||||||
|
---
|
||||||
|
VMInstructions {
|
||||||
|
labels: {
|
||||||
|
LabelSlot {
|
||||||
|
index: 0,
|
||||||
|
}: 7,
|
||||||
|
LabelSlot {
|
||||||
|
index: 2,
|
||||||
|
}: 7,
|
||||||
|
},
|
||||||
|
instructions: [
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Bool(true))]"true" (6..10),
|
||||||
|
value: Bool {
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
JumpIfNotTrue {
|
||||||
|
emit_slot: VariableSlot {
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
jump: LabelSlot {
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppendContent {
|
||||||
|
content: "\n " (13..18),
|
||||||
|
},
|
||||||
|
AppendContent {
|
||||||
|
content: "Hello World!" (18..30),
|
||||||
|
},
|
||||||
|
AppendContent {
|
||||||
|
content: "\n" (30..31),
|
||||||
|
},
|
||||||
|
Jump {
|
||||||
|
jump: LabelSlot {
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppendContent {
|
||||||
|
content: "\n " (13..18),
|
||||||
|
},
|
||||||
|
NoOp,
|
||||||
|
],
|
||||||
|
}
|
||||||
187
tests/cases/3-instructions@maths.snap
Normal file
187
tests/cases/3-instructions@maths.snap
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: emit
|
||||||
|
info:
|
||||||
|
input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/maths.nomo
|
||||||
|
---
|
||||||
|
VMInstructions {
|
||||||
|
labels: {},
|
||||||
|
instructions: [
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(5))]"5" (4..5),
|
||||||
|
value: Integer {
|
||||||
|
value: 5,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(3))]"3" (8..9),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: Times,
|
||||||
|
left_slot: VariableSlot {
|
||||||
|
index: 1,
|
||||||
|
},
|
||||||
|
right_slot: VariableSlot {
|
||||||
|
index: 2,
|
||||||
|
},
|
||||||
|
result_slot: VariableSlot {
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EmitFromSlot {
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppendContent {
|
||||||
|
content: "\n" (12..13),
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(2))]"2" (17..18),
|
||||||
|
value: Integer {
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(3))]"3" (21..22),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: Times,
|
||||||
|
left_slot: VariableSlot {
|
||||||
|
index: 5,
|
||||||
|
},
|
||||||
|
right_slot: VariableSlot {
|
||||||
|
index: 6,
|
||||||
|
},
|
||||||
|
result_slot: VariableSlot {
|
||||||
|
index: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(4))]"4" (25..26),
|
||||||
|
value: Integer {
|
||||||
|
value: 4,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(3))]"3" (29..30),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 9,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: Times,
|
||||||
|
left_slot: VariableSlot {
|
||||||
|
index: 8,
|
||||||
|
},
|
||||||
|
right_slot: VariableSlot {
|
||||||
|
index: 9,
|
||||||
|
},
|
||||||
|
result_slot: VariableSlot {
|
||||||
|
index: 7,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: Plus,
|
||||||
|
left_slot: VariableSlot {
|
||||||
|
index: 4,
|
||||||
|
},
|
||||||
|
right_slot: VariableSlot {
|
||||||
|
index: 7,
|
||||||
|
},
|
||||||
|
result_slot: VariableSlot {
|
||||||
|
index: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EmitFromSlot {
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AppendContent {
|
||||||
|
content: "\n" (33..34),
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(3))]"3" (38..39),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(3))]"3" (42..43),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 13,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: Divide,
|
||||||
|
left_slot: VariableSlot {
|
||||||
|
index: 12,
|
||||||
|
},
|
||||||
|
right_slot: VariableSlot {
|
||||||
|
index: 13,
|
||||||
|
},
|
||||||
|
result_slot: VariableSlot {
|
||||||
|
index: 11,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LoadLiteralToSlot {
|
||||||
|
source: [Literal(Integer(3))]"3" (46..47),
|
||||||
|
value: Integer {
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 14,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
MathOperate {
|
||||||
|
op: Plus,
|
||||||
|
left_slot: VariableSlot {
|
||||||
|
index: 11,
|
||||||
|
},
|
||||||
|
right_slot: VariableSlot {
|
||||||
|
index: 14,
|
||||||
|
},
|
||||||
|
result_slot: VariableSlot {
|
||||||
|
index: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
EmitFromSlot {
|
||||||
|
slot: VariableSlot {
|
||||||
|
index: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
9
tests/cases/4-output@literals.snap
Normal file
9
tests/cases/4-output@literals.snap
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: output
|
||||||
|
info:
|
||||||
|
input: "{{ if true }}\n Hello World!\n{{ end }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/literals.nomo
|
||||||
|
---
|
||||||
|
"\n Hello World!\n"
|
||||||
9
tests/cases/4-output@maths.snap
Normal file
9
tests/cases/4-output@maths.snap
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
source: tests/file_tests.rs
|
||||||
|
expression: output
|
||||||
|
info:
|
||||||
|
input: "{{= 5 * 3 }}\n{{= 2 * 3 + 4 * 3 }}\n{{= 3 / 3 + 3 }}"
|
||||||
|
context: {}
|
||||||
|
input_file: tests/cases/maths.nomo
|
||||||
|
---
|
||||||
|
"15\n18\n4"
|
||||||
6
tests/cases/literals.nomo
Normal file
6
tests/cases/literals.nomo
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
}
|
||||||
|
---
|
||||||
|
{{ if true }}
|
||||||
|
Hello World!
|
||||||
|
{{ end }}
|
||||||
6
tests/cases/maths.nomo
Normal file
6
tests/cases/maths.nomo
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
}
|
||||||
|
---
|
||||||
|
{{= 5 * 3 }}
|
||||||
|
{{= 2 * 3 + 4 * 3 }}
|
||||||
|
{{= 3 / 3 + 3 }}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue