-
Notifications
You must be signed in to change notification settings - Fork 469
Pattern matching for dicts #7059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
35d0e09
ed30f68
5b93ca1
011b3bb
a4e809a
1a079a4
9c4c091
09f07f2
35a6dad
a2d09fa
f783d20
f134c62
b1ebde4
6510847
4073eb0
768814f
828b01c
821bd2d
ae1ff05
c558771
f91c04c
00dd136
db29437
d4df2f8
01132d3
849d950
c082baa
9fa2193
81b11ec
02c12f1
3d42ed0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
|
||
[1;31mWe've found a bug for you![0m | ||
[36m/.../fixtures/dict_coercion.res[0m:[2m7:10-30[0m | ||
|
||
5 [2m│[0m type fakeDict<'t> = {dictValuesType?: 't} | ||
6 [2m│[0m | ||
[1;31m7[0m [2m│[0m let d = ([1;31mdict :> fakeDict<int>[0m) | ||
8 [2m│[0m | ||
|
||
Type Js.Dict.t<int> = dict<int> is not a subtype of fakeDict<int> | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
|
||
[1;31mWe've found a bug for you![0m | ||
[36m/.../fixtures/dict_magic_field_on_non_dict.res[0m:[2m5:6-23[0m | ||
|
||
3 [2m│[0m let foo = (fakeDict: fakeDict<'a>) => { | ||
4 [2m│[0m switch fakeDict { | ||
[1;31m5[0m [2m│[0m | {[1;31msomeUndefinedField[0m: 1} => Js.log("one") | ||
6 [2m│[0m | _ => Js.log("not one") | ||
7 [2m│[0m } | ||
|
||
The field [1;31msomeUndefinedField[0m does not belong to type [1;33mfakeDict[0m | ||
|
||
This record pattern is expected to have type [1;33mfakeDict<'a>[0m |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
|
||
[1;31mWe've found a bug for you![0m | ||
[36m/.../fixtures/dict_pattern_inference.res[0m:[2m3:27-33[0m | ||
|
||
1 [2m│[0m let foo = dict => | ||
2 [2m│[0m switch dict { | ||
[1;31m3[0m [2m│[0m | dict{"one": 1, "two": [1;31m"hello"[0m} => Js.log("one") | ||
4 [2m│[0m | _ => Js.log("not one") | ||
5 [2m│[0m } | ||
|
||
This pattern matches values of type [1;31mstring[0m | ||
but a pattern was expected which matches values of type [1;33mint[0m |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
|
||
[1;31mWe've found a bug for you![0m | ||
[36m/.../fixtures/dict_pattern_inference_constrained.res[0m:[2m4:27-30[0m | ||
|
||
2 [2m┆[0m switch dict { | ||
3 [2m┆[0m | dict{"one": 1} => | ||
[1;31m4[0m [2m┆[0m let _: dict<string> = [1;31mdict[0m | ||
5 [2m┆[0m Js.log("one") | ||
6 [2m┆[0m | _ => Js.log("not one") | ||
|
||
This has type: [1;31mdict<int>[0m | ||
But it's expected to have type: [1;33mdict<string>[0m | ||
|
||
The incompatible parts: | ||
[1;31mint[0m vs [1;33mstring[0m | ||
|
||
You can convert [1;33mint[0m to [1;33mstring[0m with [1;33mBelt.Int.toString[0m. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
|
||
[1;31mWe've found a bug for you![0m | ||
[36m/.../fixtures/dict_pattern_regular_record.res[0m:[2m5:5-22[0m | ||
|
||
3 [2m│[0m let constrainedAsDict = (dict: x) => | ||
4 [2m│[0m switch dict { | ||
[1;31m5[0m [2m│[0m | [1;31mdict{"one": "one"}[0m => Js.log("one") | ||
6 [2m│[0m | _ => Js.log("not one") | ||
7 [2m│[0m } | ||
|
||
This pattern matches values of type [1;31mdict<string>[0m | ||
but a pattern was expected which matches values of type [1;33mx[0m |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
|
||
[1;31mWe've found a bug for you![0m | ||
[36m/.../fixtures/dict_record_style_field_access.res[0m:[2m5:20-23[0m | ||
|
||
3 [2m│[0m } | ||
4 [2m│[0m | ||
[1;31m5[0m [2m│[0m let x = stringDict.[1;31mname[0m | ||
|
||
Direct field access on a dict is not supported. Use Dict.get instead. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
let dict = Js.Dict.empty() | ||
dict->Js.Dict.set("someKey1", 1) | ||
dict->Js.Dict.set("someKey2", 2) | ||
|
||
type fakeDict<'t> = {dictValuesType?: 't} | ||
|
||
let d = (dict :> fakeDict<int>) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
type fakeDict<'t> = {dictValuesType?: 't} | ||
|
||
let foo = (fakeDict: fakeDict<'a>) => { | ||
switch fakeDict { | ||
| {someUndefinedField: 1} => Js.log("one") | ||
| _ => Js.log("not one") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
let foo = dict => | ||
switch dict { | ||
| dict{"one": 1, "two": "hello"} => Js.log("one") | ||
| _ => Js.log("not one") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
let foo = dict => | ||
switch dict { | ||
| dict{"one": 1} => | ||
let _: dict<string> = dict | ||
Js.log("one") | ||
| _ => Js.log("not one") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
type x = {one: int} | ||
|
||
let constrainedAsDict = (dict: x) => | ||
switch dict { | ||
| dict{"one": "one"} => Js.log("one") | ||
| _ => Js.log("not one") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
let stringDict = dict{ | ||
"name": "hello", | ||
} | ||
|
||
let x = stringDict.name |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
(* | ||
An overview of the implementation of dicts in ReScript: | ||
### What is a dict? | ||
Dicts are effectively an object with unknown fields, but a single known type of the values it holds. | ||
|
||
### How are they implemented? | ||
Dicts in ReScript are implemented as predefined record type, with a single (magic) field that holds | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps somewhere: say explicitly that it represents every possible key in the dict |
||
the type of the dict's values. This field is called `dictValuesType`, and it represent every possible | ||
key in the dict. It's just an implementation detail - it's never actually exposed to the user, just | ||
used internally. | ||
|
||
The compiler will route any label lookup on the dict record type to the magic field, which creates a | ||
record with unknown keys, but of a single type. | ||
|
||
The reason for this seemingly convoluted implementation is that it allows us to piggyback on the | ||
existing record pattern matching mechanism, which means we get pattern matching on dicts for free. | ||
|
||
### Modifications to the type checker | ||
We've made a few smaller modifications to the type checker to support this implementation: | ||
|
||
- We've added a new predefined type `dict` that is a record with a single field called `dictValuesType`. | ||
This type is used to represent the type of the values in a dict. | ||
- We've modified the type checker to recognize `dict` patterns, and route them to the predefined `dict` type. | ||
This allows us to get full inference for dicts in patterns. | ||
|
||
### Syntax | ||
There's first class syntax support for dicts, both as expressions and as patterns. | ||
A dict pattern is treated as a record pattern in the compiler and syntax, with an attriubute `@res.dictPattern` | ||
attached to it. This attribute is used to tell the compiler that the pattern is a dict pattern, and is what | ||
triggers the compiler to treat the dict record type differently to regular record types. | ||
*) | ||
let dict_magic_field_name = "dictValuesType" | ||
|
||
let has_dict_pattern_attribute attrs = | ||
attrs | ||
|> List.find_opt (fun (({txt}, _) : Parsetree.attribute) -> | ||
txt = "res.dictPattern") | ||
|> Option.is_some | ||
|
||
let has_dict_attribute attrs = | ||
attrs | ||
|> List.find_opt (fun (({txt}, _) : Parsetree.attribute) -> txt = "res.$dict") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why are we using 2 distinct annotations for expressions and patterns? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should do a separate pass that goes through attributes in general. There's a lot of definition and utils duplication between syntax and the compiler. |
||
|> Option.is_some | ||
|
||
let dict_attr : Parsetree.attribute = | ||
(Location.mknoloc "res.$dict", Parsetree.PStr []) | ||
|
||
let dict_magic_field_attr : Parsetree.attribute = | ||
(Location.mknoloc "res.$dictMagicField", Parsetree.PStr []) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why not? Should the message say a bit more?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely. But that's a separate, and large, task.