Skip to content
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

Add dict literal syntax #6774

Merged
merged 7 commits into from
Aug 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
#### :rocket: New Feature

- Allow coercing polyvariants to variants when we can guarantee that the runtime representation matches. https://github.com/rescript-lang/rescript-compiler/pull/6981
- Add new dict literal syntax (`dict{"foo": "bar"}`). https://github.com/rescript-lang/rescript-compiler/pull/6774

#### :nail_care: Polish

Expand Down
13 changes: 13 additions & 0 deletions jscomp/syntax/src/res_comments_table.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,19 @@ and walk_expression expr t comments =
walk_list
[Expression parent_expr; Expression member_expr; Expression target_expr]
t comments
| Pexp_apply
( {
pexp_desc =
Pexp_ident
{
txt =
Longident.Ldot
(Longident.Ldot (Lident "Js", "Dict"), "fromArray");
};
},
[(Nolabel, key_values)] )
when Res_parsetree_viewer.is_tuple_array key_values ->
walk_list [Expression key_values] t comments
| Pexp_apply (call_expr, arguments) ->
let before, inside, after = partition_by_loc comments call_expr.pexp_loc in
let after =
Expand Down
50 changes: 49 additions & 1 deletion jscomp/syntax/src/res_core.ml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ let get_closing_token = function
| Lbrace -> Rbrace
| Lbracket -> Rbracket
| List -> Rbrace
| Dict -> Rbrace
| LessThan -> GreaterThan
| _ -> assert false

Expand All @@ -233,7 +234,7 @@ let rec go_to_closing closing_token state =
| GreaterThan, GreaterThan ->
Parser.next state;
()
| ((Token.Lbracket | Lparen | Lbrace | List | LessThan) as t), _ ->
| ((Token.Lbracket | Lparen | Lbrace | List | Dict | LessThan) as t), _ ->
Parser.next state;
go_to_closing (get_closing_token t) state;
go_to_closing closing_token state
Expand Down Expand Up @@ -1896,6 +1897,9 @@ and parse_atomic_expr p =
| List ->
Parser.next p;
parse_list_expr ~start_pos p
| Dict ->
Parser.next p;
parse_dict_expr ~start_pos p
| Module ->
Parser.next p;
parse_first_class_module_expr ~start_pos p
Expand Down Expand Up @@ -3122,6 +3126,20 @@ and parse_record_expr_row p =
| _ -> None)
| _ -> None

and parse_dict_expr_row p =
match p.Parser.token with
| String s -> (
let loc = mk_loc p.start_pos p.end_pos in
Parser.next p;
let field = Location.mkloc (Longident.Lident s) loc in
match p.Parser.token with
| Colon ->
Parser.next p;
let fieldExpr = parse_expr p in
Some (field, fieldExpr)
| _ -> Some (field, Ast_helper.Exp.ident ~loc:field.loc field))
| _ -> None

and parse_record_expr_with_string_keys ~start_pos first_row p =
let rows =
first_row
Expand Down Expand Up @@ -3903,6 +3921,36 @@ and parse_list_expr ~start_pos p =
loc))
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc list_exprs)]

