From 0370de40a16d84a52ac5edaadd888bbfef3f5c09 Mon Sep 17 00:00:00 2001 From: Olivier 'reivilibre Date: Wed, 1 Mar 2023 21:09:10 +0000 Subject: [PATCH] Simplify and use locators in the IR and interpreter --- hornbeam_grammar/src/lib.rs | 28 ++++ ...r__parser__tests__fragments_and_slots.snap | 30 +++- ...m_grammar__parser__tests__if_blocks-2.snap | 18 ++- ...eam_grammar__parser__tests__if_blocks.snap | 18 ++- ..._grammar__parser__tests__localisation.snap | 34 ++++- ..._parser__tests__simple_parses_correct.snap | 6 +- ...__tests__string_interpolations_nested.snap | 6 +- ...ts__supply_slots_to_components_only-2.snap | 6 +- hornbeam_interpreter/src/engine.rs | 134 +++++++++--------- hornbeam_interpreter/src/interface.rs | 16 +-- hornbeam_interpreter/src/lib.rs | 6 +- hornbeam_ir/src/ast_to_ir.rs | 32 ++--- hornbeam_ir/src/ir.rs | 26 ++-- hornbeam_ir/src/lib.rs | 2 +- hornbeam_ir/src/peephole.rs | 12 +- ...ornbeam_ir__ast_to_ir__tests__compile.snap | 75 ++++++++-- ..._ast_to_ir__tests__compile_attributes.snap | 78 ++++++++-- ...st_to_ir__tests__pull_out_entrypoints.snap | 24 ++++ ...am_ir__peephole__tests__adjacent_text.snap | 40 ++++-- ..._ir__peephole__tests__with_attributes.snap | 43 +++++- 20 files changed, 471 insertions(+), 163 deletions(-) diff --git a/hornbeam_grammar/src/lib.rs b/hornbeam_grammar/src/lib.rs index 0ca6034..3eacfa6 100644 --- a/hornbeam_grammar/src/lib.rs +++ b/hornbeam_grammar/src/lib.rs @@ -1,6 +1,8 @@ pub mod ast; mod parser; + use serde::Serialize; +use std::fmt::{Display, Formatter}; use arc_interner::ArcIntern; pub use parser::parse_template; @@ -31,4 +33,30 @@ impl Locator { column: col.min(u16::MAX as usize) as u16, } } + + pub fn empty() -> Locator { + Locator { + filename: intern(""), + line: 0, + column: 0, + } + } + + pub fn is_empty(&self) -> bool { + self.filename.is_empty() && self.line == 0 && self.column == 0 + } +} + +impl Display for Locator { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if self.is_empty() { + write!(f, "unknown location") + } else { + write!( + f, + "file {:?} @ line {} (col {})", + self.filename, self.line, self.column + ) + } + } } diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__fragments_and_slots.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__fragments_and_slots.snap index c071cb8..9cc7605 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__fragments_and_slots.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__fragments_and_slots.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\ndiv\n fragment BobbyDazzler\n span\n \"Wow!\"\n fragment MainPart\n slot :main\n div\n optional slot :footer\n \"#).unwrap()" +expression: "parse_template(r#\"\ndiv\n fragment BobbyDazzler\n span\n \"Wow!\"\n fragment MainPart\n slot :main\n div\n optional slot :footer\n \"#,\n \"inp\").unwrap()" --- blocks: - HtmlElement: @@ -18,22 +18,50 @@ blocks: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 4 + column: 9 + loc: + filename: inp + line: 3 + column: 5 - DefineFragment: name: MainPart blocks: - DefineExpandSlot: name: main optional: false + loc: + filename: inp + line: 7 + column: 9 - HtmlElement: name: div children: - DefineExpandSlot: name: footer optional: true + loc: + filename: inp + line: 9 + column: 13 classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 8 + column: 9 + loc: + filename: inp + line: 6 + column: 5 classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks-2.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks-2.snap index a608306..c15ad5b 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks-2.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks-2.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\nif 1 + 1 == 2\n div\n \"Phew, safe!\"\nelse if 1 + 2 - 1 == 3 // not too far off, I suppose\n Warning\n \"Not quite, but fairly close. What kind of world is this?\"\nelse // peculiar.\n \"Not even close, eh?\"\n \"#).unwrap()" +expression: "parse_template(r#\"\nif 1 + 1 == 2\n div\n \"Phew, safe!\"\nelse if 1 + 2 - 1 == 3 // not too far off, I suppose\n Warning\n \"Not quite, but fairly close. What kind of world is this?\"\nelse // peculiar.\n \"Not even close, eh?\"\n \"#,\n \"inp\").unwrap()" --- blocks: - IfBlock: @@ -27,6 +27,10 @@ blocks: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 3 + column: 3 else_blocks: - IfBlock: condition: @@ -56,8 +60,20 @@ blocks: pieces: - Literal: "Not quite, but fairly close. What kind of world is this?" attributes: {} + loc: + filename: inp + line: 6 + column: 3 else_blocks: - Text: pieces: - Literal: "Not even close, eh?" + loc: + filename: inp + line: 5 + column: 6 + loc: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks.snap index 1268c91..4817ad0 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__if_blocks.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\nif 10 / 2 == 5 or $point.x == 42 // div for a div?\n div\n \"#).unwrap()" +expression: "parse_template(r#\"\nif 10 / 2 == 5 or $point.x == 42 // div for a div?\n div\n \"#,\n \"inp\").unwrap()" --- blocks: - IfBlock: @@ -26,7 +26,15 @@ blocks: obj: Variable: name: point + loc: + filename: inp + line: 2 + column: 19 ident: x + loc: + filename: inp + line: 2 + column: 25 right: IntLiteral: val: 42 @@ -37,5 +45,13 @@ blocks: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 3 + column: 5 else_blocks: [] + loc: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__localisation.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__localisation.snap index 0667bd3..ec07c13 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__localisation.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__localisation.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\ndiv\n span\n @header-wow\n MainBody\n ''\n @body-welcome\n @body-msg{count = $messages.len()}\n ''\n Footer\n @footer-copyright{year = today().year}\n \"#).unwrap()" +expression: "parse_template(r#\"\ndiv\n span\n @header-wow\n MainBody\n ''\n @body-welcome\n @body-msg{count = $messages.len()}\n ''\n Footer\n @footer-copyright{year = today().year}\n \"#,\n \"inp\").unwrap()" --- blocks: - HtmlElement: @@ -17,6 +17,10 @@ blocks: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 3 + column: 5 - ComponentElement: name: MainBody slots: @@ -35,9 +39,21 @@ blocks: obj: Variable: name: messages + loc: + filename: inp + line: 8 + column: 27 ident: len args: [] + loc: + filename: inp + line: 8 + column: 36 attributes: {} + loc: + filename: inp + line: 5 + column: 5 - ComponentElement: name: Footer slots: @@ -53,9 +69,25 @@ blocks: FunctionCall: name: today args: [] + loc: + filename: inp + line: 11 + column: 34 ident: year + loc: + filename: inp + line: 11 + column: 41 attributes: {} + loc: + filename: inp + line: 10 + column: 5 classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__simple_parses_correct.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__simple_parses_correct.snap index f7340cf..7b03b4a 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__simple_parses_correct.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__simple_parses_correct.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a
\ndiv\n \"#).unwrap()" +expression: "parse_template(r#\"\n// This is a simple Hornbeam template that just shows a
\ndiv\n \"#,\n \"inp\").unwrap()" --- blocks: - HtmlElement: @@ -9,4 +9,8 @@ blocks: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 3 + column: 1 diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__string_interpolations_nested.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__string_interpolations_nested.snap index eebf03b..514f802 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__string_interpolations_nested.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__string_interpolations_nested.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\nMyComponent\n :someslot\n ''\n ${\"abc\" + \"def${ 1 + 1 }\"}\n Not too bad now.\n ''\n \"#).unwrap()" +expression: "parse_template(r#\"\nMyComponent\n :someslot\n ''\n ${\"abc\" + \"def${ 1 + 1 }\"}\n Not too bad now.\n ''\n \"#,\n \"inp\").unwrap()" --- blocks: - ComponentElement: @@ -30,4 +30,8 @@ blocks: - Literal: "\n" - Literal: Not too bad now. attributes: {} + loc: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__supply_slots_to_components_only-2.snap b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__supply_slots_to_components_only-2.snap index 717dc0b..5d620cb 100644 --- a/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__supply_slots_to_components_only-2.snap +++ b/hornbeam_grammar/src/snapshots/hornbeam_grammar__parser__tests__supply_slots_to_components_only-2.snap @@ -1,6 +1,6 @@ --- source: hornbeam_grammar/src/parser.rs -expression: "parse_template(r#\"\nMyComponent\n :someslot\n \"That's better!\"\n \"#).unwrap()" +expression: "parse_template(r#\"\nMyComponent\n :someslot\n \"That's better!\"\n \"#,\n \"inp\").unwrap()" --- blocks: - ComponentElement: @@ -11,4 +11,8 @@ blocks: pieces: - Literal: "That's better!" attributes: {} + loc: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_interpreter/src/engine.rs b/hornbeam_interpreter/src/engine.rs index 39be15f..9ec8ab4 100644 --- a/hornbeam_interpreter/src/engine.rs +++ b/hornbeam_interpreter/src/engine.rs @@ -3,6 +3,7 @@ use crate::InterpreterError; use async_recursion::async_recursion; use bevy_reflect::{FromReflect, Reflect, ReflectRef}; use fluent_templates::lazy_static::lazy_static; +use hornbeam_grammar::Locator; use hornbeam_ir::ir::{Expression, Step, StepDef, StringPiece}; use itertools::Itertools; use std::any::{Any, TypeId}; @@ -12,22 +13,22 @@ use std::fmt::Write; use std::ops::Deref; use std::sync::Arc; -pub(crate) struct FilledSlot<'a, L> { +pub(crate) struct FilledSlot<'a> { pub scope_idx: usize, - pub steps: &'a [Step], + pub steps: &'a [Step], } -pub(crate) struct Scope<'a, L> { +pub(crate) struct Scope<'a> { pub variables: BTreeMap, - pub slots: BTreeMap>, + pub slots: BTreeMap>, } -pub(crate) struct Interpreter<'a, L, O, LS> { +pub(crate) struct Interpreter<'a, O, LS> { pub(crate) entrypoint: String, - pub(crate) program: &'a BTreeMap>>>, + pub(crate) program: &'a BTreeMap>>, pub(crate) output: O, pub(crate) localisation: Arc, - pub(crate) scopes: Vec>, + pub(crate) scopes: Vec>, } #[derive(Debug)] @@ -90,14 +91,12 @@ impl Clone for Value { } } -impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> - Interpreter<'a, L, O, LS> -{ +impl<'a, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Send> Interpreter<'a, O, LS> { #[async_recursion] pub async fn run_steps( &mut self, scope_idx: usize, - steps: &'a [Step], + steps: &'a [Step], ) -> Result<(), InterpreterError> { for step in steps { self.run_step(scope_idx, step).await?; @@ -108,7 +107,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + pub async fn run_step( &mut self, scope_idx: usize, - step: &'a Step, + step: &'a Step, ) -> Result<(), InterpreterError> { match &step.def { StepDef::WriteLiteral { escape, text } => { @@ -128,9 +127,9 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + } } StepDef::WriteEval { escape, expr } => { - let val = self.evaluate_expression(scope_idx, expr)?; + let val = self.evaluate_expression(scope_idx, expr, &step.locator)?; let mut buf = String::new(); - self.write_out_value_as_display_string(val, &mut buf)?; + self.write_out_value_as_display_string(val, &mut buf, &step.locator)?; let to_write = if *escape { html_escape::encode_safe(&buf) @@ -148,7 +147,8 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + true_steps, false_steps, } => { - let evaled_condition = self.evaluate_expression(scope_idx, condition)?; + let evaled_condition = + self.evaluate_expression(scope_idx, condition, &step.locator)?; let steps = match evaled_condition { Value::Bool(true) => true_steps, Value::Bool(false) => false_steps, @@ -156,7 +156,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + return Err(InterpreterError::TypeError { context: "If".to_string(), conflict: format!("condition {other:?} is not a boolean."), - location: "".to_string(), + location: step.locator.clone(), }); } }; @@ -169,7 +169,8 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + body_steps, empty_steps, } => { - let iterable_evaled = self.evaluate_expression(scope_idx, iterable)?; + let iterable_evaled = + self.evaluate_expression(scope_idx, iterable, &step.locator)?; match iterable_evaled { Value::List(list) => { @@ -191,7 +192,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + return Err(InterpreterError::TypeError { context: "For".to_string(), conflict: format!("not iterable: {other:?}."), - location: "".to_string(), + location: step.locator.clone(), }); } } @@ -201,7 +202,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + for (key, expr) in args { evaled_args.insert( String::from(key as &str), - self.evaluate_expression(scope_idx, expr)?, + self.evaluate_expression(scope_idx, expr, &step.locator)?, ); } @@ -228,7 +229,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + return Err(InterpreterError::TypeError { context: "Call".to_string(), conflict: format!("no entrypoint for {name:?}."), - location: "".to_string(), + location: step.locator.clone(), }); }; @@ -246,7 +247,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Err(InterpreterError::TypeError { context: format!("Required slot '{name}' not filled"), conflict: format!("slot was left empty."), - location: "".to_string(), + location: step.locator.clone(), }) } else { Ok(()) @@ -266,113 +267,114 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + &self, scope_idx: usize, expr: &'a Expression, + loc: &Locator, ) -> Result> { match expr { Expression::Add { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint + rint)), (lother, rother) => Err(InterpreterError::TypeError { context: "Add".to_string(), conflict: format!("can't add {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::Sub { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint - rint)), (lother, rother) => Err(InterpreterError::TypeError { context: "Sub".to_string(), conflict: format!("can't sub {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::Mul { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint * rint)), (lother, rother) => Err(InterpreterError::TypeError { context: "Mul".to_string(), conflict: format!("can't mul {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::Div { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Int(lint), Value::Int(rint)) => Ok(Value::Int(lint / rint)), (lother, rother) => Err(InterpreterError::TypeError { context: "Div".to_string(), conflict: format!("can't div {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::Negate { sub } => { - let sval = self.evaluate_expression(scope_idx, &sub)?; + let sval = self.evaluate_expression(scope_idx, &sub, loc)?; match sval { Value::Int(sint) => Ok(Value::Int(-sint)), sother => Err(InterpreterError::TypeError { context: "Negate".to_string(), conflict: format!("can't negate {sother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::BAnd { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool && rbool)), (lother, rother) => Err(InterpreterError::TypeError { context: "BAnd".to_string(), conflict: format!("can't and together {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::BOr { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool || rbool)), (lother, rother) => Err(InterpreterError::TypeError { context: "BOr".to_string(), conflict: format!("can't or together {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::BNot { sub } => { - let sval = self.evaluate_expression(scope_idx, &sub)?; + let sval = self.evaluate_expression(scope_idx, &sub, loc)?; match sval { Value::Bool(sbool) => Ok(Value::Bool(!sbool)), sother => Err(InterpreterError::TypeError { context: "BNot".to_string(), conflict: format!("can't invert {sother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::Equals { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::Bool(lbool), Value::Bool(rbool)) => Ok(Value::Bool(lbool == rbool)), @@ -380,13 +382,13 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + (lother, rother) => Err(InterpreterError::TypeError { context: "Equals".to_string(), conflict: format!("can't test {lother:?} and {rother:?} for equality!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::ListAdd { left, right } => { - let lval = self.evaluate_expression(scope_idx, &left)?; - let rval = self.evaluate_expression(scope_idx, &right)?; + let lval = self.evaluate_expression(scope_idx, &left, loc)?; + let rval = self.evaluate_expression(scope_idx, &right, loc)?; match (lval, rval) { (Value::List(mut llist), Value::List(rlist)) => { @@ -396,14 +398,14 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + (lother, rother) => Err(InterpreterError::TypeError { context: "ListAdd".to_string(), conflict: format!("can't list-add {lother:?} and {rother:?}!"), - location: "".to_string(), + location: loc.clone(), }), } } Expression::List { elements } => { let mut result = Vec::with_capacity(elements.len()); for expr in elements { - result.push(self.evaluate_expression(scope_idx, expr)?); + result.push(self.evaluate_expression(scope_idx, expr, loc)?); } Ok(Value::List(result)) } @@ -411,12 +413,12 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Expression::StringExpr(sexpr) => { let mut output = String::new(); for piece in &sexpr.pieces { - self.evaluate_string_piece(scope_idx, piece, &mut output)?; + self.evaluate_string_piece(scope_idx, piece, &mut output, loc)?; } Ok(Value::Str(Arc::new(output))) } Expression::FieldLookup { obj, ident, loc } => { - let obj_val = self.evaluate_expression(scope_idx, obj)?; + let obj_val = self.evaluate_expression(scope_idx, obj, loc)?; match obj_val { Value::Reflective(reflective) => match reflective.reflect_ref() { ReflectRef::Struct(ss) => { @@ -426,7 +428,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Err(InterpreterError::TypeError { context: format!("Field Lookup for '{ident}'"), conflict: format!("{reflective:?} is a reflective struct that does not have that field!"), - location: "".to_string(), + location: loc.clone(), }) } } @@ -439,14 +441,14 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Err(InterpreterError::TypeError { context: format!("Field Lookup for '{ident}'"), conflict: format!("{reflective:?} is a reflective tuple struct that does not have that field!"), - location: "".to_string(), + location: loc.clone(), }) } } else { Err(InterpreterError::TypeError { context: format!("Field Lookup for '{ident}'"), conflict: format!("{reflective:?} is a reflective tuple struct that does not have names for field!"), - location: "".to_string(), + location: loc.clone(), }) } } @@ -455,13 +457,13 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + conflict: format!( "{reflective:?} is of a reflective type that has no fields!" ), - location: "".to_string(), + location: loc.clone(), }), }, other => Err(InterpreterError::TypeError { context: format!("Field Lookup for '{ident}'"), conflict: format!("{other:?} is of a type that has no fields!"), - location: "".to_string(), + location: loc.clone(), }), } } @@ -479,7 +481,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + conflict: format!( "No local by that name; the only locals are: {locals_list}." ), - location: "".to_string(), + location: loc.clone(), }) } } @@ -494,6 +496,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + &self, val: Value, output: &mut String, + loc: &Locator, ) -> Result<(), InterpreterError> { match val { Value::Str(s) => { @@ -516,7 +519,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + conflict: format!( "Don't know how to write {reflective:?} as a sensible string output." ), - location: "".to_string(), + location: loc.clone(), }) } Value::List(_) => { @@ -526,7 +529,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + conflict: format!( "Don't know how to write List[...] as a sensible string output." ), - location: "".to_string(), + location: loc.clone(), }) } } @@ -537,6 +540,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + scope_idx: usize, piece: &StringPiece, output: &mut String, + loc: &Locator, ) -> Result<(), InterpreterError> { match piece { StringPiece::Literal(lit) => { @@ -544,8 +548,8 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + Ok(()) } StringPiece::Interpolation(expr) => { - let val = self.evaluate_expression(scope_idx, expr)?; - self.write_out_value_as_display_string(val, output)?; + let val = self.evaluate_expression(scope_idx, expr, loc)?; + self.write_out_value_as_display_string(val, output, loc)?; Ok(()) } StringPiece::Localise { @@ -555,7 +559,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + let mut parameters_evaluated = BTreeMap::new(); for (key, expr) in parameters { parameters_evaluated - .insert(key as &str, self.evaluate_expression(scope_idx, expr)?); + .insert(key as &str, self.evaluate_expression(scope_idx, expr, loc)?); } let localised_text = self @@ -564,7 +568,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + .map_err(|underlying| InterpreterError::Localisation { underlying, trans_key: (trans_key as &str).to_owned(), - location: "".to_string(), + location: loc.clone(), })?; output.push_str(&localised_text); @@ -581,7 +585,7 @@ impl<'a, L: Sync + Send, O: OutputSystem + Send, LS: LocalisationSystem + Sync + return Err(InterpreterError::TypeError { context: format!("No entrypoint called {:?}", self.entrypoint), conflict: "".to_string(), - location: "".to_string(), + location: Locator::empty(), }); }; self.run_steps(0, main).await?; diff --git a/hornbeam_interpreter/src/interface.rs b/hornbeam_interpreter/src/interface.rs index 5b084ca..40e44f3 100644 --- a/hornbeam_interpreter/src/interface.rs +++ b/hornbeam_interpreter/src/interface.rs @@ -37,10 +37,10 @@ pub trait OutputSystem { pub use crate::engine::Value; use crate::InterpreterError; -pub struct LoadedTemplates { +pub struct LoadedTemplates { // todo might be tempted to use e.g. ouroboros here, to keep the file source adjacent? // or do we just staticify? - template_functions: BTreeMap>>>, + template_functions: BTreeMap>>, localisation: Arc, } @@ -56,7 +56,7 @@ impl Params { } } -impl<'a, LS> LoadedTemplates<(), LS> { +impl<'a, LS> LoadedTemplates { pub fn new(localisation_system: LS) -> Self { LoadedTemplates { template_functions: Default::default(), @@ -103,7 +103,7 @@ impl<'a, LS> LoadedTemplates<(), LS> { template_name: &str, fragment_name: Option<&str>, params: Params, - ) -> PreparedTemplate<'a, (), LS> { + ) -> PreparedTemplate<'a, LS> { PreparedTemplate { all_instructions: Arc::new(self.template_functions.clone()), entrypoint: if let Some(frag) = fragment_name { @@ -120,14 +120,14 @@ impl<'a, LS> LoadedTemplates<(), LS> { } } -pub struct PreparedTemplate<'a, L, LS> { - pub(crate) all_instructions: Arc>>>>, +pub struct PreparedTemplate<'a, LS> { + pub(crate) all_instructions: Arc>>>, pub(crate) entrypoint: String, - pub(crate) scope: Scope<'a, L>, + pub(crate) scope: Scope<'a>, pub localisation: Arc, } -impl<'a, L: Sync + Send, LS: LocalisationSystem + Sync + Send> PreparedTemplate<'a, L, LS> { +impl<'a, LS: LocalisationSystem + Sync + Send> PreparedTemplate<'a, LS> { pub async fn render_to_output( self, output: O, diff --git a/hornbeam_interpreter/src/lib.rs b/hornbeam_interpreter/src/lib.rs index 5aef200..74f8443 100644 --- a/hornbeam_interpreter/src/lib.rs +++ b/hornbeam_interpreter/src/lib.rs @@ -5,7 +5,7 @@ pub(crate) mod interface; pub mod localisation; -use hornbeam_grammar::ParseError; +use hornbeam_grammar::{Locator, ParseError}; use hornbeam_ir::AstToIrError; use thiserror::Error; @@ -15,13 +15,13 @@ pub enum InterpreterError { TypeError { context: String, conflict: String, - location: String, + location: Locator, }, #[error("localisation lookup of {trans_key:?} at {location} failed: {underlying:?}")] Localisation { underlying: LE, trans_key: String, - location: String, + location: Locator, }, #[error("failed to write to output: {underlying:?}")] OutputError { underlying: OE }, diff --git a/hornbeam_ir/src/ast_to_ir.rs b/hornbeam_ir/src/ast_to_ir.rs index 86ab2a7..5b4913a 100644 --- a/hornbeam_ir/src/ast_to_ir.rs +++ b/hornbeam_ir/src/ast_to_ir.rs @@ -1,6 +1,6 @@ use crate::ir::{Step, StepDef}; use hornbeam_grammar::ast::{Block, Expression, StringExpr, StringPiece, Template}; -use hornbeam_grammar::intern; +use hornbeam_grammar::{intern, Locator}; use std::borrow::Cow; use std::collections::btree_map::Entry; use std::collections::BTreeMap; @@ -115,7 +115,7 @@ fn pull_out_entrypoints_from_block<'a>( /// Step 2. Compile the AST to IR steps. pub(crate) fn compile_functions<'a>( functions: &BTreeMap>, -) -> Result>>, AstToIrError> { +) -> Result>, AstToIrError> { let mut result = BTreeMap::new(); for (func_name, func_blocks) in functions { let mut steps = Vec::new(); @@ -129,7 +129,7 @@ pub(crate) fn compile_functions<'a>( fn compile_ast_block_to_steps<'a>( block: &Block, - instructions: &mut Vec>, + instructions: &mut Vec, ) -> Result<(), AstToIrError> { match block { Block::HtmlElement(he) => { @@ -147,7 +147,7 @@ fn compile_ast_block_to_steps<'a>( escape: false, text: intern(text), }, - locator: (), + locator: he.loc.clone(), }); // Write attributes @@ -157,7 +157,7 @@ fn compile_ast_block_to_steps<'a>( escape: false, text: intern(format!(" {attr_name}=\"")), }, - locator: (), + locator: he.loc.clone(), }); instructions.push(Step { @@ -165,7 +165,7 @@ fn compile_ast_block_to_steps<'a>( escape: true, expr: attr_expr.clone(), }, - locator: (), + locator: he.loc.clone(), }); instructions.push(Step { @@ -173,7 +173,7 @@ fn compile_ast_block_to_steps<'a>( escape: false, text: intern("\""), }, - locator: (), + locator: he.loc.clone(), }); } @@ -183,7 +183,7 @@ fn compile_ast_block_to_steps<'a>( escape: false, text: intern(">"), }, - locator: (), + locator: he.loc.clone(), }); for child in &he.children { @@ -195,7 +195,7 @@ fn compile_ast_block_to_steps<'a>( escape: false, text: intern(format!("", he.name)), }, - locator: (), + locator: he.loc.clone(), }); } Block::ComponentElement(ce) => { @@ -214,7 +214,7 @@ fn compile_ast_block_to_steps<'a>( args: ce.attributes.clone(), slots: all_slots_steps, }, - locator: (), + locator: ce.loc.clone(), }) } Block::IfBlock(ifb) => { @@ -235,7 +235,7 @@ fn compile_ast_block_to_steps<'a>( true_steps: true_instrs, false_steps: false_instrs, }, - locator: (), + locator: ifb.loc.clone(), }); } Block::Text(text) => { @@ -247,7 +247,7 @@ fn compile_ast_block_to_steps<'a>( text: lit.clone(), escape: true, }, - locator: (), + locator: Locator::empty(), }); } StringPiece::Interpolation(expr) => { @@ -256,7 +256,7 @@ fn compile_ast_block_to_steps<'a>( expr: expr.clone(), escape: true, }, - locator: (), + locator: Locator::empty(), }); } piece @ StringPiece::Localise { .. } => { @@ -267,7 +267,7 @@ fn compile_ast_block_to_steps<'a>( }), escape: true, }, - locator: (), + locator: Locator::empty(), }); } } @@ -279,7 +279,7 @@ fn compile_ast_block_to_steps<'a>( name: slot.name.clone(), optional: slot.optional, }, - locator: (), + locator: slot.loc.clone(), }); } Block::DefineFragment(frag) => { @@ -290,7 +290,7 @@ fn compile_ast_block_to_steps<'a>( args: Default::default(), slots: BTreeMap::new(), }, - locator: (), + locator: frag.loc.clone(), }); } } diff --git a/hornbeam_ir/src/ir.rs b/hornbeam_ir/src/ir.rs index 8aa144a..dd9049f 100644 --- a/hornbeam_ir/src/ir.rs +++ b/hornbeam_ir/src/ir.rs @@ -1,24 +1,24 @@ pub use hornbeam_grammar::ast::{Expression, StringPiece}; -use hornbeam_grammar::IStr; +use hornbeam_grammar::{IStr, Locator}; use serde::Serialize; use std::collections::BTreeMap; #[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct Function { +pub struct Function { pub name: IStr, - pub locator: L, - pub steps: Vec>, + pub locator: Option, + pub steps: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub struct Step { +pub struct Step { #[serde(flatten)] - pub def: StepDef, - pub locator: L, + pub def: StepDef, + pub locator: Locator, } #[derive(Clone, Debug, Eq, PartialEq, Serialize)] -pub enum StepDef { +pub enum StepDef { WriteLiteral { escape: bool, text: IStr, @@ -29,20 +29,20 @@ pub enum StepDef { }, If { condition: Expression, - true_steps: Vec>, - false_steps: Vec>, + true_steps: Vec, + false_steps: Vec, }, For { iterable: Expression, // TODO! binding: IStr, - body_steps: Vec>, - empty_steps: Vec>, + body_steps: Vec, + empty_steps: Vec, }, Call { name: IStr, args: BTreeMap, - slots: BTreeMap>>, + slots: BTreeMap>, }, CallSlotWithParentScope { name: IStr, diff --git a/hornbeam_ir/src/lib.rs b/hornbeam_ir/src/lib.rs index 7f28e52..92cf1be 100644 --- a/hornbeam_ir/src/lib.rs +++ b/hornbeam_ir/src/lib.rs @@ -22,7 +22,7 @@ pub use ast_to_ir::AstToIrError; pub fn ast_to_optimised_ir( template_name: &str, template: Template, -) -> Result>>, AstToIrError> { +) -> Result>, AstToIrError> { let entrypoints = pull_out_entrypoints(template, template_name)?; let mut compiled_funcs = ast_to_ir::compile_functions(&entrypoints)?; for steps in compiled_funcs.values_mut() { diff --git a/hornbeam_ir/src/peephole.rs b/hornbeam_ir/src/peephole.rs index db58392..79ed416 100644 --- a/hornbeam_ir/src/peephole.rs +++ b/hornbeam_ir/src/peephole.rs @@ -48,7 +48,7 @@ fn peephole_opt]) -> ()>( *steps = result; } -fn apply_peephole_pass<'a, L, F: Fn(&mut Vec>)>(steps: &mut Vec>, pass: &F) { +fn apply_peephole_pass)>(steps: &mut Vec, pass: &F) { pass(steps); for step in steps { match &mut step.def { @@ -83,7 +83,7 @@ fn apply_peephole_pass<'a, L, F: Fn(&mut Vec>)>(steps: &mut Vec> //// Peephole Passes /// Given a WriteEval step that just writes literals, convert it to a WriteLiteral step. -fn pass_write_eval_literal_to_write_literal(steps: &mut Vec>) { +fn pass_write_eval_literal_to_write_literal(steps: &mut Vec) { 'next_step: for step in steps { if let StepDef::WriteEval { escape, @@ -110,7 +110,7 @@ fn pass_write_eval_literal_to_write_literal(steps: &mut Vec>) { } /// Given a WriteLiteral step that escapes the HTML as it is written, precompute the escaped HTML. -fn pass_write_literal_preescape(steps: &mut Vec>) { +fn pass_write_literal_preescape(steps: &mut Vec) { for step in steps { if let StepDef::WriteLiteral { escape, text } = &mut step.def { if *escape { @@ -125,7 +125,7 @@ fn pass_write_literal_preescape(steps: &mut Vec>) { } /// Given two adjacent WriteLiteral steps, combine them together. -fn pass_combine_write_literals(steps: &mut Vec>) { +fn pass_combine_write_literals(steps: &mut Vec) { peephole_opt(steps, 2, |steps| { let (l, r) = steps.split_at_mut(1); let left = l[0].as_mut().unwrap(); @@ -153,7 +153,7 @@ fn pass_combine_write_literals(steps: &mut Vec>) { //// Apply all passes in the preferred order -pub fn apply_all_peephole_passes(steps: &mut Vec>) { +pub fn apply_all_peephole_passes(steps: &mut Vec) { apply_peephole_pass(steps, &pass_write_eval_literal_to_write_literal); apply_peephole_pass(steps, &pass_write_literal_preescape); apply_peephole_pass(steps, &pass_combine_write_literals); @@ -167,7 +167,7 @@ mod tests { use insta::assert_yaml_snapshot; use std::collections::BTreeMap; - fn parse_ir_and_peephole(text: &str) -> BTreeMap>> { + fn parse_ir_and_peephole(text: &str) -> BTreeMap> { let template = parse_template(text, "inp").unwrap(); let entrypoints = pull_out_entrypoints(template, "TemplateName").unwrap(); let mut compiled = compile_functions(&entrypoints).unwrap(); diff --git a/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile.snap b/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile.snap index 392fbbb..358638d 100644 --- a/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile.snap +++ b/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile.snap @@ -6,67 +6,112 @@ TemplateName: - WriteLiteral: escape: false text: "" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 - Call: name: TemplateName__Frag1 args: {} slots: {} - locator: ~ + locator: + filename: inp + line: 3 + column: 5 - Call: name: TemplateName__Footer args: {} slots: {} - locator: ~ + locator: + filename: inp + line: 9 + column: 5 - WriteLiteral: escape: false text: "
" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 TemplateName__Footer: - WriteLiteral: escape: true text: Or even adjacent ones - locator: ~ + locator: + filename: "" + line: 0 + column: 0 TemplateName__Frag1: - WriteLiteral: escape: false text: "" - locator: ~ + locator: + filename: inp + line: 4 + column: 9 - WriteLiteral: escape: true text: This is a fragment!! - locator: ~ + locator: + filename: "" + line: 0 + column: 0 - WriteLiteral: escape: false text: "" - locator: ~ + locator: + filename: inp + line: 4 + column: 9 - Call: name: TemplateName__Frag2 args: {} slots: {} - locator: ~ + locator: + filename: inp + line: 6 + column: 9 TemplateName__Frag2: - WriteLiteral: escape: false text: "" - locator: ~ + locator: + filename: inp + line: 7 + column: 13 - WriteLiteral: escape: true text: "There's no problem having nested fragments!" - locator: ~ + locator: + filename: "" + line: 0 + column: 0 - WriteLiteral: escape: false text: "
" - locator: ~ + locator: + filename: inp + line: 7 + column: 13 diff --git a/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile_attributes.snap b/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile_attributes.snap index d533e54..500d76c 100644 --- a/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile_attributes.snap +++ b/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__compile_attributes.snap @@ -6,58 +6,98 @@ TemplateName: - WriteLiteral: escape: false text: "" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 - WriteLiteral: escape: true text: This is a div with a few extras - locator: ~ + locator: + filename: "" + line: 0 + column: 0 - Call: name: OtherComponent args: @@ -71,11 +111,21 @@ TemplateName: param3: Variable: name: three + loc: + filename: inp + line: 4 + column: 52 slots: main: [] - locator: ~ + locator: + filename: inp + line: 4 + column: 5 - WriteLiteral: escape: false text: "" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 diff --git a/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__pull_out_entrypoints.snap b/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__pull_out_entrypoints.snap index 7ea0e41..12dc799 100644 --- a/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__pull_out_entrypoints.snap +++ b/hornbeam_ir/src/snapshots/hornbeam_ir__ast_to_ir__tests__pull_out_entrypoints.snap @@ -9,12 +9,24 @@ TemplateName: - DefineFragment: name: TemplateName__Frag1 blocks: [] + loc: + filename: inp + line: 3 + column: 5 - DefineFragment: name: TemplateName__Footer blocks: [] + loc: + filename: inp + line: 9 + column: 5 classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 2 + column: 1 TemplateName__Footer: - Text: pieces: @@ -29,9 +41,17 @@ TemplateName__Frag1: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 4 + column: 9 - DefineFragment: name: TemplateName__Frag2 blocks: [] + loc: + filename: inp + line: 6 + column: 9 TemplateName__Frag2: - HtmlElement: name: div @@ -42,4 +62,8 @@ TemplateName__Frag2: classes: [] dom_id: ~ attributes: {} + loc: + filename: inp + line: 7 + column: 13 diff --git a/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__adjacent_text.snap b/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__adjacent_text.snap index d45cd67..d7618ab 100644 --- a/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__adjacent_text.snap +++ b/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__adjacent_text.snap @@ -6,39 +6,63 @@ TemplateName: - WriteLiteral: escape: false text: "
" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 - Call: name: TemplateName__Frag1 args: {} slots: {} - locator: ~ + locator: + filename: inp + line: 3 + column: 5 - Call: name: TemplateName__Footer args: {} slots: {} - locator: ~ + locator: + filename: inp + line: 9 + column: 5 - WriteLiteral: escape: false text: "
" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 TemplateName__Footer: - WriteLiteral: escape: false text: Or even adjacent ones - locator: ~ + locator: + filename: "" + line: 0 + column: 0 TemplateName__Frag1: - WriteLiteral: escape: false text: "This is a fragment!!" - locator: ~ + locator: + filename: inp + line: 4 + column: 9 - Call: name: TemplateName__Frag2 args: {} slots: {} - locator: ~ + locator: + filename: inp + line: 6 + column: 9 TemplateName__Frag2: - WriteLiteral: escape: false text: "
There's no problem having <<nested>> fragments!
" - locator: ~ + locator: + filename: inp + line: 7 + column: 13 diff --git a/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__with_attributes.snap b/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__with_attributes.snap index ca1f0b3..32acdef 100644 --- a/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__with_attributes.snap +++ b/hornbeam_ir/src/snapshots/hornbeam_ir__peephole__tests__with_attributes.snap @@ -6,27 +6,46 @@ TemplateName: - WriteLiteral: escape: false text: "
This is a div with a few extras" - locator: ~ + locator: + filename: inp + line: 2 + column: 1 - Call: name: OtherComponent args: @@ -40,11 +59,21 @@ TemplateName: param3: Variable: name: three + loc: + filename: inp + line: 4 + column: 52 slots: main: [] - locator: ~ + locator: + filename: inp + line: 4 + column: 5 - WriteLiteral: escape: false text: "
" - locator: ~ + locator: + filename: inp + line: 2 + column: 1