Skip to content

Commit ee070ad

Browse files
authored
Unpack function arguments (#1160)
* Unpack function arguments * Fix test_grammar
1 parent 99f54b3 commit ee070ad

10 files changed

Lines changed: 402 additions & 76 deletions

File tree

Src/IronPython/Compiler/Ast/Arg.cs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,20 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5-
using MSAst = System.Linq.Expressions;
5+
#nullable enable
66

7-
using IronPython.Runtime;
8-
using Microsoft.Scripting;
97
using Microsoft.Scripting.Actions;
108

119
namespace IronPython.Compiler.Ast {
1210
public class Arg : Node {
1311
public Arg(Expression expression) : this(null, expression) { }
1412

15-
public Arg(string name, Expression expression) {
13+
public Arg(string? name, Expression expression) {
1614
Name = name;
1715
Expression = expression;
1816
}
1917

20-
public string Name { get; }
18+
public string? Name { get; }
2119

2220
public Expression Expression { get; }
2321

Src/IronPython/Compiler/Ast/AstMethods.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ internal static class AstMethods {
8080
public static readonly MethodInfo GeneratorCheckThrowableAndReturnSendValue = GetMethod((Func<object, object>)PythonOps.GeneratorCheckThrowableAndReturnSendValue);
8181

8282
// methods matching Python opcodes
83+
public static readonly MethodInfo DictMerge = GetMethod((Action<CodeContext, PythonDictionary, object>)PythonOps.DictMerge);
8384
public static readonly MethodInfo DictUpdate = GetMethod((Action<CodeContext, PythonDictionary, object>)PythonOps.DictUpdate);
8485
public static readonly MethodInfo ListAppend = GetMethod((Action<PythonList, object>)PythonOps.ListAppend);
8586
public static readonly MethodInfo ListExtend = GetMethod((Action<PythonList, object>)PythonOps.ListExtend);

Src/IronPython/Compiler/Ast/CallExpression.cs

Lines changed: 149 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,29 @@
22
// The .NET Foundation licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information.
44

5-
using MSAst = System.Linq.Expressions;
5+
#nullable enable
66

77
using System;
88
using System.Collections.Generic;
99
using System.Diagnostics;
1010
using System.Linq;
1111
using System.Runtime.CompilerServices;
1212

13+
using IronPython.Runtime;
14+
1315
using Microsoft.Scripting.Actions;
14-
using Microsoft.Scripting.Utils;
1516
using Microsoft.Scripting.Interpreter;
16-
using Microsoft.Scripting.Runtime;
17+
using Microsoft.Scripting.Utils;
1718

18-
using IronPython.Runtime;
19-
using IronPython.Runtime.Operations;
20-
using IronPython.Runtime.Types;
19+
using AstUtils = Microsoft.Scripting.Ast.Utils;
20+
using MSAst = System.Linq.Expressions;
2121

2222
namespace IronPython.Compiler.Ast {
2323

2424
public class CallExpression : Expression, IInstructionProvider {
2525
public CallExpression(Expression target, Arg[] args) {
26+
// TODO: use two arrays (args/keywords) instead
27+
if (args == null) throw new ArgumentNullException(nameof(args));
2628
Target = target;
2729
Args = args;
2830
}
@@ -36,55 +38,168 @@ public CallExpression(Expression target, Arg[] args) {
3638
public bool NeedsLocalsDictionary() {
3739
if (!(Target is NameExpression nameExpr)) return false;
3840

39-
if (Args.Length == 0) {
40-
if (nameExpr.Name == "locals") return true;
41-
if (nameExpr.Name == "vars") return true;
42-
if (nameExpr.Name == "dir") return true;
43-
return false;
44-
} else if (Args.Length == 1 && (nameExpr.Name == "dir" || nameExpr.Name == "vars")) {
45-
if (Args[0].Name == "*" || Args[0].Name == "**") {
46-
// could be splatting empty list or dict resulting in 0-param call which needs context
47-
return true;
48-
}
49-
} else if (Args.Length == 2 && (nameExpr.Name == "dir" || nameExpr.Name == "vars")) {
50-
if (Args[0].Name == "*" && Args[1].Name == "**") {
51-
// could be splatting empty list and dict resulting in 0-param call which needs context
52-
return true;
53-
}
54-
} else {
55-
if (nameExpr.Name == "eval" || nameExpr.Name == "exec") return true;
41+
if (nameExpr.Name == "eval" || nameExpr.Name == "exec") return true;
42+
if (nameExpr.Name == "dir" || nameExpr.Name == "vars" || nameExpr.Name == "locals") {
43+
// could be splatting empty list or dict resulting in 0-param call which needs context
44+
return Args.All(arg => arg.Name == "*" || arg.Name == "**");
5645
}
46+
5747
return false;
5848
}
5949

6050
public override MSAst.Expression Reduce() {
6151
Arg[] args = Args;
62-
if(Args.Length == 0 && ImplicitArgs.Count > 0) {
52+
if (Args.Length == 0 && ImplicitArgs.Count > 0) {
6353
args = ImplicitArgs.ToArray();
6454
}
6555

66-
MSAst.Expression[] values = new MSAst.Expression[args.Length + 2];
67-
Argument[] kinds = new Argument[args.Length];
56+
SplitArgs(args, out var simpleArgs, out var listArgs, out var namedArgs, out var dictArgs, out var numDict);
57+
58+
Argument[] kinds = new Argument[simpleArgs.Length + Math.Min(listArgs.Length, 1) + namedArgs.Length + (dictArgs.Length - numDict) + Math.Min(numDict, 1)];
59+
MSAst.Expression[] values = new MSAst.Expression[2 + kinds.Length];
6860

6961
values[0] = Parent.LocalContext;
7062
values[1] = Target;
7163

72-
for (int i = 0; i < args.Length; i++) {
73-
kinds[i] = args[i].GetArgumentInfo();
74-
values[i + 2] = args[i].Expression;
64+
int i = 0;
65+
66+
// add simple arguments
67+
foreach (var arg in simpleArgs) {
68+
kinds[i] = arg.GetArgumentInfo();
69+
values[i + 2] = arg.Expression;
70+
i++;
71+
}
72+
73+
// unpack list arguments
74+
if (listArgs.Length > 0) {
75+
var arg = listArgs[0];
76+
Debug.Assert(arg.GetArgumentInfo().Kind == ArgumentType.List);
77+
kinds[i] = arg.GetArgumentInfo();
78+
values[i + 2] = UnpackListHelper(listArgs);
79+
i++;
80+
}
81+
82+
// add named arguments
83+
foreach (var arg in namedArgs) {
84+
kinds[i] = arg.GetArgumentInfo();
85+
values[i + 2] = arg.Expression;
86+
i++;
87+
}
88+
89+
// add named arguments specified after a dict unpack
90+
if (dictArgs.Length != numDict) {
91+
foreach (var arg in dictArgs) {
92+
var info = arg.GetArgumentInfo();
93+
if (info.Kind == ArgumentType.Named) {
94+
kinds[i] = info;
95+
values[i + 2] = arg.Expression;
96+
i++;
97+
}
98+
}
99+
}
100+
101+
// unpack dict arguments
102+
if (dictArgs.Length > 0) {
103+
var arg = dictArgs[0];
104+
Debug.Assert(arg.GetArgumentInfo().Kind == ArgumentType.Dictionary);
105+
kinds[i] = arg.GetArgumentInfo();
106+
values[i + 2] = UnpackDictHelper(Parent.LocalContext, dictArgs);
75107
}
76108

77109
return Parent.Invoke(
78110
new CallSignature(kinds),
79111
values
80112
);
113+
114+
static void SplitArgs(Arg[] args, out ReadOnlySpan<Arg> simpleArgs, out ReadOnlySpan<Arg> listArgs, out ReadOnlySpan<Arg> namedArgs, out ReadOnlySpan<Arg> dictArgs, out int numDict) {
115+
if (args.Length == 0) {
116+
simpleArgs = default;
117+
listArgs = default;
118+
namedArgs = default;
119+
dictArgs = default;
120+
numDict = 0;
121+
return;
122+
}
123+
124+
int idxSimple = args.Length;
125+
int idxList = args.Length;
126+
int idxNamed = args.Length;
127+
int idxDict = args.Length;
128+
numDict = 0;
129+
130+
// we want idxSimple <= idxList <= idxNamed <= idxDict
131+
for (var i = args.Length - 1; i >= 0; i--) {
132+
var arg = args[i];
133+
var info = arg.GetArgumentInfo();
134+
switch (info.Kind) {
135+
case ArgumentType.Simple:
136+
idxSimple = i;
137+
break;
138+
case ArgumentType.List:
139+
idxList = i;
140+
break;
141+
case ArgumentType.Named:
142+
idxNamed = i;
143+
break;
144+
case ArgumentType.Dictionary:
145+
idxDict = i;
146+
numDict++;
147+
break;
148+
default:
149+
throw new InvalidOperationException();
150+
}
151+
}
152+
dictArgs = args.AsSpan(idxDict);
153+
if (idxNamed > idxDict) idxNamed = idxDict;
154+
namedArgs = args.AsSpan(idxNamed, idxDict - idxNamed);
155+
if (idxList > idxNamed) idxList = idxNamed;
156+
listArgs = args.AsSpan(idxList, idxNamed - idxList);
157+
if (idxSimple > idxList) idxSimple = idxList;
158+
simpleArgs = args.AsSpan(idxSimple, idxList - idxSimple);
159+
}
160+
161+
static MSAst.Expression UnpackListHelper(ReadOnlySpan<Arg> args) {
162+
Debug.Assert(args.Length > 0);
163+
Debug.Assert(args[0].GetArgumentInfo().Kind == ArgumentType.List);
164+
if (args.Length == 1) return args[0].Expression;
165+
166+
var expressions = new ReadOnlyCollectionBuilder<MSAst.Expression>(args.Length + 2);
167+
var varExpr = Expression.Variable(typeof(PythonList), "$coll");
168+
expressions.Add(Expression.Assign(varExpr, Expression.Call(AstMethods.MakeEmptyList)));
169+
foreach (var arg in args) {
170+
if (arg.GetArgumentInfo().Kind == ArgumentType.List) {
171+
expressions.Add(Expression.Call(AstMethods.ListExtend, varExpr, AstUtils.Convert(arg.Expression, typeof(object))));
172+
} else {
173+
expressions.Add(Expression.Call(AstMethods.ListAppend, varExpr, AstUtils.Convert(arg.Expression, typeof(object))));
174+
}
175+
}
176+
expressions.Add(varExpr);
177+
return Expression.Block(typeof(PythonList), new MSAst.ParameterExpression[] { varExpr }, expressions);
178+
}
179+
180+
static MSAst.Expression UnpackDictHelper(MSAst.Expression context, ReadOnlySpan<Arg> args) {
181+
Debug.Assert(args.Length > 0);
182+
Debug.Assert(args[0].GetArgumentInfo().Kind == ArgumentType.Dictionary);
183+
if (args.Length == 1) return args[0].Expression;
184+
185+
var expressions = new List<MSAst.Expression>(args.Length + 2);
186+
var varExpr = Expression.Variable(typeof(PythonDictionary), "$dict");
187+
expressions.Add(Expression.Assign(varExpr, Expression.Call(AstMethods.MakeEmptyDict)));
188+
foreach (var arg in args) {
189+
if (arg.GetArgumentInfo().Kind == ArgumentType.Dictionary) {
190+
expressions.Add(Expression.Call(AstMethods.DictMerge, context, varExpr, arg.Expression));
191+
}
192+
}
193+
expressions.Add(varExpr);
194+
return Expression.Block(typeof(PythonDictionary), new MSAst.ParameterExpression[] { varExpr }, expressions);
195+
}
81196
}
82197

83198
#region IInstructionProvider Members
84199

85200
void IInstructionProvider.AddInstructions(LightCompiler compiler) {
86201
Arg[] args = Args;
87-
if(args.Length == 0 && ImplicitArgs.Count > 0) {
202+
if (args.Length == 0 && ImplicitArgs.Count > 0) {
88203
args = ImplicitArgs.ToArray();
89204
}
90205

@@ -158,27 +273,19 @@ void IInstructionProvider.AddInstructions(LightCompiler compiler) {
158273
compiler.Instructions.Emit(new Invoke6Instruction(Parent.PyContext));
159274
return;
160275

161-
// *** END GENERATED CODE ***
276+
// *** END GENERATED CODE ***
162277

163-
#endregion
278+
#endregion
164279
}
165280
compiler.Compile(Reduce());
166281
}
167282

168283
#endregion
169284

170285
private abstract class InvokeInstruction : Instruction {
171-
public override int ProducedStack {
172-
get {
173-
return 1;
174-
}
175-
}
286+
public override int ProducedStack => 1;
176287

177-
public override string InstructionName {
178-
get {
179-
return "Python Invoke" + (ConsumedStack - 1);
180-
}
181-
}
288+
public override string InstructionName => "Python Invoke" + (ConsumedStack - 1);
182289
}
183290

184291
#region Generated Python Call Expression Instructions

Src/IronPython/Compiler/Ast/DictionaryExpression.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public class DictionaryExpression : Expression, IInstructionProvider {
2222
private static readonly MSAst.Expression EmptyDictExpression = Expression.Call(AstMethods.MakeEmptyDict);
2323

2424
public DictionaryExpression(params SliceExpression[] items) {
25+
// TODO: use two arrays instead of SliceExpression
2526
foreach (var item in items) {
2627
if (item.SliceStart is null) _hasNullKey = true;
2728
if (item.SliceStop is null) throw PythonOps.ValueError("None disallowed in expression list");

Src/IronPython/Compiler/Ast/SetExpression.cs

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ public SetExpression(params Expression[] items) {
2929
protected bool HasStarredExpression => Items.OfType<StarredExpression>().Any();
3030

3131
public override MSAst.Expression Reduce() {
32-
if (Items.Count == 0) {
33-
return Expression.Call(AstMethods.MakeEmptySet);
34-
}
35-
3632
if (HasStarredExpression) {
3733
return UnpackSequenceHelper<SetCollection>(Items, AstMethods.MakeEmptySet, AstMethods.SetAdd, AstMethods.SetUpdate);
3834
}

Src/IronPython/Compiler/Parser.cs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,31 +2901,24 @@ private void PushFunction(FunctionDefinition function) {
29012901
}
29022902

29032903
private CallExpression FinishCallExpr(Expression target, params Arg[] args) {
2904-
bool hasArgsTuple = false;
2905-
bool hasKeywordDict = false;
2906-
int keywordCount = 0;
2907-
int extraArgs = 0;
2904+
bool hasKeyword = false;
2905+
bool hasKeywordUnpacking = false;
29082906

29092907
foreach (Arg arg in args) {
29102908
if (arg.Name == null) {
2911-
if (hasArgsTuple || hasKeywordDict || keywordCount > 0) {
2912-
ReportSyntaxError(IronPython.Resources.NonKeywordAfterKeywordArg);
2909+
if (hasKeywordUnpacking) {
2910+
ReportSyntaxError(arg.StartIndex, arg.EndIndex, "positional argument follows keyword argument unpacking");
2911+
} else if (hasKeyword) {
2912+
ReportSyntaxError(arg.StartIndex, arg.EndIndex, "positional argument follows keyword argument");
29132913
}
29142914
} else if (arg.Name == "*") {
2915-
if (hasArgsTuple || hasKeywordDict) {
2916-
ReportSyntaxError(IronPython.Resources.OneListArgOnly);
2915+
if (hasKeywordUnpacking) {
2916+
ReportSyntaxError(arg.StartIndex, arg.EndIndex, "iterable argument unpacking follows keyword argument unpacking");
29172917
}
2918-
hasArgsTuple = true; extraArgs++;
29192918
} else if (arg.Name == "**") {
2920-
if (hasKeywordDict) {
2921-
ReportSyntaxError(IronPython.Resources.OneKeywordArgOnly);
2922-
}
2923-
hasKeywordDict = true; extraArgs++;
2919+
hasKeywordUnpacking = true;
29242920
} else {
2925-
if (hasKeywordDict) {
2926-
ReportSyntaxError(IronPython.Resources.KeywordOutOfSequence);
2927-
}
2928-
keywordCount++;
2921+
hasKeyword = true;
29292922
}
29302923
}
29312924

Src/IronPython/Modules/_ast.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1283,7 +1283,7 @@ internal Dict(DictionaryExpression expr)
12831283
keys = new PythonList(expr.Items.Count);
12841284
values = new PythonList(expr.Items.Count);
12851285
foreach (SliceExpression item in expr.Items) {
1286-
keys.Add(Convert(item.SliceStart));
1286+
keys.Add(item.SliceStart is null ? null : Convert(item.SliceStart));
12871287
values.Add(Convert(item.SliceStop));
12881288
}
12891289
}

Src/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,6 +1486,27 @@ public static PythonTuple MakeTupleFromSequence(object items) {
14861486
return PythonTuple.Make(items);
14871487
}
14881488

1489+
/// <summary>
1490+
/// DICT_MERGE
1491+
/// </summary>
1492+
[EditorBrowsable(EditorBrowsableState.Never)]
1493+
public static void DictMerge(CodeContext context, PythonDictionary dict, object? item) {
1494+
// call dict.keys()
1495+
if (!PythonTypeOps.TryInvokeUnaryOperator(context, item, "keys", out object keys)) {
1496+
throw TypeError($"'{PythonTypeOps.GetName(item)}' object is not a mapping");
1497+
}
1498+
1499+
// enumerate the keys getting their values
1500+
IEnumerator enumerator = GetEnumerator(keys);
1501+
while (enumerator.MoveNext()) {
1502+
object? o = enumerator.Current;
1503+
if (dict.ContainsKey(o) && (o is string || o is Extensible<string>)) {
1504+
throw TypeError($"function got multiple values for keyword argument '{o}'");
1505+
}
1506+
dict[o] = PythonOps.GetIndex(context, item, o);
1507+
}
1508+
}
1509+
14891510
/// <summary>
14901511
/// DICT_UPDATE
14911512
/// </summary>

0 commit comments

Comments
 (0)