diff --git a/src/compiler/mod.rs b/src/compiler/mod.rs index 00fe68e..4fc3644 100644 --- a/src/compiler/mod.rs +++ b/src/compiler/mod.rs @@ -108,6 +108,20 @@ pub enum Instruction { args: Vec, slot: VariableSlot, }, + LoadFromSlotToSlot { + from_slot: VariableSlot, + to_slot: VariableSlot, + }, + JumpIfUndefined { + slot: VariableSlot, + jump: LabelSlot, + }, + IndexSlotToSlot { + name: NomoInput, + from_slot: VariableSlot, + to_slot: VariableSlot, + fail_on_not_found: bool, + }, } #[derive(Debug, Clone)] @@ -452,6 +466,63 @@ fn emit_expr_load( fail_on_not_found: false, }); } + TemplateAstExpr::AccessOperation { op, lhs, rhs } => { + assert_eq!( + op, + &TokenOperator::Dot, + "Only dot can be used to access variables, this is a bug" + ); + fn inner_access_op( + eval: &mut Vec, + end_label: LabelSlot, + current_slot: VariableSlot, + next: &TemplateAstExpr<'_>, + ) { + match next { + TemplateAstExpr::ConditionalAccess(template_token) + | TemplateAstExpr::VariableAccess(template_token) => { + eval.push(Instruction::IndexSlotToSlot { + name: template_token.source().clone(), + from_slot: current_slot, + to_slot: current_slot, + fail_on_not_found: matches!( + next, + TemplateAstExpr::VariableAccess { .. } + ), + }); + eval.push(Instruction::JumpIfUndefined { + slot: current_slot, + jump: end_label, + }); + } + TemplateAstExpr::AccessOperation { op, lhs, rhs } => { + assert_eq!( + op, + &TokenOperator::Dot, + "Only dot can be used to access variables, this is a bug" + ); + + inner_access_op(eval, end_label, current_slot, lhs); + inner_access_op(eval, end_label, current_slot, rhs); + } + _ => unreachable!(), + } + } + + let end_label = machine.reserve_label(); + let start_slot = machine.reserve_slot(); + emit_expr_load(machine, eval, start_slot, lhs); + eval.push(Instruction::JumpIfUndefined { + slot: start_slot, + jump: end_label, + }); + inner_access_op(eval, end_label, start_slot, rhs); + machine.assign_label(end_label, eval.len()); + eval.push(Instruction::LoadFromSlotToSlot { + from_slot: start_slot, + to_slot: emit_slot, + }); + } TemplateAstExpr::Literal { source, value } => { eval.push(Instruction::LoadLiteralToSlot { source: source.clone(), @@ -492,7 +563,6 @@ fn emit_expr_load( } TemplateAstExpr::ConditionalChain { .. } => todo!(), TemplateAstExpr::ElseConditional { .. } => todo!(), - TemplateAstExpr::AccessOperation { .. } => todo!(), TemplateAstExpr::EndBlock => todo!(), TemplateAstExpr::Block { .. } => todo!(), TemplateAstExpr::ForChain { .. } => todo!(), diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 62a013f..283432a 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -119,7 +119,7 @@ pub fn execute( } else if matches!(value, NomoValue::Undefined) { String::new() } else { - panic!("Unknown variable"); + panic!("Could not print out value {value:?}"); }; output.push_str(&value); @@ -243,6 +243,46 @@ pub fn execute( scopes.insert_into_slot(*slot, value); } + Instruction::LoadFromSlotToSlot { from_slot, to_slot } => { + let value = scopes.get(from_slot); + scopes.insert_into_slot(*to_slot, value.clone()); + } + Instruction::JumpIfUndefined { slot, jump } => { + let is_undefined = matches!(scopes.get(slot), NomoValue::Undefined); + if is_undefined { + let Some(new_ip) = vm.labels.get(jump) else { + return Err(EvaluationError::LabelNotFound); + }; + + ip = *new_ip; + continue; + } + } + Instruction::IndexSlotToSlot { + name, + from_slot, + to_slot, + fail_on_not_found, + } => { + let value = scopes.get(from_slot); + + match value { + NomoValue::Object { value } => { + let value = value.get(name.as_str()); + + let value = if let Some(value) = value { + value.clone() + } else if *fail_on_not_found { + panic!("Could not index"); + } else { + NomoValue::Undefined + }; + + scopes.insert_into_slot(dbg!(*to_slot), dbg!(value)); + } + _ => panic!("Invalid indexing"), + } + } } ip += 1; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index d7e1271..43481a2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -715,10 +715,10 @@ fn parse_expression<'input>( Left MathOperation GreaterOrEqual => 15, Left MathOperation Lesser => 15, Left MathOperation LesserOrEqual => 15, - Left AccessOperation Dot => 23, + Left AccessOperation Dot => 22, ] }).postfix(dispatch! { surrounded(ws, parse_operator); - TokenOperator::QuestionMark => Postfix(22, |input, rhs| { + TokenOperator::QuestionMark => Postfix(23, |input, rhs| { match rhs { TemplateAstExpr::VariableAccess(access) => Ok(TemplateAstExpr::ConditionalAccess(access)), _ => Err(AstError::from_input(input)), diff --git a/src/value.rs b/src/value.rs index c587f82..ab317bb 100644 --- a/src/value.rs +++ b/src/value.rs @@ -54,6 +54,8 @@ impl NomoValue { pub fn as_bool(&self) -> Option { if let Self::Bool { value } = self { Some(*value) + } else if let Self::Undefined = self { + Some(false) } else { None } @@ -438,7 +440,14 @@ impl TryFrom for NomoValue { .map(TryInto::try_into) .collect::>()?, }), - serde_json::Value::Object(_map) => todo!(), + serde_json::Value::Object(map) => { + let value = map + .into_iter() + .map(|(key, value)| Ok((key, NomoValue::try_from(value)?))) + .collect::>()?; + + Ok(NomoValue::Object { value }) + } } } } diff --git a/tests/cases/deep_access.1-parsed.snap b/tests/cases/deep_access.1-parsed.snap new file mode 100644 index 0000000..ecc2284 --- /dev/null +++ b/tests/cases/deep_access.1-parsed.snap @@ -0,0 +1,74 @@ +--- +source: tests/file_tests.rs +expression: parsed +info: + input: "{{ if world?.active }}\nHello {{= world.name }}\n{{ end }}\n\n{{ if world?.foo?.bar?.active }}\n Weird!\n{{ else }}\n Deep access is working :)\n{{ end }}" + context: + world: + active: true + name: World! +--- +ParsedTemplate { + tokens: [ + [LeftDelim]"{{" (0..2), + [Whitespace]" " (2..3), + [ConditionalIf]"if" (3..5), + [Whitespace]" " (5..6), + [Ident]"world" (6..11), + [Operator(QuestionMark)]"?" (11..12), + [Operator(Dot)]"." (12..13), + [Ident]"active" (13..19), + [Whitespace]" " (19..20), + [RightDelim]"}}" (20..22), + [Whitespace]"\n" (22..23), + [Content]"Hello" (23..28), + [Whitespace]" " (28..29), + [LeftDelim]"{{" (29..31), + [WantsOutput]"=" (31..32), + [Whitespace]" " (32..33), + [Ident]"world" (33..38), + [Operator(Dot)]"." (38..39), + [Ident]"name" (39..43), + [Whitespace]" " (43..44), + [RightDelim]"}}" (44..46), + [Whitespace]"\n" (46..47), + [LeftDelim]"{{" (47..49), + [Whitespace]" " (49..50), + [End]"end" (50..53), + [Whitespace]" " (53..54), + [RightDelim]"}}" (54..56), + [Whitespace]"\n\n" (56..58), + [LeftDelim]"{{" (58..60), + [Whitespace]" " (60..61), + [ConditionalIf]"if" (61..63), + [Whitespace]" " (63..64), + [Ident]"world" (64..69), + [Operator(QuestionMark)]"?" (69..70), + [Operator(Dot)]"." (70..71), + [Ident]"foo" (71..74), + [Operator(QuestionMark)]"?" (74..75), + [Operator(Dot)]"." (75..76), + [Ident]"bar" (76..79), + [Operator(QuestionMark)]"?" (79..80), + [Operator(Dot)]"." (80..81), + [Ident]"active" (81..87), + [Whitespace]" " (87..88), + [RightDelim]"}}" (88..90), + [Whitespace]"\n " (90..95), + [Content]"Weird!" (95..101), + [Whitespace]"\n" (101..102), + [LeftDelim]"{{" (102..104), + [Whitespace]" " (104..105), + [ConditionalElse]"else" (105..109), + [Whitespace]" " (109..110), + [RightDelim]"}}" (110..112), + [Whitespace]"\n " (112..117), + [Content]"Deep access is working :)" (117..142), + [Whitespace]"\n" (142..143), + [LeftDelim]"{{" (143..145), + [Whitespace]" " (145..146), + [End]"end" (146..149), + [Whitespace]" " (149..150), + [RightDelim]"}}" (150..152), + ], +} diff --git a/tests/cases/deep_access.2-ast.snap b/tests/cases/deep_access.2-ast.snap new file mode 100644 index 0000000..bd3439e --- /dev/null +++ b/tests/cases/deep_access.2-ast.snap @@ -0,0 +1,131 @@ +--- +source: tests/file_tests.rs +expression: ast +info: + input: "{{ if world?.active }}\nHello {{= world.name }}\n{{ end }}\n\n{{ if world?.foo?.bar?.active }}\n Weird!\n{{ else }}\n Deep access is working :)\n{{ end }}" + context: + world: + active: true + name: World! +--- +TemplateAst { + root: [ + ConditionalChain { + chain: [ + Block { + prev_whitespace_content: None, + expression: IfConditional { + expression: AccessOperation { + op: Dot, + lhs: ConditionalAccess( + [Ident]"world" (6..11), + ), + rhs: VariableAccess( + [Ident]"active" (13..19), + ), + }, + }, + post_whitespace_content: Some( + [Whitespace]"\n" (22..23), + ), + }, + ConditionalContent { + content: [ + StaticContent( + [Content]"Hello" (23..28), + ), + Interpolation { + prev_whitespace_content: Some( + [Whitespace]" " (28..29), + ), + expression: AccessOperation { + op: Dot, + lhs: VariableAccess( + [Ident]"world" (33..38), + ), + rhs: VariableAccess( + [Ident]"name" (39..43), + ), + }, + post_whitespace_content: Some( + [Whitespace]"\n" (46..47), + ), + }, + ], + }, + Block { + prev_whitespace_content: None, + expression: EndBlock, + post_whitespace_content: Some( + [Whitespace]"\n\n" (56..58), + ), + }, + ], + }, + ConditionalChain { + chain: [ + Block { + prev_whitespace_content: None, + expression: IfConditional { + expression: AccessOperation { + op: Dot, + lhs: AccessOperation { + op: Dot, + lhs: AccessOperation { + op: Dot, + lhs: ConditionalAccess( + [Ident]"world" (64..69), + ), + rhs: ConditionalAccess( + [Ident]"foo" (71..74), + ), + }, + rhs: ConditionalAccess( + [Ident]"bar" (76..79), + ), + }, + rhs: VariableAccess( + [Ident]"active" (81..87), + ), + }, + }, + post_whitespace_content: Some( + [Whitespace]"\n " (90..95), + ), + }, + ConditionalContent { + content: [ + StaticContent( + [Content]"Weird!" (95..101), + ), + ], + }, + Block { + prev_whitespace_content: Some( + [Whitespace]"\n" (101..102), + ), + expression: ElseConditional { + expression: None, + }, + post_whitespace_content: Some( + [Whitespace]"\n " (112..117), + ), + }, + ConditionalContent { + content: [ + StaticContent( + [Content]"Deep access is working :)" (117..142), + ), + ], + }, + Block { + prev_whitespace_content: Some( + [Whitespace]"\n" (142..143), + ), + expression: EndBlock, + post_whitespace_content: None, + }, + ], + }, + ], +} diff --git a/tests/cases/deep_access.3-instructions.snap b/tests/cases/deep_access.3-instructions.snap new file mode 100644 index 0000000..dd12a27 --- /dev/null +++ b/tests/cases/deep_access.3-instructions.snap @@ -0,0 +1,313 @@ +--- +source: tests/file_tests.rs +expression: emit +info: + input: "{{ if world?.active }}\nHello {{= world.name }}\n{{ end }}\n\n{{ if world?.foo?.bar?.active }}\n Weird!\n{{ else }}\n Deep access is working :)\n{{ end }}" + context: + world: + active: true + name: World! +--- +VMInstructions { + labels: { + LabelSlot { + index: 0, + }: 18, + LabelSlot { + index: 2, + }: 4, + LabelSlot { + index: 4, + }: 18, + LabelSlot { + index: 6, + }: 13, + LabelSlot { + index: 8, + }: 43, + LabelSlot { + index: 10, + }: 31, + LabelSlot { + index: 12, + }: 27, + LabelSlot { + index: 14, + }: 23, + LabelSlot { + index: 16, + }: 38, + }, + instructions: [ + LoadFromContextToSlot { + name: "world" (6..11), + slot: VariableSlot { + index: 3, + }, + fail_on_not_found: false, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 3, + }, + jump: LabelSlot { + index: 2, + }, + }, + IndexSlotToSlot { + name: "active" (13..19), + from_slot: VariableSlot { + index: 3, + }, + to_slot: VariableSlot { + index: 3, + }, + fail_on_not_found: true, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 3, + }, + jump: LabelSlot { + index: 2, + }, + }, + LoadFromSlotToSlot { + from_slot: VariableSlot { + index: 3, + }, + to_slot: VariableSlot { + index: 1, + }, + }, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 1, + }, + jump: LabelSlot { + index: 4, + }, + }, + AppendContent { + content: "\n" (22..23), + }, + AppendContent { + content: "Hello" (23..28), + }, + AppendContent { + content: " " (28..29), + }, + LoadFromContextToSlot { + name: "world" (33..38), + slot: VariableSlot { + index: 7, + }, + fail_on_not_found: true, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 7, + }, + jump: LabelSlot { + index: 6, + }, + }, + IndexSlotToSlot { + name: "name" (39..43), + from_slot: VariableSlot { + index: 7, + }, + to_slot: VariableSlot { + index: 7, + }, + fail_on_not_found: true, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 7, + }, + jump: LabelSlot { + index: 6, + }, + }, + LoadFromSlotToSlot { + from_slot: VariableSlot { + index: 7, + }, + to_slot: VariableSlot { + index: 5, + }, + }, + EmitFromSlot { + slot: VariableSlot { + index: 5, + }, + }, + AppendContent { + content: "\n" (46..47), + }, + Jump { + jump: LabelSlot { + index: 0, + }, + }, + AppendContent { + content: "\n" (22..23), + }, + AppendContent { + content: "\n\n" (56..58), + }, + LoadFromContextToSlot { + name: "world" (64..69), + slot: VariableSlot { + index: 15, + }, + fail_on_not_found: false, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 15, + }, + jump: LabelSlot { + index: 14, + }, + }, + IndexSlotToSlot { + name: "foo" (71..74), + from_slot: VariableSlot { + index: 15, + }, + to_slot: VariableSlot { + index: 15, + }, + fail_on_not_found: false, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 15, + }, + jump: LabelSlot { + index: 14, + }, + }, + LoadFromSlotToSlot { + from_slot: VariableSlot { + index: 15, + }, + to_slot: VariableSlot { + index: 13, + }, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 13, + }, + jump: LabelSlot { + index: 12, + }, + }, + IndexSlotToSlot { + name: "bar" (76..79), + from_slot: VariableSlot { + index: 13, + }, + to_slot: VariableSlot { + index: 13, + }, + fail_on_not_found: false, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 13, + }, + jump: LabelSlot { + index: 12, + }, + }, + LoadFromSlotToSlot { + from_slot: VariableSlot { + index: 13, + }, + to_slot: VariableSlot { + index: 11, + }, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 11, + }, + jump: LabelSlot { + index: 10, + }, + }, + IndexSlotToSlot { + name: "active" (81..87), + from_slot: VariableSlot { + index: 11, + }, + to_slot: VariableSlot { + index: 11, + }, + fail_on_not_found: true, + }, + JumpIfUndefined { + slot: VariableSlot { + index: 11, + }, + jump: LabelSlot { + index: 10, + }, + }, + LoadFromSlotToSlot { + from_slot: VariableSlot { + index: 11, + }, + to_slot: VariableSlot { + index: 9, + }, + }, + JumpIfNotTrue { + emit_slot: VariableSlot { + index: 9, + }, + jump: LabelSlot { + index: 16, + }, + }, + AppendContent { + content: "\n " (90..95), + }, + AppendContent { + content: "Weird!" (95..101), + }, + AppendContent { + content: "\n" (101..102), + }, + Jump { + jump: LabelSlot { + index: 8, + }, + }, + AppendContent { + content: "\n " (90..95), + }, + AppendContent { + content: "\n " (112..117), + }, + AppendContent { + content: "Deep access is working :)" (117..142), + }, + AppendContent { + content: "\n" (142..143), + }, + Jump { + jump: LabelSlot { + index: 8, + }, + }, + AppendContent { + content: "\n " (112..117), + }, + NoOp, + ], +} diff --git a/tests/cases/deep_access.4-output.snap b/tests/cases/deep_access.4-output.snap new file mode 100644 index 0000000..a6d57e5 --- /dev/null +++ b/tests/cases/deep_access.4-output.snap @@ -0,0 +1,11 @@ +--- +source: tests/file_tests.rs +expression: output +info: + input: "{{ if world?.active }}\nHello {{= world.name }}\n{{ end }}\n\n{{ if world?.foo?.bar?.active }}\n Weird!\n{{ else }}\n Deep access is working :)\n{{ end }}" + context: + world: + active: true + name: World! +--- +"\nHello World!\n\n\n\n Deep access is working :)\n" diff --git a/tests/cases/deep_access.nomo b/tests/cases/deep_access.nomo new file mode 100644 index 0000000..cde34cb --- /dev/null +++ b/tests/cases/deep_access.nomo @@ -0,0 +1,16 @@ +{ + "world": { + "active": true, + "name": "World!" + } +} +--- +{{ if world?.active }} +Hello {{= world.name }} +{{ end }} + +{{ if world?.foo?.bar?.active }} + Weird! +{{ else }} + Deep access is working :) +{{ end }} \ No newline at end of file