diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b740dd..e47dc084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ Bug fixes * Compound assignments now work properly with indexers. +Script-breaking changes +----------------------- + +* _Strict Variables Mode_ no longer returns an error when an undeclared variable matches a constant in the provided external `Scope`. + Enhancements ------------ diff --git a/src/api/compile.rs b/src/api/compile.rs index 2259ec92..ab82f90a 100644 --- a/src/api/compile.rs +++ b/src/api/compile.rs @@ -225,13 +225,8 @@ impl Engine { scripts.as_ref(), self.token_mapper.as_ref().map(Box::as_ref), ); - let mut state = ParseState::new(self, tokenizer_control); - self.parse( - &mut stream.peekable(), - &mut state, - scope, - optimization_level, - ) + let mut state = ParseState::new(self, scope, tokenizer_control); + self.parse(&mut stream.peekable(), &mut state, optimization_level) } /// Compile a string containing an expression into an [`AST`], /// which can be used later for evaluation. @@ -300,12 +295,7 @@ impl Engine { self.lex_raw(&scripts, self.token_mapper.as_ref().map(Box::as_ref)); let mut peekable = stream.peekable(); - let mut state = ParseState::new(self, tokenizer_control); - self.parse_global_expr( - &mut peekable, - &mut state, - scope, - self.options.optimization_level, - ) + let mut state = ParseState::new(self, scope, tokenizer_control); + self.parse_global_expr(&mut peekable, &mut state, self.options.optimization_level) } } diff --git a/src/api/eval.rs b/src/api/eval.rs index 49e26243..094fd117 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -116,13 +116,12 @@ impl Engine { let scripts = [script]; let (stream, tokenizer_control) = self.lex_raw(&scripts, self.token_mapper.as_ref().map(Box::as_ref)); - let mut state = ParseState::new(self, tokenizer_control); + let mut state = ParseState::new(self, scope, tokenizer_control); // No need to optimize a lone expression let ast = self.parse_global_expr( &mut stream.peekable(), &mut state, - scope, #[cfg(not(feature = "no_optimize"))] OptimizationLevel::None, #[cfg(feature = "no_optimize")] diff --git a/src/api/json.rs b/src/api/json.rs index 4eba3eb6..b495c4a0 100644 --- a/src/api/json.rs +++ b/src/api/json.rs @@ -116,12 +116,12 @@ impl Engine { }, ); - let mut state = ParseState::new(self, tokenizer_control); + let scope = &Scope::new(); + let mut state = ParseState::new(self, scope, tokenizer_control); let ast = self.parse_global_expr( &mut stream.peekable(), &mut state, - &Scope::new(), #[cfg(not(feature = "no_optimize"))] OptimizationLevel::None, #[cfg(feature = "no_optimize")] diff --git a/src/api/run.rs b/src/api/run.rs index 77097d29..cbda58c7 100644 --- a/src/api/run.rs +++ b/src/api/run.rs @@ -24,12 +24,11 @@ impl Engine { let scripts = [script]; let (stream, tokenizer_control) = self.lex_raw(&scripts, self.token_mapper.as_ref().map(Box::as_ref)); - let mut state = ParseState::new(self, tokenizer_control); + let mut state = ParseState::new(self, scope, tokenizer_control); let ast = self.parse( &mut stream.peekable(), &mut state, - scope, self.options.optimization_level, )?; diff --git a/src/parser.rs b/src/parser.rs index a4b6fa60..e33ece2b 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -47,6 +47,8 @@ pub struct ParseState<'e> { pub tokenizer_control: TokenizerControl, /// Interned strings. interned_strings: StringsInterner, + /// External [scope][Scope] with constants. + pub scope: &'e Scope<'e>, /// Encapsulates a local stack with variable names to simulate an actual runtime scope. pub stack: Scope<'e>, /// Size of the local variables stack upon entry of the current block scope. @@ -72,7 +74,7 @@ impl<'e> ParseState<'e> { /// Create a new [`ParseState`]. #[inline(always)] #[must_use] - pub fn new(engine: &Engine, tokenizer_control: TokenizerControl) -> Self { + pub fn new(engine: &Engine, scope: &'e Scope, tokenizer_control: TokenizerControl) -> Self { Self { tokenizer_control, #[cfg(not(feature = "no_closure"))] @@ -80,6 +82,7 @@ impl<'e> ParseState<'e> { #[cfg(not(feature = "no_closure"))] allow_capture: true, interned_strings: StringsInterner::new(), + scope, stack: Scope::new(), block_stack_len: 0, #[cfg(not(feature = "no_module"))] @@ -1261,7 +1264,8 @@ impl Engine { // | ... #[cfg(not(feature = "no_function"))] Token::Pipe | Token::Or if settings.options.allow_anonymous_fn => { - let mut new_state = ParseState::new(self, state.tokenizer_control.clone()); + let mut new_state = + ParseState::new(self, state.scope, state.tokenizer_control.clone()); #[cfg(not(feature = "unchecked"))] { @@ -1297,6 +1301,10 @@ impl Engine { if settings.options.strict_var && !settings.is_closure_scope && index.is_none() + && !matches!( + state.scope.get_index(name), + Some((_, AccessMode::ReadOnly)) + ) { // If the parent scope is not inside another capturing closure // then we can conclude that the captured variable doesn't exist. @@ -1440,7 +1448,10 @@ impl Engine { _ => { let index = state.access_var(&s, settings.pos); - if settings.options.strict_var && index.is_none() { + if settings.options.strict_var + && index.is_none() + && !matches!(state.scope.get_index(&s), Some((_, AccessMode::ReadOnly))) + { return Err( PERR::VariableUndefined(s.to_string()).into_err(settings.pos) ); @@ -3061,7 +3072,8 @@ impl Engine { match input.next().expect(NEVER_ENDS) { (Token::Fn, pos) => { - let mut new_state = ParseState::new(self, state.tokenizer_control.clone()); + let mut new_state = + ParseState::new(self, state.scope, state.tokenizer_control.clone()); #[cfg(not(feature = "unchecked"))] { @@ -3533,7 +3545,6 @@ impl Engine { &self, input: &mut TokenStream, state: &mut ParseState, - _scope: &Scope, _optimization_level: OptimizationLevel, ) -> ParseResult { let mut functions = BTreeMap::new(); @@ -3575,7 +3586,7 @@ impl Engine { #[cfg(not(feature = "no_optimize"))] return Ok(crate::optimizer::optimize_into_ast( self, - _scope, + state.scope, statements, #[cfg(not(feature = "no_function"))] StaticVec::new_const(), @@ -3657,7 +3668,6 @@ impl Engine { &self, input: &mut TokenStream, state: &mut ParseState, - _scope: &Scope, _optimization_level: OptimizationLevel, ) -> ParseResult { let (statements, _lib) = self.parse_global_level(input, state)?; @@ -3665,7 +3675,7 @@ impl Engine { #[cfg(not(feature = "no_optimize"))] return Ok(crate::optimizer::optimize_into_ast( self, - _scope, + state.scope, statements, #[cfg(not(feature = "no_function"))] _lib, diff --git a/tests/options.rs b/tests/options.rs index 803a5802..f4154d72 100644 --- a/tests/options.rs +++ b/tests/options.rs @@ -56,6 +56,10 @@ fn test_options_allow() -> Result<(), Box> { fn test_options_strict_var() -> Result<(), Box> { let mut engine = Engine::new(); + let mut scope = Scope::new(); + scope.push_constant("x", 42 as INT); + scope.push_constant("y", 0 as INT); + engine.compile("let x = if y { z } else { w };")?; #[cfg(not(feature = "no_function"))] @@ -79,6 +83,11 @@ fn test_options_strict_var() -> Result<(), Box> { assert!(engine.compile("let x = if y { z } else { w };").is_err()); engine.compile("let y = 42; let x = y;")?; + assert_eq!( + engine.eval_with_scope::(&mut scope, "{ let y = 42; x * y }")?, + 42 * 42 + ); + #[cfg(not(feature = "no_function"))] assert!(engine.compile("fn foo(x) { x + y }").is_err()); @@ -103,6 +112,10 @@ fn test_options_strict_var() -> Result<(), Box> { engine.compile("let x = 42; let f = |y| { || x + y };")?; assert!(engine.compile("fn foo() { |y| { || x + y } }").is_err()); } + assert_eq!( + engine.eval_with_scope::(&mut scope, "fn foo(z) { x * y + z } foo(1)")?, + 1 + ); } Ok(())