and parse_dict_expr ~start_pos p =
let rows =
parse_comma_delimited_region ~grammar:Grammar.DictRows ~closing:Rbrace
~f:parse_dict_expr_row p
in
let loc = mk_loc start_pos p.end_pos in
let to_key_value_pair
(record_item : Longident.t Location.loc * Parsetree.expression) =
match record_item with
| ( {Location.txt = Longident.Lident key; loc = keyLoc},
({pexp_loc = value_loc} as value_expr) ) ->
Some
(Ast_helper.Exp.tuple
~loc:(mk_loc keyLoc.loc_start value_loc.loc_end)
[
Ast_helper.Exp.constant ~loc:keyLoc (Pconst_string (key, None));
value_expr;
])
| _ -> None
in
let key_value_pairs = List.filter_map to_key_value_pair rows in
Parser.expect Rbrace p;
Ast_helper.Exp.apply ~loc
IwanKaramazow marked this conversation as resolved.
Show resolved Hide resolved
(Ast_helper.Exp.ident ~loc
(Location.mkloc
(Longident.Ldot
(Longident.Ldot (Longident.Lident "Js", "Dict"), "fromArray"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related #6836

@cknitt I think this needs to be adapted, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but this can be done independently later.

loc))
[(Asttypes.Nolabel, Ast_helper.Exp.array ~loc key_value_pairs)]

and parse_array_exp p =
let start_pos = p.Parser.start_pos in
Parser.expect Lbracket p;
Expand Down
13 changes: 10 additions & 3 deletions jscomp/syntax/src/res_grammar.ml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type t =
| Pattern
| AttributePayload
| TagNames
| DictRows

let to_string = function
| OpenDescription -> "an open description"
Expand Down Expand Up @@ -120,6 +121,7 @@ let to_string = function
| ExprFor -> "a for expression"
| AttributePayload -> "an attribute payload"
| TagNames -> "tag names"
| DictRows -> "rows of a dict"

let is_signature_item_start = function
| Token.At | Let | Typ | External | Exception | Open | Include | Module | AtAt
Expand All @@ -136,7 +138,7 @@ let is_atomic_pattern_start = function
let is_atomic_expr_start = function
| Token.True | False | Int _ | String _ | Float _ | Codepoint _ | Backtick
| Uident _ | Lident _ | Hash | Lparen | List | Lbracket | Lbrace | LessThan
| Module | Percent | Forwardslash | ForwardslashDot ->
| Module | Percent | Forwardslash | ForwardslashDot | Dict ->
true
| _ -> false

Expand All @@ -151,7 +153,7 @@ let is_expr_start = function
| For | Hash | If | Int _ | Lbrace | Lbracket | LessThan | Lident _ | List
| Lparen | Minus | MinusDot | Module | Percent | Plus | PlusDot | String _
| Switch | True | Try | Uident _ | Underscore (* _ => doThings() *)
| While | Forwardslash | ForwardslashDot ->
| While | Forwardslash | ForwardslashDot | Dict ->
true
| _ -> false

Expand Down Expand Up @@ -219,6 +221,10 @@ let is_mod_expr_start = function
true
| _ -> false

let is_dict_row_start = function
| Token.String _ -> true
| _ -> false

let is_record_row_start = function
| Token.DotDotDot -> true
| Token.Uident _ | Lident _ -> true
Expand Down Expand Up @@ -260,7 +266,7 @@ let is_block_expr_start = function
| False | Float _ | For | Forwardslash | ForwardslashDot | Hash | If | Int _
| Lbrace | Lbracket | LessThan | Let | Lident _ | List | Lparen | Minus
| MinusDot | Module | Open | Percent | Plus | PlusDot | String _ | Switch
| True | Try | Uident _ | Underscore | While ->
| True | Try | Uident _ | Underscore | While | Dict ->
true
| _ -> false

Expand All @@ -278,6 +284,7 @@ let is_list_element grammar token =
| FunctorArgs -> is_functor_arg_start token
| ModExprList -> is_mod_expr_start token
| TypeParameters -> is_type_parameter_start token
| DictRows -> is_dict_row_start token
| RecordRows -> is_record_row_start token
| RecordRowsStringKey -> is_record_row_string_key_start token
| ArgumentList -> is_argument_start token
Expand Down
10 changes: 10 additions & 0 deletions jscomp/syntax/src/res_parsetree_viewer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -743,3 +743,13 @@ let is_rewritten_underscore_apply_sugar expr =
match expr.pexp_desc with
| Pexp_ident {txt = Longident.Lident "_"} -> true
| _ -> false

let is_tuple_array (expr : Parsetree.expression) =
let is_plain_tuple (expr : Parsetree.expression) =
match expr with
| {pexp_desc = Pexp_tuple _} -> true
| _ -> false
in
match expr with
| {pexp_desc = Pexp_array items} -> List.for_all is_plain_tuple items
| _ -> false
2 changes: 2 additions & 0 deletions jscomp/syntax/src/res_parsetree_viewer.mli
Original file line number Diff line number Diff line change
Expand Up @@ -164,3 +164,5 @@ val has_if_let_attribute : Parsetree.attributes -> bool
val is_rewritten_underscore_apply_sugar : Parsetree.expression -> bool

val is_fun_newtype : Parsetree.expression -> bool

val is_tuple_array : Parsetree.expression -> bool
67 changes: 64 additions & 3 deletions jscomp/syntax/src/res_printer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -1406,6 +1406,49 @@ and print_record_declaration ~state (lds : Parsetree.label_declaration list)
Doc.rbrace;
])

