Skip to content

Commit 866dd38

Browse files
authored
Complete implementation of nonlocal (#1054)
* Implement nonlocal * Enable nonlocal tests from stdlib * Unblock some nonlocal-dependent tests * Disable failing tests * Revert "Workaround for missing nonlocal implementation (#453)" * test_shelve is not parallel-safe * Implement correct error line number reporting for nonlocal * Add extra tests for nonlocal * Align error type and message with CPython for unassigned variable use * Implement ast.Nonlocal * Cleanup
1 parent a92e9ac commit 866dd38

18 files changed

Lines changed: 585 additions & 63 deletions

Src/IronPython/Compiler/Ast/AstMethods.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ internal static class AstMethods {
3131
public static readonly MethodInfo ListAddForComprehension = GetMethod((Action<PythonList, object>)PythonOps.ListAddForComprehension);
3232
public static readonly MethodInfo SetAddForComprehension = GetMethod((Action<SetCollection, object>)PythonOps.SetAddForComprehension);
3333
public static readonly MethodInfo DictAddForComprehension = GetMethod((Action<PythonDictionary, object, object>)PythonOps.DictAddForComprehension);
34-
public static readonly MethodInfo CheckUninitialized = GetMethod((Func<object, string, object>)PythonOps.CheckUninitialized);
34+
public static readonly MethodInfo CheckUninitializedFree = GetMethod((Func<object, string, object>)PythonOps.CheckUninitializedFree);
35+
public static readonly MethodInfo CheckUninitializedLocal = GetMethod((Func<object, string, object>)PythonOps.CheckUninitializedLocal);
3536
public static readonly MethodInfo PublishModule = GetMethod((Func<CodeContext, string, object>)PythonOps.PublishModule);
3637
public static readonly MethodInfo RemoveModule = GetMethod((Action<CodeContext, string, object>)PythonOps.RemoveModule);
3738
public static readonly MethodInfo ModuleStarted = GetMethod((Action<CodeContext, ModuleOptions>)PythonOps.ModuleStarted);

Src/IronPython/Compiler/Ast/ClassDefinition.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,12 +114,15 @@ internal override PythonVariable BindReference(PythonNameBinder binder, PythonRe
114114
return null;
115115
}
116116

117-
return variable;
117+
if (variable.Kind != VariableKind.Nonlocal) {
118+
return variable;
119+
}
118120
}
119121

120122
// Try to bind in outer scopes, if we have an unqualified exec we need to leave the
121123
// variables as free for the same reason that locals are accessed by name.
122-
for (ScopeStatement parent = Parent; parent != null; parent = parent.Parent) {
124+
bool stopAtGlobal = variable?.Kind == VariableKind.Nonlocal;
125+
for (ScopeStatement parent = Parent; parent != null && !(stopAtGlobal && parent.IsGlobal); parent = parent.Parent) {
123126
if (parent.TryBindOuter(this, reference, out variable)) {
124127
return variable;
125128
}

Src/IronPython/Compiler/Ast/Comprehension.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ internal override PythonVariable BindReference(PythonNameBinder binder, PythonRe
242242
if (variable.Kind == VariableKind.Global) {
243243
AddReferencedGlobal(reference.Name);
244244
}
245+
Debug.Assert(variable.Kind != VariableKind.Nonlocal, "there should be no nonlocals in a comprehension");
245246
return variable;
246247
}
247248

Src/IronPython/Compiler/Ast/FlowChecker.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ public override bool Walk(Parameter node) {
116116
}
117117
}
118118

119+
// TODO: probably obsolete, PythonVariable.Deleted does the job better (across scopes)
119120
internal class FlowDeleter : PythonWalkerNonRecursive {
120121
private readonly FlowChecker _fc;
121122

@@ -192,7 +193,7 @@ private void SetInitialized(PythonVariable/*!*/ variable, bool value) {
192193
_bits.Set(variable.Index * 2 + 1, value);
193194
}
194195
private bool IsAssigned(PythonVariable/*!*/ variable) {
195-
return _bits.Get(variable.Index * 2);
196+
return _bits.Get(variable.Index * 2) && !variable.MaybeDeleted;
196197
}
197198

198199
private bool IsInitialized(PythonVariable/*!*/ variable) {

Src/IronPython/Compiler/Ast/FunctionDefinition.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ internal override FunctionAttributes Flags {
188188
internal override bool TryBindOuter(ScopeStatement from, PythonReference reference, out PythonVariable variable) {
189189
// Functions expose their locals to direct access
190190
ContainsNestedFreeVariables = true;
191-
if (TryGetVariable(reference.Name, out variable)) {
191+
if (TryGetVariable(reference.Name, out variable) && variable.Kind != VariableKind.Nonlocal) {
192192
variable.AccessedInNestedScope = true;
193193

194194
if (variable.Kind == VariableKind.Local || variable.Kind == VariableKind.Parameter) {
@@ -215,11 +215,15 @@ internal override PythonVariable BindReference(PythonNameBinder binder, PythonRe
215215
if (variable.Kind == VariableKind.Global) {
216216
AddReferencedGlobal(reference.Name);
217217
}
218-
return variable;
218+
219+
if (variable.Kind != VariableKind.Nonlocal) {
220+
return variable;
221+
}
219222
}
220223

221224
// Try to bind in outer scopes
222-
for (ScopeStatement parent = Parent; parent != null; parent = parent.Parent) {
225+
bool stopAtGlobal = variable?.Kind == VariableKind.Nonlocal;
226+
for (ScopeStatement parent = Parent; parent != null && !(stopAtGlobal && parent.IsGlobal); parent = parent.Parent) {
223227
if (parent.TryBindOuter(this, reference, out variable)) {
224228
return variable;
225229
}

Src/IronPython/Compiler/Ast/NameExpression.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ public override MSAst.Expression Reduce() {
4444

4545
if (!Assigned && !(read is IPythonGlobalExpression)) {
4646
read = Ast.Call(
47-
AstMethods.CheckUninitialized,
47+
Parent.IsFreeVariable(Reference.PythonVariable) ?
48+
AstMethods.CheckUninitializedFree :
49+
AstMethods.CheckUninitializedLocal,
4850
read,
4951
Ast.Constant(Name)
5052
);

Src/IronPython/Compiler/Ast/PythonNameBinder.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* if (cond): del x # illegal because x is a closure variable
3333
* def g():
3434
* print x
35+
* TODO: the example above is no longer valid; also the description of the name binding algorithm does not match the code
3536
*/
3637

3738
namespace IronPython.Compiler.Ast {
@@ -69,6 +70,7 @@ public override bool Walk(Parameter node) {
6970
return false;
7071
}
7172

73+
// TODO: obsolete?
7274
private void WalkTuple(TupleExpression tuple) {
7375
tuple.Parent = _binder._currentScope;
7476
foreach (Expression innerNode in tuple.Items) {
@@ -190,7 +192,7 @@ internal PythonVariable DefineParameter(string name) {
190192

191193
internal PythonVariable DefineDeleted(string name) {
192194
PythonVariable variable = _currentScope.EnsureVariable(name);
193-
variable.Deleted = true;
195+
variable.RegisterDeletion();
194196
return variable;
195197
}
196198

@@ -691,6 +693,10 @@ public override bool Walk(GlobalStatement node) {
691693
case VariableKind.Parameter:
692694
ReportSyntaxError($"name '{n}' is parameter and global", node);
693695
break;
696+
697+
case VariableKind.Nonlocal:
698+
ReportSyntaxError($"name '{n}' is nonlocal and global", node);
699+
break;
694700
}
695701
}
696702

@@ -734,21 +740,27 @@ public override bool Walk(NonlocalStatement node) {
734740
case VariableKind.Global:
735741
ReportSyntaxError($"name '{n}' is nonlocal and global", node);
736742
break;
743+
737744
case VariableKind.Local:
738745
ReportSyntaxError($"name '{n}' is assigned to before nonlocal declaration", node);
739746
break;
747+
740748
case VariableKind.Parameter:
741749
ReportSyntaxError($"name '{n}' is parameter and nonlocal", node);
742750
break;
751+
752+
case VariableKind.Nonlocal:
753+
// no conflict, name redeclared as nonlocal
754+
break;
743755
}
744756
}
745757

746758
// Check for the name being referenced previously
747-
if (_currentScope.IsReferenced(n)) {
759+
if (_currentScope.IsReferenced(n) && conflict is null) {
748760
ReportSyntaxError($"name '{n}' is used prior to nonlocal declaration", node);
749761
}
750762

751-
// TODO: do we need to do other stuff here?
763+
_currentScope.EnsureNonlocalVariable(n, node);
752764
}
753765
return true;
754766
}

Src/IronPython/Compiler/Ast/PythonVariable.cs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,20 @@
1616
namespace IronPython.Compiler.Ast {
1717
using Ast = MSAst.Expression;
1818

19+
[DebuggerDisplay("{Kind} {Name} from {Scope.Name}")]
1920
public class PythonVariable {
2021

2122
public PythonVariable(string name, VariableKind kind, ScopeStatement/*!*/ scope) {
2223
Assert.NotNull(scope);
24+
Debug.Assert(kind != VariableKind.Nonlocal || !scope.IsGlobal);
2325
Name = name;
2426
Kind = kind;
2527
Scope = scope;
2628
}
2729

30+
/// <summary>
31+
/// The name of the variable as used in Python code.
32+
/// </summary>
2833
public string Name { get; }
2934

3035
public bool IsGlobal {
@@ -33,28 +38,56 @@ public bool IsGlobal {
3338
}
3439
}
3540

41+
/// <summary>
42+
/// The original scope in which the variable is defined.
43+
/// </summary>
3644
public ScopeStatement Scope { get; }
3745

38-
public VariableKind Kind { get; set; }
46+
public VariableKind Kind { get; set; } // TODO: make readonly
47+
48+
/// <summary>
49+
/// The actual variable represented by this variable instance.
50+
/// For reference variables this may be null if the reference is not yet resolved.
51+
/// </summary>
52+
public virtual PythonVariable LimitVariable => this;
53+
54+
/// <summary>
55+
/// Gets a value indicating whether the variable gets deleted by a <c>del</c> statement in any scope.
56+
/// </summary>
57+
internal bool MaybeDeleted { get; private set; }
3958

4059
/// <summary>
41-
/// Gets or sets a value indicating whether the variable gets deleted.
60+
/// Mark the variable as argument to a del statement in some scope.
4261
/// </summary>
43-
internal bool Deleted { get; set; }
62+
internal void RegisterDeletion() => MaybeDeleted = true;
4463

4564
/// <summary>
4665
/// Gets the index used for tracking in the flow checker.
4766
/// </summary>
4867
internal int Index { get; set; }
4968

5069
/// <summary>
51-
/// True iff there is a path in control flow graph on which the variable is used before initialized (assigned or deleted).
70+
/// True iff there is a path in the control flow graph of a single scope
71+
/// on which the variable is used before explicitly initialized (assigned or deleted)
72+
/// in that scope.
5273
/// </summary>
5374
public bool ReadBeforeInitialized { get; set; }
5475

5576
/// <summary>
56-
/// True iff the variable is referred to from the inner scope.
77+
/// True iff the variable is referred to from an inner scope.
5778
/// </summary>
5879
public bool AccessedInNestedScope { get; set; }
5980
}
81+
82+
internal class PythonReferenceVariable : PythonVariable {
83+
84+
internal PythonReferenceVariable(PythonReference reference, ScopeStatement scope)
85+
: base(reference.Name, VariableKind.Nonlocal, scope) {
86+
Reference = reference;
87+
}
88+
89+
internal PythonReference Reference { get; }
90+
91+
public override PythonVariable LimitVariable => Reference.PythonVariable?.LimitVariable;
92+
}
6093
}

Src/IronPython/Compiler/Ast/ScopeStatement.cs

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public abstract class ScopeStatement : Statement {
3232
private List<string> _globalVars; // global variables accessed from this scope
3333
private List<string> _cellVars; // variables accessed from nested scopes
3434
private Dictionary<string, PythonReference> _references; // names of all variables referenced, null after binding completes
35+
private Dictionary<string, NonlocalStatement> _nonlocalVars; // nonlocal variables declared in this scope, null after binding completes
3536

3637
internal Dictionary<PythonVariable, MSAst.Expression> _variableMapping = new Dictionary<PythonVariable, MSAst.Expression>();
3738
private readonly DelayedFunctionCode _funcCodeExpr = new DelayedFunctionCode(); // expression that refers to the function code for this scope
@@ -184,12 +185,20 @@ internal virtual IList<string> GetVarNames() {
184185

185186

186187
internal void AddFreeVariable(PythonVariable variable, bool accessedInScope) {
188+
Debug.Assert(variable?.Kind is VariableKind.Local or VariableKind.Parameter);
189+
187190
if (_freeVars == null) {
188191
_freeVars = new List<PythonVariable>();
189192
}
190193

191194
if (!_freeVars.Contains(variable)) {
192195
_freeVars.Add(variable);
196+
if (TryGetVariable(variable.Name, out PythonVariable nonlocal) &&
197+
nonlocal.Kind is VariableKind.Nonlocal &&
198+
nonlocal.MaybeDeleted) {
199+
200+
variable.RegisterDeletion();
201+
}
193202
}
194203
}
195204

@@ -331,6 +340,9 @@ internal virtual void Bind(PythonNameBinder binder) {
331340
if (_references != null) {
332341
foreach (var reference in _references.Values) {
333342
reference.PythonVariable = BindReference(binder, reference);
343+
if (reference.PythonVariable is null && TryGetNonlocalStatement(reference.Name, out NonlocalStatement node)) {
344+
binder.ReportSyntaxError($"no binding for nonlocal '{reference.Name}' found", node);
345+
}
334346
}
335347
}
336348
}
@@ -350,10 +362,10 @@ internal virtual void FinishBind(PythonNameBinder binder) {
350362
if (FreeVariables != null && FreeVariables.Count > 0) {
351363
LocalParentTuple = Ast.Parameter(Parent.GetClosureTupleType(), "$tuple");
352364

353-
foreach (var variable in _freeVars) {
354-
var parentClosure = Parent._closureVariables;
355-
Debug.Assert(parentClosure != null);
365+
var parentClosure = Parent._closureVariables;
366+
Debug.Assert(parentClosure != null);
356367

368+
foreach (var variable in FreeVariables) {
357369
for (int i = 0; i < parentClosure.Length; i++) {
358370
if (parentClosure[i].Variable == variable) {
359371
_variableMapping[variable] = new ClosureExpression(variable, Ast.Property(LocalParentTuple, String.Format("Item{0:D3}", i)), null);
@@ -372,7 +384,10 @@ internal virtual void FinishBind(PythonNameBinder binder) {
372384
if (Variables != null) {
373385
foreach (PythonVariable variable in Variables.Values) {
374386
if (!HasClosureVariable(closureVariables, variable) &&
375-
!variable.IsGlobal && (variable.AccessedInNestedScope || ExposesLocalVariable(variable))) {
387+
!variable.IsGlobal &&
388+
variable.Kind != VariableKind.Nonlocal &&
389+
(variable.AccessedInNestedScope || ExposesLocalVariable(variable))) {
390+
376391
if (closureVariables == null) {
377392
closureVariables = new List<ClosureInfo>();
378393
}
@@ -397,6 +412,7 @@ internal virtual void FinishBind(PythonNameBinder binder) {
397412

398413
// no longer needed
399414
_references = null;
415+
_nonlocalVars = null;
400416
}
401417

402418
private static bool HasClosureVariable(List<ClosureInfo> closureVariables, PythonVariable variable) {
@@ -440,6 +456,15 @@ internal bool IsReferenced(string name) {
440456
return _references != null && _references.TryGetValue(name, out reference);
441457
}
442458

459+
internal bool IsFreeVariable(PythonVariable variable) {
460+
return FreeVariables?.Contains(variable?.LimitVariable) ?? false;
461+
}
462+
463+
private bool TryGetNonlocalStatement(string name, out NonlocalStatement node) {
464+
node = null;
465+
return _nonlocalVars?.TryGetValue(name, out node) ?? false;
466+
}
467+
443468
internal PythonVariable/*!*/ CreateVariable(string name, VariableKind kind) {
444469
EnsureVariables();
445470
Debug.Assert(!Variables.ContainsKey(name));
@@ -456,6 +481,24 @@ internal bool IsReferenced(string name) {
456481
return variable;
457482
}
458483

484+
private PythonVariable CreateNonlocalVariable(string name) {
485+
EnsureVariables();
486+
Debug.Assert(!Variables.ContainsKey(name));
487+
Debug.Assert(!IsReferenced(name));
488+
PythonReference reference = Reference(name);
489+
PythonVariable variable;
490+
Variables[name] = variable = new PythonReferenceVariable(reference, this);
491+
return variable;
492+
}
493+
494+
internal void EnsureNonlocalVariable(string name, NonlocalStatement node) {
495+
_nonlocalVars ??= new();
496+
if (!_nonlocalVars.ContainsKey(name)) {
497+
CreateNonlocalVariable(name);
498+
_nonlocalVars[name] = node;
499+
}
500+
}
501+
459502
internal PythonVariable DefineParameter(string name) {
460503
return CreateVariable(name, VariableKind.Parameter);
461504
}
@@ -656,18 +699,19 @@ private MSAst.Expression GetClosureCell(ClosureInfo variable) {
656699
}
657700

658701
internal virtual MSAst.Expression GetVariableExpression(PythonVariable variable) {
702+
Assert.NotNull(variable?.LimitVariable);
659703
if (variable.IsGlobal) {
660704
return GlobalParent.ModuleVariables[variable];
661705
}
662706

663-
Debug.Assert(_variableMapping.ContainsKey(variable));
664-
return _variableMapping[variable];
707+
Debug.Assert(_variableMapping.ContainsKey(variable.LimitVariable));
708+
return _variableMapping[variable.LimitVariable];
665709
}
666710

667711
internal void CreateVariables(ReadOnlyCollectionBuilder<MSAst.ParameterExpression> locals, List<MSAst.Expression> init) {
668712
if (Variables != null) {
669713
foreach (PythonVariable variable in Variables.Values) {
670-
if (variable.Kind != VariableKind.Global) {
714+
if (variable.Kind is VariableKind.Local or VariableKind.Parameter) {
671715
if (GetVariableExpression(variable) is ClosureExpression closure) {
672716
init.Add(closure.Create());
673717
locals.Add((MSAst.ParameterExpression)closure.ClosureCell);

0 commit comments

Comments
 (0)