Show Nice Error when Variable Shadowing Occurs in Inner Functions#358
Show Nice Error when Variable Shadowing Occurs in Inner Functions#358JeffersGlass wants to merge 5 commits into
Conversation
|
@JeffersGlass I'm -0.5 to merge this as is. The bug is the same as #231. I tried to apply this patch from #279 and I confirm it fixes the bug: I agree that having the assertion error is not great, but we should't "officialize" the bug by making it a proper exception with a test. The best course of action IMHO is to complete and merge #279, which would fix both #231 and #340 at once. If that's not possible and/or takes too long, I'm fine raising a @aisipos do you maybe want me and/or @JeffersGlass to take over PR #279 ? |
|
So #279 fixes fixes the original example from #340, but not this further example. def foo() -> None:
x = 1
def bar() -> None:
x = 2
bar()
def main() -> None:
foo()On the 279 branch, this still gives the AssertionError about |
@JeffersGlass good catch (as always :)). I think that this example is actually a different kind of bug compared to #340. Let's examine the code around the failing assertion. It says that if a symbol has Lines 466 to 467 in 49304de Lines 125 to 127 in 49304de This is and will always be correct, because local variables are the only ones which can be assigned directly. All the other assignments must be of kind So, in the code above the bug is in the You can see it more clearly from this example: var x: int = 0
def baz() -> None:
x = 2
def main() -> None:
baz()Here, you can see that inside But in your failing example, the ScopeAnalyzer makes a mistake: def foo() -> None:
x = 1
def bar() -> None:
x = 2
bar()
def main() -> None:
foo()I think the bug originates here: Lines 333 to 335 in 49304de Lines 350 to 364 in 49304de Probably the correct logic is something like this (but it's almost 1am here so I might be wrong 😅): def _declare_target_maybe(self, target: ast.StrConst, value: ast.Expr) -> None:
# if target name does not exist elsewhere, we treat it as an implicit
# declaration
level, scope, sym = self.lookup_ref(target.value)
if sym is None:
# First assignment: mark as const unless in a loop
type_loc = value.loc
if self.loop_depth > 0:
varkind: VarKind = "var"
else:
varkind = "const"
self.define_name(target.value, varkind, "auto", target.loc, type_loc)
elif level == 0:
# possible second assignment: promote to var if needed
self._promote_const_to_var_maybe(target)
else:
# we should raise "shadowing error" here, possibly by calling define_name
XXX |
|
Thank you for the full explanation! I think that all makes sense to me, but in looking at implementing it, it raises a further question about Lines 213 to 229 in cfeae13 As the comment suggests, the module level x ends up as |
yes, I think it should raise shadowing error eagerly in that case |
|
I think I've gotten a bit turned around in my head about how the scoping is supposed to work... backing up a step and writing a test, does this test capture the desired properties in the scopes of the two functions? def test_inner_function_storage(self):
src = """
def foo() -> None:
x = 1
def bar() -> None:
x = 2
bar()
"""
scopes = self.analyze(src)
def get_scope(name: str) -> SymTable:
for funcdef, scope in scopes.inner_scopes.items():
if funcdef.name == name:
return scope
raise KeyError
x_sym_foo = get_scope("foo").lookup_maybe("x")
assert x_sym_foo is not None
assert x_sym_foo.varkind == "var"
assert x_sym_foo.storage == "cell"
assert not get_scope("bar").has_definition("x") #x should be referenced but not defined in inner scope
x_sym_bar = get_scope("bar").lookup_maybe("x")
assert x_sym_bar is not None
assert x_sym_bar.varkind == "var"
assert x_sym_bar.storage == "cell" |
that's a good question, to which I don't know the definitive answer :). Let's try to rewrite our example to be more explicit: # case 1: I expect a ScopeError because x=2 tries to mutate a const
def foo() -> None:
const x = 1
def bar() -> None:
x = 2
bar()
# case 2: I expect it works because x is var
def foo() -> None:
var x = 1
def bar() -> None:
x = 2
bar()I think that your test should pass if we decide that The current rules w.r.t
I never took into consideration the case "it's assigned only once in the outer scope but also inside a closure". Note that so far we didn't need to think about it because we simply don't support red closures. For blue closures, the answer is easy since the outer variables are always I think that the best way is to declare that if you want to mutate a closed-over variable, you MUST declare it as @JeffersGlass does this answer your question? |
Turns an
assertstatement that symbol name should be local into a nice error message, when a variable in an inner func shadows an outer func. Currently interp_only, since funcdefs cannot be redshifted (yet).#231 may be the root of this as well, or another cause - this PR is just about adding the nice error message for now.
Closes #340