and print_literal_dict_expr ~state (e : Parsetree.expression) cmt_tbl =
let force_break =
e.pexp_loc.loc_start.pos_lnum < e.pexp_loc.loc_end.pos_lnum
in
let tuple_to_row (e : Parsetree.expression) =
match e with
| {
pexp_desc =
Pexp_tuple
[
{pexp_desc = Pexp_constant (Pconst_string (name, _)); pexp_loc}; value;
];
} ->
Some ((Location.mkloc (Longident.Lident name) pexp_loc, value), e)
| _ -> None
in
let rows =
match e with
| {pexp_desc = Pexp_array expressions} ->
List.filter_map tuple_to_row expressions
| _ -> []
in
Doc.breakable_group ~force_break
(Doc.concat
[
Doc.indent
(Doc.concat
[
Doc.soft_line;
Doc.join
~sep:(Doc.concat [Doc.text ","; Doc.line])
(List.map
(fun ((row, e) :
(Longident.t Location.loc * Parsetree.expression)
* Parsetree.expression) ->
let doc = print_bs_object_row ~state row cmt_tbl in
print_comments doc cmt_tbl e.pexp_loc)
rows);
]);
Doc.trailing_comma;
Doc.soft_line;
])

and print_constructor_declarations ~state ~private_flag
(cds : Parsetree.constructor_declaration list) cmt_tbl =
let force_break =
Expand Down Expand Up @@ -4031,6 +4074,24 @@ and print_pexp_apply ~state expr cmt_tbl =
| [] -> doc
| attrs ->
Doc.group (Doc.concat [print_attributes ~state attrs cmt_tbl; doc]))
| Pexp_apply
( {
pexp_desc =
Pexp_ident
{
txt =
Longident.Ldot
(Longident.Ldot (Lident "Js", "Dict"), "fromArray");
};
},
[(Nolabel, key_values)] )
when Res_parsetree_viewer.is_tuple_array key_values ->
Doc.concat
[
Doc.text "dict{";
print_literal_dict_expr ~state key_values cmt_tbl;
Doc.rbrace;
]
| Pexp_apply
( {pexp_desc = Pexp_ident {txt = Longident.Ldot (Lident "Array", "get")}},
[(Nolabel, parent_expr); (Nolabel, member_expr)] )
Expand Down Expand Up @@ -4541,7 +4602,7 @@ and print_jsx_name {txt = lident} =
Doc.join ~sep:Doc.dot segments

