Skip to content

Commit d9df131

Browse files
authored
Merge pull request #4449 from harupy/fix-dict-spread-in-dict
Fix AST generated from a dict literal containing dict unpacking
2 parents 6dba843 + f2ffe12 commit d9df131

6 files changed

Lines changed: 146 additions & 43 deletions

File tree

‎ast/asdl_rs.py‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,20 +227,25 @@ def visitConstructor(self, cons, parent, depth):
227227
if cons.fields:
228228
self.emit(f"{cons.name} {{", depth)
229229
for f in cons.fields:
230-
self.visit(f, parent, "", depth + 1)
230+
self.visit(f, parent, "", depth + 1, cons.name)
231231
self.emit("},", depth)
232232
else:
233233
self.emit(f"{cons.name},", depth)
234234

235-
def visitField(self, field, parent, vis, depth):
235+
def visitField(self, field, parent, vis, depth, constructor=None):
236236
typ = get_rust_type(field.type)
237237
fieldtype = self.typeinfo.get(field.type)
238238
if fieldtype and fieldtype.has_userdata:
239239
typ = f"{typ}<U>"
240240
# don't box if we're doing Vec<T>, but do box if we're doing Vec<Option<Box<T>>>
241241
if fieldtype and fieldtype.boxed and (not (parent.product or field.seq) or field.opt):
242242
typ = f"Box<{typ}>"
243-
if field.opt:
243+
if field.opt or (
244+
# When a dictionary literal contains dictionary unpacking (e.g., `{**d}`),
245+
# the expression to be unpacked goes in `values` with a `None` at the corresponding
246+
# position in `keys`. To handle this, the type of `keys` needs to be `Option<Vec<T>>`.
247+
constructor == "Dict" and field.name == "keys"
248+
):
244249
typ = f"Option<{typ}>"
245250
if field.seq:
246251
typ = f"Vec<{typ}>"

‎ast/src/ast_gen.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ pub enum ExprKind<U = ()> {
195195
orelse: Box<Expr<U>>,
196196
},
197197
Dict {
198-
keys: Vec<Expr<U>>,
198+
keys: Vec<Option<Expr<U>>>,
199199
values: Vec<Expr<U>>,
200200
},
201201
Set {

‎ast/src/unparse.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,11 @@ impl<'a> Unparser<'a> {
152152
let (packed, unpacked) = values.split_at(keys.len());
153153
for (k, v) in keys.iter().zip(packed) {
154154
self.p_delim(&mut first, ", ")?;
155-
write!(self, "{}: {}", *k, *v)?;
155+
if let Some(k) = k {
156+
write!(self, "{}: {}", *k, *v)?;
157+
} else {
158+
write!(self, "**{}", *v)?;
159+
}
156160
}
157161
for d in unpacked {
158162
self.p_delim(&mut first, ", ")?;

‎parser/python.lalrpop‎

Lines changed: 5 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,44 +1136,11 @@ Atom<Goal>: ast::Expr = {
11361136
}.into())
11371137
},
11381138
<location:@L> "{" <e:DictLiteralValues?> "}" <end_location:@R> => {
1139-
let pairs = e.unwrap_or_default();
1140-
1141-
let (keys, values) = match pairs.iter().position(|(k,_)| k.is_none()) {
1142-
Some(unpack_idx) => {
1143-
let mut pairs = pairs;
1144-
let (keys, mut values): (_, Vec<_>) = pairs.drain(..unpack_idx).map(|(k, v)| (*k.unwrap(), v)).unzip();
1145-
1146-
fn build_map(items: &mut Vec<(ast::Expr, ast::Expr)>) -> ast::Expr {
1147-
let location = items[0].0.location;
1148-
let end_location = items[0].0.end_location;
1149-
let (keys, values) = items.drain(..).unzip();
1150-
ast::Expr {
1151-
location,
1152-
end_location,
1153-
custom: (),
1154-
node: ast::ExprKind::Dict { keys, values }
1155-
}
1156-
}
1157-
1158-
let mut items = Vec::new();
1159-
for (key, value) in pairs.into_iter() {
1160-
if let Some(key) = key {
1161-
items.push((*key, value));
1162-
continue;
1163-
}
1164-
if !items.is_empty() {
1165-
values.push(build_map(&mut items));
1166-
}
1167-
values.push(value);
1168-
}
1169-
if !items.is_empty() {
1170-
values.push(build_map(&mut items));
1171-
}
1172-
(keys, values)
1173-
},
1174-
None => pairs.into_iter().map(|(k, v)| (*k.unwrap(), v)).unzip()
1175-
};
1176-
1139+
let (keys, values) = e
1140+
.unwrap_or_default()
1141+
.into_iter()
1142+
.map(|(k, v)| (k.map(|x| *x), v))
1143+
.unzip();
11771144
ast::Expr {
11781145
location,
11791146
end_location: Some(end_location),

‎parser/src/parser.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,4 +309,10 @@ with (0 as a, 1 as b,): pass
309309
assert!(parse_program(source, "<test>").is_err());
310310
}
311311
}
312+
313+
#[test]
314+
fn test_dict_unpacking() {
315+
let parse_ast = parse_expression(r#"{"a": "b", **c, "d": "e"}"#, "<test>").unwrap();
316+
insta::assert_debug_snapshot!(parse_ast);
317+
}
312318
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
---
2+
source: compiler/parser/src/parser.rs
3+
expression: parse_ast
4+
---
5+
Located {
6+
location: Location {
7+
row: 1,
8+
column: 0,
9+
},
10+
end_location: Some(
11+
Location {
12+
row: 1,
13+
column: 25,
14+
},
15+
),
16+
custom: (),
17+
node: Dict {
18+
keys: [
19+
Some(
20+
Located {
21+
location: Location {
22+
row: 1,
23+
column: 1,
24+
},
25+
end_location: Some(
26+
Location {
27+
row: 1,
28+
column: 4,
29+
},
30+
),
31+
custom: (),
32+
node: Constant {
33+
value: Str(
34+
"a",
35+
),
36+
kind: None,
37+
},
38+
},
39+
),
40+
None,
41+
Some(
42+
Located {
43+
location: Location {
44+
row: 1,
45+
column: 16,
46+
},
47+
end_location: Some(
48+
Location {
49+
row: 1,
50+
column: 19,
51+
},
52+
),
53+
custom: (),
54+
node: Constant {
55+
value: Str(
56+
"d",
57+
),
58+
kind: None,
59+
},
60+
},
61+
),
62+
],
63+
values: [
64+
Located {
65+
location: Location {
66+
row: 1,
67+
column: 6,
68+
},
69+
end_location: Some(
70+
Location {
71+
row: 1,
72+
column: 9,
73+
},
74+
),
75+
custom: (),
76+
node: Constant {
77+
value: Str(
78+
"b",
79+
),
80+
kind: None,
81+
},
82+
},
83+
Located {
84+
location: Location {
85+
row: 1,
86+
column: 13,
87+
},
88+
end_location: Some(
89+
Location {
90+
row: 1,
91+
column: 14,
92+
},
93+
),
94+
custom: (),
95+
node: Name {
96+
id: "c",
97+
ctx: Load,
98+
},
99+
},
100+
Located {
101+
location: Location {
102+
row: 1,
103+
column: 21,
104+
},
105+
end_location: Some(
106+
Location {
107+
row: 1,
108+
column: 24,
109+
},
110+
),
111+
custom: (),
112+
node: Constant {
113+
value: Str(
114+
"e",
115+
),
116+
kind: None,
117+
},
118+
},
119+
],
120+
},
121+
}

0 commit comments

Comments
 (0)