Skip to content

Commit

Permalink
Add dict literal syntax (#6774)
Browse files Browse the repository at this point in the history
* Handle dict literal syntax

Closes #6545

* Update test

* Move isTupleArray to res_parsetree_viewer

* Map locations correctly

* Fix location in parsing comments printing

* Make Dict limit to List grammar-wise

* Add changelog entry
  • Loading branch information
bloodyowl authored Aug 30, 2024
1 parent d89b3c8 commit 6838749
Show file tree
Hide file tree
Showing 15 changed files with 425 additions and 11 deletions.
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
(Ast_helper.Exp.ident ~loc
(Location.mkloc
(Longident.Ldot
(Longident.Ldot (Longident.Lident "Js", "Dict"), "fromArray"))
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

0 comments on commit 6838749

Please sign in to comment.