Skip to content

Commit a208447

Browse files
authored
Merge pull request #129 from togglebyte/feature/variable-scoping
Feature/variable scoping
2 parents 396799a + 70fa4f7 commit a208447

6 files changed

Lines changed: 105 additions & 20 deletions

File tree

CHANGES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* BUGFIX: ctrl+c works with the error display
77
* Trying to use a component twice will now include the component name in the
88
error
9+
* Global definitions will raise an error if it's already assigned
910
* 0.2.7
1011
* BUGFIX: use correct truthiness check in control flow update
1112
* 0.2.6

anathema-templates/src/error/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ pub enum ErrorKind {
5454
EmptyBody,
5555
InvalidStatement(String),
5656
Io(std::io::Error),
57+
GlobalAlreadyAssigned(String),
5758
}
5859

5960
impl ErrorKind {
@@ -72,6 +73,7 @@ impl Display for ErrorKind {
7273
ErrorKind::EmptyBody => write!(f, "if or else node has no children"),
7374
ErrorKind::InvalidStatement(stmt) => write!(f, "invalid statement: {stmt}"),
7475
ErrorKind::Io(err) => write!(f, "{err}"),
76+
ErrorKind::GlobalAlreadyAssigned(name) => write!(f, "global value `{name}` already assigned"),
7577
}
7678
}
7779
}

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: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ impl Scope {
6161
}
6262
match is_global {
6363
false => _ = ctx.variables.define_local(binding, value),
64-
true => ctx.variables.define_global(binding, value),
64+
true => match ctx.variables.define_global(binding, value) {
65+
Ok(()) => (),
66+
Err(kind) => return Err(kind.to_error(ctx.template.path())),
67+
},
6568
}
6669
}
6770
Statement::ComponentSlot(slot_id) => {
@@ -89,7 +92,10 @@ impl Scope {
8992
let ident = ctx.strings.get_unchecked(ident);
9093
let attributes = self.eval_attributes(ctx)?;
9194
let value = self.statements.take_value().and_then(|v| const_eval(v, ctx));
95+
96+
ctx.variables.push();
9297
let children = self.consume_scope(ctx)?;
98+
ctx.variables.pop();
9399

94100
let node = Blueprint::Single(Single {
95101
ident,
@@ -199,8 +205,6 @@ impl Scope {
199205
}
200206

201207
fn eval_component(&mut self, component_id: ComponentBlueprintId, ctx: &mut Context<'_>) -> Result<Blueprint> {
202-
ctx.variables.push();
203-
204208
let parent = ctx.component_parent();
205209

206210
// Associated functions
@@ -209,6 +213,9 @@ impl Scope {
209213
// Attributes
210214
let attributes = self.eval_attributes(ctx)?;
211215

216+
// Scope variables to the component
217+
ctx.variables.push_scope_boundary();
218+
212219
// Slots
213220
let mut slots = SmallMap::empty();
214221
let mut scope = self.statements.take_scope();
@@ -243,7 +250,7 @@ impl Scope {
243250
parent,
244251
};
245252

246-
ctx.variables.pop();
253+
ctx.variables.pop_scope_boundary();
247254
Ok(Blueprint::Component(component))
248255
}
249256
}

anathema-templates/src/variables.rs

Lines changed: 87 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::collections::HashMap;
2+
use std::sync::OnceLock;
23

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

6+
use crate::error::ErrorKind;
57
use crate::expressions::Expression;
68

79
#[derive(Debug, Default, Clone)]
@@ -12,6 +14,10 @@ impl Globals {
1214
Self(HashMap::new())
1315
}
1416

17+
pub fn contains(&self, ident: &str) -> bool {
18+
self.0.contains_key(ident)
19+
}
20+
1521
pub fn get(&self, ident: &str) -> Option<&Expression> {
1622
self.0.get(ident)
1723
}
@@ -66,6 +72,11 @@ impl Variable {
6672
pub struct ScopeId(Box<[u16]>);
6773

6874
impl ScopeId {
75+
fn root() -> &'static Self {
76+
static ROOT: OnceLock<ScopeId> = OnceLock::new();
77+
ROOT.get_or_init(|| ScopeId(Box::new([])))
78+
}
79+
6980
// Create the next child id.
7081
fn next(&self, index: u16) -> Self {
7182
let mut scope_id = Vec::with_capacity(self.0.len() + 1);
@@ -98,6 +109,16 @@ impl ScopeId {
98109
fn as_slice(&self) -> &[u16] {
99110
&self.0
100111
}
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+
}
101122
}
102123

103124
impl AsRef<[u16]> for ScopeId {
@@ -182,13 +203,14 @@ impl Declarations {
182203
}
183204

184205
// Get the scope id that is closest to the argument
185-
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> {
186207
self.0
187208
.get(ident)?
188209
.iter()
189210
.rev()
190-
.filter(|(scope, _)| scope.as_ref() == scope_id.as_ref())
191-
.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))
192214
.next()
193215
}
194216
}
@@ -200,6 +222,7 @@ pub struct Variables {
200222
globals: Globals,
201223
root: RootScope,
202224
current: ScopeId,
225+
boundary: Vec<ScopeId>,
203226
store: Slab<VarId, Variable>,
204227
declarations: Declarations,
205228
}
@@ -210,6 +233,7 @@ impl Default for Variables {
210233
Self {
211234
globals: Globals::empty(),
212235
current: root.0.id.clone(),
236+
boundary: vec![],
213237
root,
214238
store: Slab::empty(),
215239
declarations: Declarations::new(),
@@ -232,9 +256,15 @@ impl Variables {
232256
var_id
233257
}
234258

235-
pub fn define_global(&mut self, ident: impl Into<String>, value: impl Into<Expression>) {
259+
pub fn define_global(&mut self, ident: impl Into<String>, value: impl Into<Expression>) -> Result<(), ErrorKind> {
260+
let ident = ident.into();
261+
if self.globals.contains(&ident) {
262+
return Err(ErrorKind::GlobalAlreadyAssigned(ident));
263+
}
264+
236265
let value = value.into();
237-
self.globals.set(ident.into(), value)
266+
self.globals.set(ident, value);
267+
Ok(())
238268
}
239269

240270
pub fn define_local(&mut self, ident: impl Into<String>, value: impl Into<Expression>) -> VarId {
@@ -254,7 +284,21 @@ impl Variables {
254284

255285
/// Fetch a value starting from the current path.
256286
pub fn fetch(&self, ident: &str) -> Option<VarId> {
257-
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();
258302
}
259303

260304
/// Create a new child and set the new childs id as the `current` id.
@@ -281,13 +325,17 @@ impl Variables {
281325
// Fetch and load a value from its ident
282326
#[cfg(test)]
283327
fn fetch_load(&self, ident: &str) -> Option<&Expression> {
284-
let id = self.declarations.get(ident, &self.current)?;
328+
let id = self.declarations.get(ident, &self.current, self.boundary())?;
285329
self.load(id)
286330
}
287331

288332
pub fn global_lookup(&self, ident: &str) -> Option<&Expression> {
289333
self.globals.get(ident)
290334
}
335+
336+
fn boundary(&self) -> &ScopeId {
337+
self.boundary.last().unwrap_or(ScopeId::root())
338+
}
291339
}
292340

293341
impl From<Variables> for HashMap<String, Variable> {
@@ -309,6 +357,7 @@ impl From<Variables> for HashMap<String, Variable> {
309357
#[cfg(test)]
310358
mod test {
311359
use super::*;
360+
use crate::expressions::num;
312361

313362
impl From<usize> for VarId {
314363
fn from(value: usize) -> Self {
@@ -393,15 +442,15 @@ mod test {
393442
fn declaration_lookup() {
394443
let mut dec = Declarations::new();
395444
dec.add("var", [0, 0], 0);
396-
let root = dec.get("var", [0, 0]).unwrap();
445+
let root = dec.get("var", [0, 0], ScopeId::root()).unwrap();
397446
assert_eq!(root, 0.into());
398447
}
399448

400449
#[test]
401450
fn declaration_failed_lookup() {
402451
let mut dec = Declarations::new();
403452
dec.add("var", [0], 0);
404-
let root = dec.get("var", [1, 0]);
453+
let root = dec.get("var", [1, 0], ScopeId::root());
405454
assert!(root.is_none());
406455
}
407456

@@ -413,15 +462,40 @@ mod test {
413462
dec.add(ident, [0, 0], 1);
414463
dec.add(ident, [0, 0, 0], 2);
415464

416-
assert_eq!(dec.get(ident, [0]).unwrap().0, 0);
417-
assert_eq!(dec.get(ident, [0, 0]).unwrap().0, 1);
418-
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);
419468
}
420469

421470
#[test]
422471
fn unreachable_declaration() {
423472
let mut dec = Declarations::new();
424473
dec.add("var", [0, 1], 0);
425-
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());
426500
}
427501
}

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)