Skip to content

Commit 2c684b3

Browse files
authored
Add dict unpacking (#1152)
* Add nullable annotations to DictionaryExpression * Add dict unpacking
1 parent 88a0c6e commit 2c684b3

5 files changed

Lines changed: 153 additions & 60 deletions

File tree

Src/IronPython/Compiler/Ast/AstMethods.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ internal static class AstMethods {
7777
public static readonly MethodInfo PushFrame = GetMethod((Func<CodeContext, FunctionCode, List<FunctionStack>>)PythonOps.PushFrame);
7878
public static readonly MethodInfo FormatString = GetMethod((Func<CodeContext, string, object, string>)PythonOps.FormatString);
7979
public static readonly MethodInfo GeneratorCheckThrowableAndReturnSendValue = GetMethod((Func<object, object>)PythonOps.GeneratorCheckThrowableAndReturnSendValue);
80-
80+
81+
// methods matching Python opcodes
82+
public static readonly MethodInfo DictUpdate = GetMethod((Action<CodeContext, PythonDictionary, object>)PythonOps.DictUpdate);
83+
8184
private static MethodInfo GetMethod(Delegate x) {
8285
return x.Method;
8386
}

Src/IronPython/Compiler/Ast/DictionaryExpression.cs

Lines changed: 76 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,53 +2,101 @@
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;
9-
10-
using Microsoft.Scripting.Interpreter;
9+
using System.Diagnostics;
1110

1211
using IronPython.Runtime;
1312
using IronPython.Runtime.Operations;
1413

15-
using AstUtils = Microsoft.Scripting.Ast.Utils;
14+
using Microsoft.Scripting.Interpreter;
15+
16+
using MSAst = System.Linq.Expressions;
1617

1718
namespace IronPython.Compiler.Ast {
18-
using Ast = MSAst.Expression;
19-
2019
public class DictionaryExpression : Expression, IInstructionProvider {
2120
private readonly SliceExpression[] _items;
22-
private static readonly MSAst.Expression EmptyDictExpression = Ast.Call(AstMethods.MakeEmptyDict);
21+
private readonly bool _hasNullKey;
22+
private static readonly MSAst.Expression EmptyDictExpression = Expression.Call(AstMethods.MakeEmptyDict);
2323

2424
public DictionaryExpression(params SliceExpression[] items) {
25+
foreach (var item in items) {
26+
if (item.SliceStart is null) _hasNullKey = true;
27+
if (item.SliceStop is null) throw PythonOps.ValueError("None disallowed in expression list");
28+
}
2529
_items = items;
2630
}
2731

28-
public IList<SliceExpression> Items => _items;
32+
public IReadOnlyList<SliceExpression> Items => _items;
2933

3034
public override MSAst.Expression Reduce() {
35+
// empty dictionary
36+
if (_items.Length == 0) {
37+
return EmptyDictExpression;
38+
}
39+
40+
if (_hasNullKey) {
41+
// TODO: unpack constant dicts?
42+
return ReduceDictionaryWithUnpack(Parent.LocalContext, _items.AsSpan());
43+
}
44+
3145
// create keys & values into array and then call helper function
3246
// which creates the dictionary
33-
if (_items.Length != 0) {
34-
return ReduceConstant() ?? ReduceDictionaryWithItems();
35-
}
47+
return ReduceConstant() ?? ReduceDictionaryWithItems(_items.AsSpan());
48+
}
3649

37-
// empty dictionary
38-
return EmptyDictExpression;
50+
private static MSAst.Expression ReduceDictionaryWithUnpack(MSAst.Expression context, ReadOnlySpan<SliceExpression> items) {
51+
Debug.Assert(items.Length > 0);
52+
var expressions = new List<MSAst.Expression>(items.Length + 2);
53+
var varExpr = Expression.Variable(typeof(PythonDictionary), "$dict");
54+
bool isInit = false;
55+
var cnt = 0;
56+
for (var i = 0; i < items.Length; i++) {
57+
var item = items[i];
58+
if (item.SliceStart is null) {
59+
if (cnt != 0) {
60+
var dict = ReduceDictionaryWithItems(items.Slice(i - cnt, cnt));
61+
if (!isInit) {
62+
expressions.Add(Expression.Assign(varExpr, dict));
63+
isInit = true;
64+
} else {
65+
expressions.Add(Expression.Call(AstMethods.DictUpdate, context, varExpr, dict));
66+
}
67+
cnt = 0;
68+
}
69+
if (!isInit) {
70+
expressions.Add(Expression.Assign(varExpr, EmptyDictExpression));
71+
isInit = true;
72+
}
73+
expressions.Add(Expression.Call(AstMethods.DictUpdate, context, varExpr, TransformOrConstantNull(item.SliceStop, typeof(object))));
74+
} else {
75+
cnt++;
76+
}
77+
}
78+
if (cnt != 0) {
79+
var dict = ReduceDictionaryWithItems(items.Slice(items.Length - cnt, cnt));
80+
if (isInit) {
81+
expressions.Add(Expression.Call(AstMethods.DictUpdate, context, varExpr, dict));
82+
} else {
83+
return dict;
84+
}
85+
}
86+
expressions.Add(varExpr);
87+
return Expression.Block(typeof(PythonDictionary), new MSAst.ParameterExpression[] { varExpr }, expressions);
3988
}
4089

41-
private MSAst.Expression ReduceDictionaryWithItems() {
42-
MSAst.Expression[] parts = new MSAst.Expression[_items.Length * 2];
43-
Type t = null;
90+
private static MSAst.Expression ReduceDictionaryWithItems(ReadOnlySpan<SliceExpression> items) {
91+
MSAst.Expression[] parts = new MSAst.Expression[items.Length * 2];
92+
Type? t = null;
4493
bool heterogeneous = false;
45-
for (int index = 0; index < _items.Length; index++) {
46-
SliceExpression slice = _items[index];
94+
for (int index = 0; index < items.Length; index++) {
95+
SliceExpression slice = items[index];
4796
// Eval order should be:
4897
// { 2 : 1, 4 : 3, 6 :5 }
4998
// This is backwards from parameter list eval, so create temporaries to swap ordering.
5099

51-
52100
parts[index * 2] = TransformOrConstantNull(slice.SliceStop, typeof(object));
53101
MSAst.Expression key = parts[index * 2 + 1] = TransformOrConstantNull(slice.SliceStart, typeof(object));
54102

@@ -68,19 +116,19 @@ private MSAst.Expression ReduceDictionaryWithItems() {
68116
}
69117
}
70118

71-
return Ast.Call(
119+
return Expression.Call(
72120
heterogeneous ? AstMethods.MakeDictFromItems : AstMethods.MakeHomogeneousDictFromItems,
73-
Ast.NewArrayInit(
121+
Expression.NewArrayInit(
74122
typeof(object),
75123
parts
76124
)
77125
);
78126
}
79127

80-
private MSAst.Expression ReduceConstant() {
128+
private MSAst.Expression? ReduceConstant() {
81129
for (int index = 0; index < _items.Length; index++) {
82130
SliceExpression slice = _items[index];
83-
if (!slice.SliceStop.IsConstant || !slice.SliceStart.IsConstant) {
131+
if (slice.SliceStart is null || !slice.SliceStart.IsConstant || !slice.SliceStop!.IsConstant) {
84132
return null;
85133
}
86134
}
@@ -89,11 +137,11 @@ private MSAst.Expression ReduceConstant() {
89137
for (int index = 0; index < _items.Length; index++) {
90138
SliceExpression slice = _items[index];
91139

92-
storage.AddNoLock(slice.SliceStart.GetConstantValue(), slice.SliceStop.GetConstantValue());
140+
Debug.Assert(slice.SliceStart is not null);
141+
storage.AddNoLock(slice.SliceStart!.GetConstantValue(), slice.SliceStop!.GetConstantValue());
93142
}
94143

95-
96-
return Ast.Call(AstMethods.MakeConstantDict, Ast.Constant(new ConstantDictionaryStorage(storage), typeof(object)));
144+
return Expression.Call(AstMethods.MakeConstantDict, Expression.Constant(new ConstantDictionaryStorage(storage), typeof(object)));
97145
}
98146

99147
public override void Walk(PythonWalker walker) {
@@ -120,8 +168,8 @@ void IInstructionProvider.AddInstructions(LightCompiler compiler) {
120168

121169
#endregion
122170

123-
private class EmptyDictInstruction: Instruction {
124-
public static EmptyDictInstruction Instance = new EmptyDictInstruction();
171+
private class EmptyDictInstruction : Instruction {
172+
public static readonly EmptyDictInstruction Instance = new EmptyDictInstruction();
125173

126174
public override int Run(InterpretedFrame frame) {
127175
frame.Push(PythonOps.MakeEmptyDict());

Src/IronPython/Compiler/Parser.cs

Lines changed: 44 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2562,8 +2562,10 @@ private IfStatement ParseGenExprIf() {
25622562
}
25632563

25642564
// dict_display: '{' [dictorsetmaker] '}'
2565-
// dictorsetmaker: ( (test ':' test (comp_for | (',' test ':' test)* [','])) |
2566-
// (test (comp_for | (',' test)* [','])) )
2565+
// dictorsetmaker: ( ((test ':' test | '**' expr)
2566+
// (comp_for | (',' (test ':' test | '**' expr))* [','])) |
2567+
// ((test | star_expr)
2568+
// (comp_for | (',' (test | star_expr))* [','])) )
25672569
private Expression FinishDictOrSetValue() {
25682570
var oStart = GetStart();
25692571
var oEnd = GetEnd();
@@ -2578,43 +2580,55 @@ private Expression FinishDictOrSetValue() {
25782580
break;
25792581
}
25802582
bool first = false;
2581-
Expression e1 = ParseTest();
2582-
if (MaybeEat(TokenKind.Colon)) { // dict literal
2583-
if (setMembers != null) {
2584-
ReportSyntaxError("invalid syntax");
2585-
} else if (dictMembers == null) {
2586-
dictMembers = new List<SliceExpression>();
2583+
if (MaybeEat(TokenKind.Power)) {
2584+
if (setMembers is not null) ReportSyntaxError("invalid syntax");
2585+
if (dictMembers is null) {
2586+
dictMembers = new();
25872587
first = true;
25882588
}
2589-
Expression e2 = ParseTest();
2590-
2591-
if (PeekToken(Tokens.KeywordForToken)) {
2592-
if (!first) {
2589+
var expr = ParseExpr();
2590+
var se = new SliceExpression(null, expr, null);
2591+
se.SetLoc(_globalParent, expr.StartIndex, expr.EndIndex);
2592+
dictMembers.Add(se);
2593+
} else {
2594+
Expression e1 = ParseTest();
2595+
if (MaybeEat(TokenKind.Colon)) { // dict literal
2596+
if (setMembers != null) {
25932597
ReportSyntaxError("invalid syntax");
2598+
} else if (dictMembers == null) {
2599+
dictMembers = new List<SliceExpression>();
2600+
first = true;
25942601
}
2595-
return FinishDictComp(e1, e2, oStart, oEnd);
2596-
}
2602+
Expression e2 = ParseTest();
25972603

2598-
SliceExpression se = new SliceExpression(e1, e2, null);
2599-
se.SetLoc(_globalParent, e1.StartIndex, e2.EndIndex);
2600-
dictMembers.Add(se);
2601-
} else { // set literal
2602-
if (dictMembers != null) {
2603-
ReportSyntaxError("invalid syntax");
2604-
} else if (setMembers == null) {
2605-
setMembers = new List<Expression>();
2606-
first = true;
2607-
}
2604+
if (PeekToken(Tokens.KeywordForToken)) {
2605+
if (!first) {
2606+
ReportSyntaxError("invalid syntax");
2607+
}
2608+
return FinishDictComp(e1, e2, oStart, oEnd);
2609+
}
26082610

2609-
if (PeekToken(Tokens.KeywordForToken)) {
2610-
if (!first) {
2611+
SliceExpression se = new SliceExpression(e1, e2, null);
2612+
se.SetLoc(_globalParent, e1.StartIndex, e2.EndIndex);
2613+
dictMembers.Add(se);
2614+
} else { // set literal
2615+
if (dictMembers != null) {
26112616
ReportSyntaxError("invalid syntax");
2617+
} else if (setMembers == null) {
2618+
setMembers = new List<Expression>();
2619+
first = true;
26122620
}
2613-
return FinishSetComp(e1, oStart, oEnd);
2614-
}
26152621

2616-
// error recovery
2617-
setMembers?.Add(e1);
2622+
if (PeekToken(Tokens.KeywordForToken)) {
2623+
if (!first) {
2624+
ReportSyntaxError("invalid syntax");
2625+
}
2626+
return FinishSetComp(e1, oStart, oEnd);
2627+
}
2628+
2629+
// error recovery
2630+
setMembers?.Add(e1);
2631+
}
26182632
}
26192633

26202634
if (!MaybeEat(TokenKind.Comma)) {

Src/IronPython/Runtime/Operations/PythonOps.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -926,7 +926,7 @@ internal static bool TryInvokeLengthHint(CodeContext context, object? sequence,
926926
return PythonCalls.Call(func, allArgs.GetObjectArray());
927927
}
928928

929-
public static object GetIndex(CodeContext/*!*/ context, object? o, object index) {
929+
public static object GetIndex(CodeContext/*!*/ context, object? o, object? index) {
930930
PythonContext pc = context.LanguageContext;
931931
return pc.GetIndexSite.Target(pc.GetIndexSite, o, index);
932932
}
@@ -1486,6 +1486,26 @@ public static PythonTuple MakeTupleFromSequence(object items) {
14861486
return PythonTuple.Make(items);
14871487
}
14881488

1489+
/// <summary>
1490+
/// DICT_UPDATE
1491+
/// </summary>
1492+
[EditorBrowsable(EditorBrowsableState.Never)]
1493+
public static void DictUpdate(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+
PythonDictionary res = new PythonDictionary();
1500+
1501+
// enumerate the keys getting their values
1502+
IEnumerator enumerator = GetEnumerator(keys);
1503+
while (enumerator.MoveNext()) {
1504+
object? o = enumerator.Current;
1505+
dict[o] = PythonOps.GetIndex(context, item, o);
1506+
}
1507+
}
1508+
14891509
/// <summary>
14901510
/// Python Runtime Helper for enumerator unpacking (tuple assignments, ...)
14911511
/// Creates enumerator from the input parameter e, and then extracts

Tests/test_unpack.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,14 @@ def test_ipy3_gh841(self):
194194
self.assertEqual(a, 1)
195195
self.assertEqual(b, [2])
196196

197+
def test_unpack_dict(self):
198+
self.assertEqual({'x': 1, **{'y': 2}}, {'x': 1, 'y': 2})
199+
self.assertEqual({'x': 1, **{'x': 2}}, {'x': 2})
200+
self.assertEqual({**{'x': 2}, 'x': 1}, {'x': 1})
201+
202+
with self.assertRaises(TypeError):
203+
{**[]}
204+
197205
def test_main():
198206
test.support.run_unittest(UnpackTest)
199207

0 commit comments

Comments
 (0)