diff --git a/src/main/java/net/starlark/java/eval/Dict.java b/src/main/java/net/starlark/java/eval/Dict.java
index 229b84c2fa5b74..6c047e2555ab9e 100644
--- a/src/main/java/net/starlark/java/eval/Dict.java
+++ b/src/main/java/net/starlark/java/eval/Dict.java
@@ -72,7 +72,7 @@
+ "0 in d, \"a\" in d # (True, False)\n"
+ "[(k, v) for k, v in d.items()] # [(0, \"a\"), (1, 1), (2, \"b\")]\n"
+ "\n"
- + "
There are three ways to construct a dictionary:\n"
+ + "
There are four ways to construct a dictionary:\n"
+ "
\n"
+ "A dictionary expression {k: v, ...}
yields a new dictionary with"
+ " the specified key/value entries, inserted in the order they appear in the"
@@ -90,6 +90,17 @@
+ " a dictionary containing the specified entries, which are inserted in argument"
+ " order, positional arguments before named. As with comprehensions, duplicate keys"
+ " are permitted.\n"
+ + " The union expression x | y
yields a new dictionary by combining two"
+ + " existing dictionaries. If the two dictionaries have a key k
in common,"
+ + " the right hand side dictionary's value of the key (in other words,"
+ + " y[k]
) wins. The |=
variant of the union operator modifies"
+ + " a dictionary in-place. Example: "
+ + ""
+ + "d = {\"foo\": \"FOO\", \"bar\": \"BAR\"} | {\"foo\": \"FOO2\", \"baz\": \"BAZ\"}\n"
+ + "# d == {\"foo\": \"FOO2\", \"bar\": \"BAR\", \"baz\": \"BAZ\"}\n"
+ + "d = {\"a\": 1, \"b\": 2}\n"
+ + "d |= {\"b\": 3, \"c\": 4}\n"
+ + "# d == {\"a\": 1, \"b\": 3, \"c\": 4} "
+ " ")
public class Dict
implements Map,
diff --git a/src/main/java/net/starlark/java/eval/Eval.java b/src/main/java/net/starlark/java/eval/Eval.java
index 12d48013b63954..594afd97f7705a 100644
--- a/src/main/java/net/starlark/java/eval/Eval.java
+++ b/src/main/java/net/starlark/java/eval/Eval.java
@@ -458,6 +458,14 @@ private static Object inplaceBinaryOp(StarlarkThread.Frame fr, TokenKind op, Obj
StarlarkList> list = (StarlarkList) x;
list.extend(y);
return list;
+ } else if (op == TokenKind.PIPE && x instanceof Dict && y instanceof Dict) {
+ // dict |= dict merges the contents of the second dict into the first.
+ @SuppressWarnings("unchecked")
+ Dict xDict = (Dict) x;
+ @SuppressWarnings("unchecked")
+ Dict yDict = (Dict) y;
+ xDict.putEntries(yDict);
+ return xDict;
}
return EvalUtils.binaryOp(op, x, y, fr.thread);
}
diff --git a/src/main/java/net/starlark/java/eval/EvalUtils.java b/src/main/java/net/starlark/java/eval/EvalUtils.java
index e5c3cb20caf093..ea5f8dd22102af 100644
--- a/src/main/java/net/starlark/java/eval/EvalUtils.java
+++ b/src/main/java/net/starlark/java/eval/EvalUtils.java
@@ -127,6 +127,11 @@ static Object binaryOp(TokenKind op, Object x, Object y, StarlarkThread starlark
// int | int
return StarlarkInt.or((StarlarkInt) x, (StarlarkInt) y);
}
+ } else if (x instanceof Dict) {
+ if (y instanceof Dict) {
+ // dict | dict
+ return Dict.builder().putAll((Dict, ?>) x).putAll((Dict, ?>) y).build(mu);
+ }
}
break;
diff --git a/src/test/java/net/starlark/java/eval/testdata/dict.star b/src/test/java/net/starlark/java/eval/testdata/dict.star
index 6a1ae1fe5af367..3c03a1d074eba5 100644
--- a/src/test/java/net/starlark/java/eval/testdata/dict.star
+++ b/src/test/java/net/starlark/java/eval/testdata/dict.star
@@ -75,6 +75,63 @@ d.update((["f", 0],), f = 6)
expected = {"a": 1, "b": 2, "c": 3, "d": 4, "e": 5, "f": 6}
assert_eq(d, expected)
+# dict union operator |
+
+empty_dict = dict()
+dict_with_a_b = dict(a=1, b=[1, 2])
+dict_with_b = dict(b=[1, 2])
+dict_with_other_b = dict(b=[3, 4])
+
+assert_eq(empty_dict | dict_with_a_b, dict_with_a_b)
+# Verify iteration order.
+assert_eq((empty_dict | dict_with_a_b).items(), dict_with_a_b.items())
+assert_eq(dict_with_a_b | empty_dict, dict_with_a_b)
+assert_eq((dict_with_a_b | empty_dict).items(), dict_with_a_b.items())
+assert_eq(dict_with_b | dict_with_a_b, dict_with_a_b)
+assert_eq((dict_with_b | dict_with_a_b).items(), dict(b=[1, 2], a=1).items())
+assert_eq(dict_with_a_b | dict_with_b, dict_with_a_b)
+assert_eq((dict_with_a_b | dict_with_b).items(), dict_with_a_b.items())
+assert_eq(dict_with_b | dict_with_other_b, dict_with_other_b)
+assert_eq((dict_with_b | dict_with_other_b).items(), dict_with_other_b.items())
+assert_eq(dict_with_other_b | dict_with_b, dict_with_b)
+assert_eq((dict_with_other_b | dict_with_b).items(), dict_with_b.items())
+
+assert_eq(empty_dict, dict())
+assert_eq(dict_with_b, dict(b=[1,2]))
+
+assert_fails(lambda: dict() | [], "unsupported binary operation")
+
+# dict union assignment operator |=
+
+def test_dict_union_assignment():
+ empty_dict = dict()
+ empty_dict |= {"a": 1}
+ empty_dict |= {"b": 2}
+ empty_dict |= {"c": "3", 7: 4}
+ empty_dict |= {"b": "5", "e": 6}
+ expected_1 = {"a": 1, "b": "5", "c": "3", 7: 4, "e": 6}
+ assert_eq(empty_dict, expected_1)
+ assert_eq(empty_dict.items(), expected_1.items())
+
+ dict_a = {8: 1, "b": 2}
+ dict_b = {"b": 1, "c": 6}
+ dict_c = {"d": 7}
+ dict_tuple = {(5, "a"): ("c", 8)}
+ dict_a |= dict_b
+ dict_c |= dict_a
+ dict_c |= dict_tuple
+ expected_2 = {"d": 7, 8: 1, "b": 1, "c": 6, (5, "a"): ("c", 8)}
+ assert_eq(dict_c, expected_2)
+ assert_eq(dict_c.items(), expected_2.items())
+ assert_eq(dict_b, {"b": 1, "c": 6})
+
+test_dict_union_assignment()
+
+def dict_union_assignment_type_mismatch():
+ some_dict = dict()
+ some_dict |= []
+
+assert_fails(dict_union_assignment_type_mismatch, "unsupported binary operation")
# creation with repeated keys