diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da316df0b..4869e7e8eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/jscomp/syntax/src/res_comments_table.ml b/jscomp/syntax/src/res_comments_table.ml index 3170fec71e..14365c77b3 100644 --- a/jscomp/syntax/src/res_comments_table.ml +++ b/jscomp/syntax/src/res_comments_table.ml @@ -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 = diff --git a/jscomp/syntax/src/res_core.ml b/jscomp/syntax/src/res_core.ml index c2c6702498..1ee182cba7 100644 --- a/jscomp/syntax/src/res_core.ml +++ b/jscomp/syntax/src/res_core.ml @@ -222,6 +222,7 @@ let get_closing_token = function | Lbrace -> Rbrace | Lbracket -> Rbracket | List -> Rbrace + | Dict -> Rbrace | LessThan -> GreaterThan | _ -> assert false @@ -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 @@ -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 @@ -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 @@ -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; diff --git a/jscomp/syntax/src/res_grammar.ml b/jscomp/syntax/src/res_grammar.ml index 375b8cc9ef..7e06e79c2e 100644 --- a/jscomp/syntax/src/res_grammar.ml +++ b/jscomp/syntax/src/res_grammar.ml @@ -59,6 +59,7 @@ type t = | Pattern | AttributePayload | TagNames + | DictRows let to_string = function | OpenDescription -> "an open description" @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/jscomp/syntax/src/res_parsetree_viewer.ml b/jscomp/syntax/src/res_parsetree_viewer.ml index bfef73dc8d..cc415f5fd1 100644 --- a/jscomp/syntax/src/res_parsetree_viewer.ml +++ b/jscomp/syntax/src/res_parsetree_viewer.ml @@ -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 diff --git a/jscomp/syntax/src/res_parsetree_viewer.mli b/jscomp/syntax/src/res_parsetree_viewer.mli index 0cd2053694..eea6c96c5b 100644 --- a/jscomp/syntax/src/res_parsetree_viewer.mli +++ b/jscomp/syntax/src/res_parsetree_viewer.mli @@ -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 diff --git a/jscomp/syntax/src/res_printer.ml b/jscomp/syntax/src/res_printer.ml index 7b673eddac..13bd09f7e1 100644 --- a/jscomp/syntax/src/res_printer.ml +++ b/jscomp/syntax/src/res_printer.ml @@ -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 = @@ -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)] ) @@ -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 @@ -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 @@ -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" diff --git a/jscomp/syntax/src/res_scanner.ml b/jscomp/syntax/src/res_scanner.ml index 0f6ae798b5..2ee1828a35 100644 --- a/jscomp/syntax/src/res_scanner.ml +++ b/jscomp/syntax/src/res_scanner.ml @@ -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 diff --git a/jscomp/syntax/src/res_token.ml b/jscomp/syntax/src/res_token.ml index e9c5075e4e..7410b3ba38 100644 --- a/jscomp/syntax/src/res_token.ml +++ b/jscomp/syntax/src/res_token.ml @@ -89,6 +89,7 @@ type t = | PercentPercent | Comment of Comment.t | List + | Dict | TemplateTail of string * Lexing.position | TemplatePart of string * Lexing.position | Backtick @@ -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 -> "`" @@ -224,6 +226,7 @@ let keyword_table = function | "include" -> Include | "let" -> Let | "list{" -> List + | "dict{" -> Dict | "module" -> Module | "mutable" -> Mutable | "of" -> Of @@ -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 diff --git a/jscomp/syntax/tests/parsing/grammar/expressions/dict.res b/jscomp/syntax/tests/parsing/grammar/expressions/dict.res new file mode 100644 index 0000000000..20aacf134b --- /dev/null +++ b/jscomp/syntax/tests/parsing/grammar/expressions/dict.res @@ -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} diff --git a/jscomp/syntax/tests/parsing/grammar/expressions/expected/dict.res.txt b/jscomp/syntax/tests/parsing/grammar/expressions/expected/dict.res.txt new file mode 100644 index 0000000000..9d512fb802 --- /dev/null +++ b/jscomp/syntax/tests/parsing/grammar/expressions/expected/dict.res.txt @@ -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)|] \ No newline at end of file diff --git a/jscomp/syntax/tests/printer/expr/bsObj.res b/jscomp/syntax/tests/printer/expr/bsObj.res index ee7dc4c461..467d41f3bd 100644 --- a/jscomp/syntax/tests/printer/expr/bsObj.res +++ b/jscomp/syntax/tests/printer/expr/bsObj.res @@ -56,3 +56,41 @@ React.jsx( {"data-foo": (\"data-foo": string)} } ) + +// comments +let x = {/* foo */ "foo": "bar"} +let x = {"foo": /* foo */ "bar"} +let x = {"foo": "bar" /* foo */ } + +let x = { + // foo + "foo": "bar", + // bar + "bar": "baz", + // baz + "baz": baz +} + +let x = { + "foo": "bar", // foo + "bar": "baz", // bar + "baz": baz // baz +} + +let x = { + "foo": /* foo */ "bar", + "bar": /* bar */ "baz", + "baz": /* bar */ baz +} + +let x = { + /* foo */ "foo": "bar", + /* bar */ "bar": "baz", + /* bar */ "baz": baz +} + +let x = { + "foo": "bar" /* foo */, + "bar": "baz" /* bar */, + "baz": baz /* bar */ +} \ No newline at end of file diff --git a/jscomp/syntax/tests/printer/expr/dict.res b/jscomp/syntax/tests/printer/expr/dict.res new file mode 100644 index 0000000000..3ac1fe675a --- /dev/null +++ b/jscomp/syntax/tests/printer/expr/dict.res @@ -0,0 +1,85 @@ +// 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} + +// multiline +let x = dict{ + "foo": "bar", + "bar": "baz", + "baz": baz +} + +let x = Js.Dict.fromArray([("foo", "bar"), ("bar", "baz")]) +let x = Js.Dict.fromArray([("foo", "bar"), ("bar", "baz"), ("baz", baz)]) + +let x = Js.Dict.fromArray([ + ("foo", "bar"), + ("bar", "baz"), + ("baz", baz) +]) + +// comments +let x = dict{/* foo */ "foo": "bar"} +let x = dict{"foo": /* foo */ "bar"} +let x = dict{"foo": "bar" /* foo */ } + +let x = dict{ + // foo + "foo": "bar", + // bar + "bar": "baz", + // baz + "baz": baz +} + +let x = dict{ + "foo": "bar", // foo + "bar": "baz", // bar + "baz": baz // baz +} + +let x = dict{ + "foo": /* foo */ "bar", + "bar": /* bar */ "baz", + "baz": /* bar */ baz +} + +let x = dict{ + /* foo */ "foo": "bar", + /* bar */ "bar": "baz", + /* bar */ "baz": baz +} + +let x = dict{ + "foo": "bar" /* foo */, + "bar": "baz" /* bar */, + "baz": baz /* bar */ +} + +let x = Js.Dict.fromArray([/* foo */ ("foo", "bar"), /* bar */ ("bar", "baz")]) +let x = Js.Dict.fromArray([(/* foo */ "foo", "bar"), (/* bar */"bar", "baz"), (/* baz */ "baz", baz)]) +let x = Js.Dict.fromArray([("foo", /* foo */"bar"), ("bar", /* bar */"baz"), ("baz", /* baz */baz)]) +let x = Js.Dict.fromArray([("foo", "bar" /* foo */), ("bar", "baz" /* bar */), ("baz", baz /* baz */)]) + +let x = Js.Dict.fromArray([ + // foo + ("foo", "bar"), + // bar + ("bar", "baz"), + // baz + ("baz", baz) +]) + +let x = Js.Dict.fromArray([ + ("foo", "bar"), // foo + ("bar", "baz"), // bar + ("baz", baz) // baz +]) \ No newline at end of file diff --git a/jscomp/syntax/tests/printer/expr/expected/bsObj.res.txt b/jscomp/syntax/tests/printer/expr/expected/bsObj.res.txt index c25445b586..083bb52267 100644 --- a/jscomp/syntax/tests/printer/expr/expected/bsObj.res.txt +++ b/jscomp/syntax/tests/printer/expr/expected/bsObj.res.txt @@ -60,3 +60,41 @@ React.jsx( {"data-foo": (\"data-foo": string)} }, ) + +// comments +let x = {/* foo */ "foo": "bar"} +let x = {"foo": /* foo */ "bar"} +let x = {"foo": "bar" /* foo */} + +let x = { + // foo + "foo": "bar", + // bar + "bar": "baz", + // baz + "baz": baz, +} + +let x = { + "foo": "bar", // foo + "bar": "baz", // bar + "baz": baz, // baz +} + +let x = { + "foo": /* foo */ "bar", + "bar": /* bar */ "baz", + "baz": /* bar */ baz, +} + +let x = { + /* foo */ "foo": "bar", + /* bar */ "bar": "baz", + /* bar */ "baz": baz, +} + +let x = { + "foo": "bar" /* foo */, + "bar": "baz" /* bar */, + "baz": baz /* bar */, +} diff --git a/jscomp/syntax/tests/printer/expr/expected/dict.res.txt b/jscomp/syntax/tests/printer/expr/expected/dict.res.txt new file mode 100644 index 0000000000..b6510c396a --- /dev/null +++ b/jscomp/syntax/tests/printer/expr/expected/dict.res.txt @@ -0,0 +1,85 @@ +// 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} + +// multiline +let x = dict{ + "foo": "bar", + "bar": "baz", + "baz": baz, +} + +let x = dict{"foo": "bar", "bar": "baz"} +let x = dict{"foo": "bar", "bar": "baz", "baz": baz} + +let x = dict{ + "foo": "bar", + "bar": "baz", + "baz": baz, +} + +// comments +let x = dict{/* foo */ "foo": "bar"} +let x = dict{"foo": /* foo */ "bar"} +let x = dict{"foo": "bar" /* foo */} + +let x = dict{ + // foo + "foo": "bar", + // bar + "bar": "baz", + // baz + "baz": baz, +} + +let x = dict{ + "foo": "bar", // foo + "bar": "baz", // bar + "baz": baz, // baz +} + +let x = dict{ + "foo": /* foo */ "bar", + "bar": /* bar */ "baz", + "baz": /* bar */ baz, +} + +let x = dict{ + /* foo */ "foo": "bar", + /* bar */ "bar": "baz", + /* bar */ "baz": baz, +} + +let x = dict{ + "foo": "bar" /* foo */, + "bar": "baz" /* bar */, + "baz": baz /* bar */, +} + +let x = dict{/* foo */ "foo": "bar", /* bar */ "bar": "baz"} +let x = dict{/* foo */ "foo": "bar", /* bar */ "bar": "baz", /* baz */ "baz": baz} +let x = dict{"foo": /* foo */ "bar", "bar": /* bar */ "baz", "baz": /* baz */ baz} +let x = dict{"foo": "bar" /* foo */, "bar": "baz" /* bar */, "baz": baz /* baz */} + +let x = dict{ + // foo + "foo": "bar", + // bar + "bar": "baz", + // baz + "baz": baz, +} + +let x = dict{ + "foo": "bar", // foo + "bar": "baz", // bar + "baz": baz, // baz +}