Skip to content

Commit 70fa4f7

Browse files
committed
locals are now scoped to their parent element
1 parent 754f56c commit 70fa4f7

5 files changed

Lines changed: 89 additions & 20 deletions

File tree

anathema-templates/src/error/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub enum ErrorKind {
5454
EmptyBody,
5555
InvalidStatement(String),
5656
Io(std::io::Error),
57-
GlobalAlreadyAssigned(String)
57+
GlobalAlreadyAssigned(String),
5858
}
5959

6060
impl ErrorKind {

anathema-templates/src/lexer.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ mod test {
332332
| crate::error::ErrorKind::MissingComponent(_)
333333
| crate::error::ErrorKind::EmptyTemplate
334334
| crate::error::ErrorKind::EmptyBody
335+
| crate::error::ErrorKind::GlobalAlreadyAssigned(_)
335336
| crate::error::ErrorKind::Io(_) => panic!("invalid error"),
336337
}
337338
}

anathema-templates/src/statements/eval.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ impl Scope {
6464
true => match ctx.variables.define_global(binding, value) {
6565
Ok(()) => (),
6666
Err(kind) => return Err(kind.to_error(ctx.template.path())),
67-
}
67+
},
6868
}
6969
}
7070
Statement::ComponentSlot(slot_id) => {
@@ -92,7 +92,10 @@ impl Scope {
9292
let ident = ctx.strings.get_unchecked(ident);
9393
let attributes = self.eval_attributes(ctx)?;
9494
let value = self.statements.take_value().and_then(|v| const_eval(v, ctx));
95+
96+
ctx.variables.push();
9597
let children = self.consume_scope(ctx)?;
98+
ctx.variables.pop();
9699

97100
let node = Blueprint::Single(Single {
98101
ident,
@@ -202,8 +205,6 @@ impl Scope {
202205
}
203206

204207
fn eval_component(&mut self, component_id: ComponentBlueprintId, ctx: &mut Context<'_>) -> Result<Blueprint> {
205-
ctx.variables.push();
206-
207208
let parent = ctx.component_parent();
208209

209210
// Associated functions
@@ -212,6 +213,9 @@ impl Scope {
212213
// Attributes
213214
let attributes = self.eval_attributes(ctx)?;
214215

216+
// Scope variables to the component
217+
ctx.variables.push_scope_boundary();
218+
215219
// Slots
216220
let mut slots = SmallMap::empty();
217221
let mut scope = self.statements.take_scope();
@@ -246,7 +250,7 @@ impl Scope {
246250
parent,
247251
};
248252

249-
ctx.variables.pop();
253+
ctx.variables.pop_scope_boundary();
250254
Ok(Blueprint::Component(component))
251255
}
252256
}

anathema-templates/src/variables.rs

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
use std::collections::HashMap;
2+
use std::sync::OnceLock;
23

34
use anathema_store::slab::{Slab, SlabIndex};
45

5-
use crate::{error::ErrorKind, expressions::Expression};
6+
use crate::error::ErrorKind;
7+
use crate::expressions::Expression;
68

79
#[derive(Debug, Default, Clone)]
810
pub(crate) struct Globals(HashMap<String, Expression>);
@@ -70,6 +72,11 @@ impl Variable {
7072
pub struct ScopeId(Box<[u16]>);
7173

7274
impl ScopeId {
75+
fn root() -> &'static Self {
76+
static ROOT: OnceLock<ScopeId> = OnceLock::new();
77+
ROOT.get_or_init(|| ScopeId(Box::new([])))
78+
}
79+
7380
// Create the next child id.
7481
fn next(&self, index: u16) -> Self {
7582
let mut scope_id = Vec::with_capacity(self.0.len() + 1);
@@ -102,6 +109,16 @@ impl ScopeId {
102109
fn as_slice(&self) -> &[u16] {
103110
&self.0
104111
}
112+
113+
fn contains(&self, other: impl AsRef<[u16]>) -> Option<&ScopeId> {
114+
let other = other.as_ref();
115+
let len = self.0.len();
116+
117+
match other.len() >= len {
118+
true => (*self.0 == other[..len]).then_some(self),
119+
false => None,
120+
}
121+
}
105122
}
106123

107124
impl AsRef<[u16]> for ScopeId {
@@ -186,13 +203,14 @@ impl Declarations {
186203
}
187204

188205
// Get the scope id that is closest to the argument
189-
fn get(&self, ident: &str, scope_id: impl AsRef<[u16]>) -> Option<VarId> {
206+
fn get(&self, ident: &str, scope_id: impl AsRef<[u16]>, boundary: &ScopeId) -> Option<VarId> {
190207
self.0
191208
.get(ident)?
192209
.iter()
193210
.rev()
194-
.filter(|(scope, _)| scope.as_ref() == scope_id.as_ref())
195-
.map(|(_, var)| *var)
211+
// here we need to look up closest scope that is still within the last boundary
212+
.filter(|(scope, _)| boundary.contains(scope).is_some())
213+
.filter_map(|(scope, var)| scope.contains(&scope_id).map(|_| *var))
196214
.next()
197215
}
198216
}
@@ -204,6 +222,7 @@ pub struct Variables {
204222
globals: Globals,
205223
root: RootScope,
206224
current: ScopeId,
225+
boundary: Vec<ScopeId>,
207226
store: Slab<VarId, Variable>,
208227
declarations: Declarations,
209228
}
@@ -214,6 +233,7 @@ impl Default for Variables {
214233
Self {
215234
globals: Globals::empty(),
216235
current: root.0.id.clone(),
236+
boundary: vec![],
217237
root,
218238
store: Slab::empty(),
219239
declarations: Declarations::new(),
@@ -264,7 +284,21 @@ impl Variables {
264284

265285
/// Fetch a value starting from the current path.
266286
pub fn fetch(&self, ident: &str) -> Option<VarId> {
267-
self.declarations.get(ident, &self.current)
287+
self.declarations.get(ident, &self.current, self.boundary())
288+
}
289+
290+
/// Create a new scope and set that scope as a boundary.
291+
/// This prevents inner components from accessing values
292+
/// declared outside of the component.
293+
pub(crate) fn push_scope_boundary(&mut self) {
294+
self.push();
295+
self.boundary.push(self.current.clone());
296+
}
297+
298+
/// Pop the scope boundary.
299+
pub(crate) fn pop_scope_boundary(&mut self) {
300+
self.pop();
301+
self.boundary.pop();
268302
}
269303

270304
/// Create a new child and set the new childs id as the `current` id.
@@ -291,13 +325,17 @@ impl Variables {
291325
// Fetch and load a value from its ident
292326
#[cfg(test)]
293327
fn fetch_load(&self, ident: &str) -> Option<&Expression> {
294-
let id = self.declarations.get(ident, &self.current)?;
328+
let id = self.declarations.get(ident, &self.current, self.boundary())?;
295329
self.load(id)
296330
}
297331

298332
pub fn global_lookup(&self, ident: &str) -> Option<&Expression> {
299333
self.globals.get(ident)
300334
}
335+
336+
fn boundary(&self) -> &ScopeId {
337+
self.boundary.last().unwrap_or(ScopeId::root())
338+
}
301339
}
302340

303341
impl From<Variables> for HashMap<String, Variable> {
@@ -319,6 +357,7 @@ impl From<Variables> for HashMap<String, Variable> {
319357
#[cfg(test)]
320358
mod test {
321359
use super::*;
360+
use crate::expressions::num;
322361

323362
impl From<usize> for VarId {
324363
fn from(value: usize) -> Self {
@@ -403,15 +442,15 @@ mod test {
403442
fn declaration_lookup() {
404443
let mut dec = Declarations::new();
405444
dec.add("var", [0, 0], 0);
406-
let root = dec.get("var", [0, 0]).unwrap();
445+
let root = dec.get("var", [0, 0], ScopeId::root()).unwrap();
407446
assert_eq!(root, 0.into());
408447
}
409448

410449
#[test]
411450
fn declaration_failed_lookup() {
412451
let mut dec = Declarations::new();
413452
dec.add("var", [0], 0);
414-
let root = dec.get("var", [1, 0]);
453+
let root = dec.get("var", [1, 0], ScopeId::root());
415454
assert!(root.is_none());
416455
}
417456

@@ -423,15 +462,40 @@ mod test {
423462
dec.add(ident, [0, 0], 1);
424463
dec.add(ident, [0, 0, 0], 2);
425464

426-
assert_eq!(dec.get(ident, [0]).unwrap().0, 0);
427-
assert_eq!(dec.get(ident, [0, 0]).unwrap().0, 1);
428-
assert!(dec.get(ident, [0, 0, 0, 1, 1]).is_none());
465+
assert_eq!(dec.get(ident, [0], ScopeId::root()).unwrap().0, 0);
466+
assert_eq!(dec.get(ident, [0, 0], ScopeId::root()).unwrap().0, 1);
467+
assert_eq!(dec.get(ident, [0, 0, 0, 1, 1], ScopeId::root()).unwrap().0, 2);
429468
}
430469

431470
#[test]
432471
fn unreachable_declaration() {
433472
let mut dec = Declarations::new();
434473
dec.add("var", [0, 1], 0);
435-
assert!(dec.get("var", [0, 0, 1]).is_none());
474+
assert!(dec.get("var", [0, 0, 1], ScopeId::root()).is_none());
475+
}
476+
477+
#[test]
478+
fn get_inside_boundary() {
479+
let mut vars = Variables::new();
480+
481+
// Define a varialbe in the root scope
482+
_ = vars.define_local("var", 1);
483+
484+
// Create a new unique scope and boundary.
485+
// * `var` should be inaccessible from within the new scope boundary
486+
// * `outer_var` should be inaccessible to the root scope
487+
vars.push_scope_boundary();
488+
assert!(vars.fetch("var").is_none());
489+
_ = vars.define_local("var", 2);
490+
_ = vars.define_local("other_var", 3);
491+
assert_eq!(vars.fetch_load("var").unwrap(), &*num(2));
492+
vars.push();
493+
assert_eq!(vars.fetch_load("other_var").unwrap(), &*num(3));
494+
vars.pop();
495+
496+
// Return to root scope
497+
vars.pop_scope_boundary();
498+
assert_eq!(vars.fetch_load("var").unwrap(), &*num(1));
499+
assert!(vars.fetch("other_var").is_none());
436500
}
437501
}

anathema-value-resolver/src/value.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ pub(crate) mod test {
573573

574574
let mut states = States::new();
575575
let mut globals = Variables::new();
576-
globals.define_global("index", 0);
576+
globals.define_global("index", 0).unwrap();
577577

578578
setup(&mut states, globals, |test| {
579579
let value = test.eval(&expr);
@@ -881,7 +881,7 @@ pub(crate) mod test {
881881
// state[empty|full]
882882
let mut states = States::new();
883883
let mut globals = Variables::new();
884-
globals.define_global("full", "string");
884+
globals.define_global("full", "string").unwrap();
885885
setup(&mut states, globals, |test| {
886886
let expr = index(ident("state"), either(ident("empty"), ident("full")));
887887
test.with_state(|state| state.string.set("a string"));
@@ -934,7 +934,7 @@ pub(crate) mod test {
934934
fn test_either() {
935935
let mut states = States::new();
936936
let mut globals = Variables::new();
937-
globals.define_global("missing", 111);
937+
globals.define_global("missing", 111).unwrap();
938938
setup(&mut states, globals, |test| {
939939
let expr = either(ident("missings"), num(2));
940940
let value = test.eval(&expr);

0 commit comments

Comments
 (0)