diff --git a/Lib/test/test_named_expression.py b/Lib/test/test_named_expression.py index 74500c4a0..692265688 100644 --- a/Lib/test/test_named_expression.py +++ b/Lib/test/test_named_expression.py @@ -166,7 +166,7 @@ class NamedExpressionInvalidTest(unittest.TestCase): with self.assertRaisesRegex(SyntaxError, msg): exec(f"lambda: {code}", {}) # Function scope - @unittest.expectedFailure # TODO RustPython + @unittest.expectedFailure # TODO RustPythonskip def test_named_expression_invalid_comprehension_iterable_expression(self): cases = [ ("Top level", "[i for i in (i := range(5))]"), @@ -394,21 +394,21 @@ print(a)""" self.assertEqual(b, [1, 1]) self.assertEqual(a, 1) - # the following test is not from CPyrgon and just as refernce for a common scoping problem of RustPython - @unittest.skip # needs skipping due to weired behaviour + # the following test is not from CPython and just as refernce for a common scoping problem of RustPython def test_named_expression_scop_10_rp_scope_prob(self): def foo(): - rr=0 + rr=36 def foo0(): nonlocal rr def foo1(): nonlocal rr + self.assertEqual(rr,36) rr+=42 foo1() foo0() return rr - self.assertEqual(foo(), 42) + self.assertEqual(foo(), 78) def test_named_expression_scope_11(self): @@ -550,7 +550,6 @@ spam()""" f() self.assertEqual(GLOBAL_VAR, None) - @unittest.expectedFailure # TODO RustPython def test_named_expression_nonlocal_scope(self): sentinel = object() def f(): diff --git a/compiler/src/symboltable.rs b/compiler/src/symboltable.rs index ca9594e79..cb53c017c 100644 --- a/compiler/src/symboltable.rs +++ b/compiler/src/symboltable.rs @@ -221,11 +221,19 @@ impl<'a> SymbolTableAnalyzer<'a> { } else { match symbol.scope { SymbolScope::Nonlocal => { - // check if name is defined in parent table! - let parent_symbol_table = self.tables.last(); - if let Some((symbols, _)) = parent_symbol_table { - let scope_depth = self.tables.len(); - if !symbols.contains_key(&symbol.name) || scope_depth < 2 { + let scope_depth = self.tables.len(); + if scope_depth > 0 { + // check if the name is already defined in any outer scope + // therefore + if scope_depth < 2 + || !self + .tables + .iter() + .skip(1) // omit the global scope as it is not non-local + .rev() // revert the order for better performance + .any(|t| t.0.contains_key(&symbol.name)) + // true when any of symbol tables contains the name -> then negate + { return Err(SymbolTableError { error: format!("no binding for nonlocal '{}' found", symbol.name), location: Default::default(), diff --git a/vm/src/scope.rs b/vm/src/scope.rs index bbba87a30..b7203e0c9 100644 --- a/vm/src/scope.rs +++ b/vm/src/scope.rs @@ -113,11 +113,29 @@ impl NameProtocol for Scope { } fn store_cell(&self, vm: &VirtualMachine, name: &str, value: PyObjectRef) { - self.locals - .get(1) - .expect("no outer scope for non-local") - .set_item(name, value, vm) - .unwrap(); + // find the innermost outer scope that contains the symbol name + if let Some(locals) = self + .locals + .iter() + .rev() + .filter(|l| l.contains_key(name, vm)) + .nth(0) + { + // add to the symbol + locals.set_item(name, value, vm).unwrap(); + } else { + // somewhat limited solution -> fallback to the old rustpython strategy + // and store the next outer scope + // This case is usually considered as a failure case, but kept for the moment + // to support the scope propagation for named expression assignments to so far + // unknown names in comprehensions. We need to consider here more context + // information for correct handling. + self.locals + .get(1) + .expect("no outer scope for non-local") + .set_item(name, value, vm) + .unwrap(); + } } fn store_name(&self, vm: &VirtualMachine, key: &str, value: PyObjectRef) {