and print_arguments_with_callback_in_first_position ~state args cmt_tbl =
(* Because the same subtree gets printed twice, we need to copy the cmtTbl.
(* Because the same subtree gets printed twice, we need to copy the cmt_tbl.
* consumed comments need to be marked not-consumed and reprinted…
* Cheng's different comment algorithm will solve this. *)
let state = State.next_custom_layout state in
Expand Down Expand Up @@ -4624,7 +4685,7 @@ and print_arguments_with_callback_in_first_position ~state args cmt_tbl =
Doc.custom_layout [Lazy.force fits_on_one_line; Lazy.force break_all_args]

and print_arguments_with_callback_in_last_position ~state args cmt_tbl =
(* Because the same subtree gets printed twice, we need to copy the cmtTbl.
(* Because the same subtree gets printed twice, we need to copy the cmt_tbl.
* consumed comments need to be marked not-consumed and reprinted…
* Cheng's different comment algorithm will solve this. *)
let state = state |> State.next_custom_layout in
Expand Down Expand Up @@ -5822,7 +5883,7 @@ let print_pattern p = print_pattern ~state:(State.init ()) p
let print_implementation ~width (s : Parsetree.structure) ~comments =
let cmt_tbl = CommentTable.make () in
CommentTable.walk_structure s cmt_tbl comments;
(* CommentTable.log cmtTbl; *)
(* CommentTable.log cmt_tbl; *)
let doc = print_structure ~state:(State.init ()) s cmt_tbl in
(* Doc.debug doc; *)
Doc.to_string ~width doc ^ "\n"
Expand Down
11 changes: 8 additions & 3 deletions jscomp/syntax/src/res_scanner.ml
Original file line number Diff line number Diff line change
Expand Up @@ -196,11 +196,16 @@ let scan_identifier scanner =
(String.sub [@doesNotRaise]) scanner.src start_off
(scanner.offset - start_off)
in
if '{' == scanner.ch && str = "list" then (
match (scanner, str) with
| {ch = '{'}, "list" ->
next scanner;
(* TODO: this isn't great *)
Token.lookup_keyword "list{")
else Token.lookup_keyword str
Token.lookup_keyword "list{"
| {ch = '{'}, "dict" ->
next scanner;
(* TODO: this isn't great *)
Token.lookup_keyword "dict{"
| _ -> Token.lookup_keyword str

let scan_digits scanner ~base =
if base <= 10 then
Expand Down
5 changes: 4 additions & 1 deletion jscomp/syntax/src/res_token.ml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ type t =
| PercentPercent
| Comment of Comment.t
| List
| Dict
| TemplateTail of string * Lexing.position
| TemplatePart of string * Lexing.position
| Backtick
Expand Down Expand Up @@ -200,6 +201,7 @@ let to_string = function
| PercentPercent -> "%%"
| Comment c -> "Comment" ^ Comment.to_string c
| List -> "list{"
| Dict -> "dict{"
| TemplatePart (text, _) -> text ^ "${"
| TemplateTail (text, _) -> "TemplateTail(" ^ text ^ ")"
| Backtick -> "`"
Expand All @@ -224,6 +226,7 @@ let keyword_table = function
| "include" -> Include
| "let" -> Let
| "list{" -> List
| "dict{" -> Dict
| "module" -> Module
| "mutable" -> Mutable
| "of" -> Of
Expand All @@ -242,7 +245,7 @@ let keyword_table = function
let is_keyword = function
| Await | And | As | Assert | Constraint | Else | Exception | External | False
| For | If | In | Include | Land | Let | List | Lor | Module | Mutable | Of
| Open | Private | Rec | Switch | True | Try | Typ | When | While ->
| Open | Private | Rec | Switch | True | Try | Typ | When | While | Dict ->
true
| _ -> false

Expand Down
11 changes: 11 additions & 0 deletions jscomp/syntax/tests/parsing/grammar/expressions/dict.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// empty dict
let x = dict{}

// one value
let x = dict{"foo": "bar"}

// two values
let x = dict{"foo": "bar", "bar": "baz"}

let baz = "foo"
let x = dict{"foo": "bar", "bar": "baz", "baz": baz}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let x = Js.Dict.fromArray [||]
let x = Js.Dict.fromArray [|("foo", {js|bar|js})|]
let x = Js.Dict.fromArray [|("foo", {js|bar|js});("bar", {js|baz|js})|]
let baz = {js|foo|js}
let x =
Js.Dict.fromArray
[|("foo", {js|bar|js});("bar", {js|baz|js});("baz", baz)|]
Loading