From 7e75974e9aa09d00289a5287b71f85436fc7af9e Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 18 Mar 2026 11:20:38 +0100 Subject: [PATCH 1/3] support inline records in external definitions --- compiler/syntax/src/res_core.ml | 694 ++++++++++++------ compiler/syntax/src/res_printer.ml | 285 +++++-- .../expected/externalInlineRecord.res.txt | 11 + .../externalInlineRecordAnonymous.res.txt | 18 + .../externalInlineRecordCollision.res.txt | 4 + .../expected/inlineRecordCollision.res.txt | 5 + .../structure/externalInlineRecord.res | 4 + .../externalInlineRecordAnonymous.res | 4 + .../externalInlineRecordCollision.res | 2 + .../structure/inlineRecordCollision.res | 2 + .../expected/externalInlineRecord.resi.txt | 11 + .../externalInlineRecordAnonymous.resi.txt | 4 + .../externalInlineRecordBareArrow.resi.txt | 1 + .../expected/externalObjectSpread.resi.txt | 3 + .../signature/externalInlineRecord.resi | 9 + .../externalInlineRecordAnonymous.resi | 4 + .../externalInlineRecordBareArrow.resi | 2 + .../signature/externalObjectSpread.resi | 4 + .../expected/externalInlineRecord.res.txt | 11 + .../expected/externalInlineRecordAll.res.txt | 23 + .../externalInlineRecordAnonymous.res.txt | 7 + .../externalInlineRecordBareArrow.res.txt | 1 + .../externalInlineRecordComments.res.txt | 3 + .../expected/externalObjectSpread.res.txt | 3 + .../structure/externalInlineRecord.res | 9 + .../structure/externalInlineRecordAll.res | 21 + .../externalInlineRecordAnonymous.res | 7 + .../externalInlineRecordBareArrow.res | 1 + .../externalInlineRecordComments.res | 3 + .../structure/externalObjectSpread.res | 4 + .../expected/inlineRecordCollision.res.txt | 2 + .../printer/typeDef/inlineRecordCollision.res | 2 + 32 files changed, 858 insertions(+), 306 deletions(-) create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecord.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordAnonymous.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordCollision.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/expected/inlineRecordCollision.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecord.res create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordAnonymous.res create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordCollision.res create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/inlineRecordCollision.res create mode 100644 tests/syntax_tests/data/printer/signature/expected/externalInlineRecord.resi.txt create mode 100644 tests/syntax_tests/data/printer/signature/expected/externalInlineRecordAnonymous.resi.txt create mode 100644 tests/syntax_tests/data/printer/signature/expected/externalInlineRecordBareArrow.resi.txt create mode 100644 tests/syntax_tests/data/printer/signature/expected/externalObjectSpread.resi.txt create mode 100644 tests/syntax_tests/data/printer/signature/externalInlineRecord.resi create mode 100644 tests/syntax_tests/data/printer/signature/externalInlineRecordAnonymous.resi create mode 100644 tests/syntax_tests/data/printer/signature/externalInlineRecordBareArrow.resi create mode 100644 tests/syntax_tests/data/printer/signature/externalObjectSpread.resi create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecord.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAll.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAnonymous.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecordBareArrow.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecordComments.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalObjectSpread.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecord.res create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecordAll.res create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecordAnonymous.res create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecordBareArrow.res create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecordComments.res create mode 100644 tests/syntax_tests/data/printer/structure/externalObjectSpread.res create mode 100644 tests/syntax_tests/data/printer/typeDef/expected/inlineRecordCollision.res.txt create mode 100644 tests/syntax_tests/data/printer/typeDef/inlineRecordCollision.res diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index aab15e930fa..05bcf981da1 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -17,17 +17,70 @@ let rec skip_doc_comments p = skip_doc_comments p | _ -> () -type inline_types_context = { - mutable found_inline_types: - (string * Warnings.loc * Parsetree.type_kind) list; +type inline_type_definition = { + name: string; + loc: Warnings.loc; + kind: Parsetree.type_kind; params: (Parsetree.core_type * Asttypes.variance) list; } +type inline_types_context = { + mutable found_inline_types: inline_type_definition list; + mutable params: (Parsetree.core_type * Asttypes.variance) list; + collect_free_type_vars: bool; +} + let extend_current_type_name_path current_type_name_path field_name = match current_type_name_path with | None -> None | Some path -> Some (path @ [field_name]) +let inline_type_name_exists inline_types_context inline_type_name = + inline_types_context.found_inline_types + |> List.exists (fun inline_type -> inline_type.name = inline_type_name) + +let inline_type_param_exists params param_name = + params + |> List.exists (fun (param, _) -> + match param.Parsetree.ptyp_desc with + | Ptyp_var existing_name -> existing_name = param_name + | _ -> false) + +let maybe_track_inline_type_param inline_types_context name loc = + match inline_types_context with + | Some inline_types_context + when inline_types_context.collect_free_type_vars + && not (inline_type_param_exists inline_types_context.params name) -> + inline_types_context.params <- + inline_types_context.params + @ [(Ast_helper.Typ.var ~loc name, Asttypes.Invariant)] + | _ -> () + +let make_inline_record_type_name current_type_name_path inline_types_context + ~base = + let extend name = extend_current_type_name_path current_type_name_path name in + let rec loop suffix = + let candidate = base ^ suffix in + match (current_type_name_path, inline_types_context) with + | Some prefix, Some inline_types_context -> + let full_name = String.concat "." (prefix @ [candidate]) in + if inline_type_name_exists inline_types_context full_name then + loop (suffix ^ "$") + else extend candidate + | _ -> extend candidate + in + loop "" + +let make_inline_record_return_type_name current_type_name_path + inline_types_context = + make_inline_record_type_name current_type_name_path inline_types_context + ~base:"return.type" + +let make_inline_record_argument_type_name current_type_name_path + inline_types_context index = + make_inline_record_type_name current_type_name_path inline_types_context + ~base:("arg" ^ string_of_int index) + module Recover = struct let default_expr () = let id = Location.mknoloc "rescript.exprhole" in @@ -4385,6 +4438,7 @@ and parse_atomic_typ_expr ?current_type_name_path ?inline_types_context ~attrs p ("", mk_loc p.start_pos p.prev_end_pos)) else parse_ident ~msg:ErrorMessages.type_var ~start_pos:p.start_pos p in + maybe_track_inline_type_param inline_types_context ident loc; Ast_helper.Typ.var ~loc ~attrs ident | Underscore -> let end_pos = p.end_pos in @@ -4428,7 +4482,8 @@ and parse_atomic_typ_expr ?current_type_name_path ?inline_types_context ~attrs p match c.ptyp_desc with | Ptyp_constr ({txt = Lident typename}, _) -> inline_types - |> List.exists (fun (name, _, _) -> name = typename) + |> List.exists (fun inline_type -> + inline_type.name = typename) | _ -> false) |> List.length in @@ -4530,7 +4585,7 @@ and parse_record_or_object_type ?current_type_name_path ?inline_types_context in match (inline_types_context, current_type_name_path) with | Some inline_types_context, Some current_type_name_path - when Grammar.is_record_decl_start p.token -> + when Grammar.is_field_decl_start p.token -> let labels = parse_comma_delimited_region ~grammar:Grammar.RecordDecl ~closing:Rbrace ~f: @@ -4541,13 +4596,19 @@ and parse_record_or_object_type ?current_type_name_path ?inline_types_context Parser.expect Rbrace p; let loc = mk_loc start_pos p.prev_end_pos in let inline_type_name = current_type_name_path |> String.concat "." in + let params = inline_types_context.params in inline_types_context.found_inline_types <- - (inline_type_name, loc, Parsetree.Ptype_record labels) + { + name = inline_type_name; + loc; + kind = Parsetree.Ptype_record labels; + params; + } :: inline_types_context.found_inline_types; let lid = Location.mkloc (Longident.Lident inline_type_name) loc in - Ast_helper.Typ.constr ~loc lid (inline_types_context.params |> List.map fst) + Ast_helper.Typ.constr ~loc lid (params |> List.map fst) | _ -> let () = match p.token with @@ -4588,7 +4649,8 @@ and parse_type_alias p typ = * | attrs ~ident: type_expr -> attrs are on the arrow * | attrs type_expr -> attrs are here part of the type_expr *) -and parse_type_parameter p = +and parse_type_parameter ?current_type_name_path ?inline_types_context + ?positional_type_name_path p = let doc_attr : Parsetree.attributes = match p.Parser.token with | DocComment (loc, s) -> @@ -4604,7 +4666,12 @@ and parse_type_parameter p = Parser.next p; let name, loc = parse_lident p in Parser.expect ~grammar:Grammar.TypeExpression Colon p; - let typ = parse_typ_expr p in + let arg_path = + extend_current_type_name_path current_type_name_path name + in + let typ = + parse_typ_expr ?current_type_name_path:arg_path ?inline_types_context p + in match p.Parser.token with | Equal -> Parser.next p; @@ -4623,7 +4690,13 @@ and parse_type_parameter p = Parser.err ~start_pos:loc.loc_start ~end_pos:loc.loc_end p error in Parser.next p; - let typ = parse_typ_expr p in + let arg_path = + extend_current_type_name_path current_type_name_path name + in + let typ = + parse_typ_expr ?current_type_name_path:arg_path ?inline_types_context + p + in match p.Parser.token with | Equal -> Parser.next p; @@ -4632,7 +4705,11 @@ and parse_type_parameter p = | _ -> Some {attrs; label = Labelled {txt = name; loc}; typ; start_pos}) | _ -> let constr = Location.mkloc (Longident.Lident name) loc in - let args = parse_type_constructor_args ~constr_name:constr p in + let args = + parse_type_constructor_args ?inline_types_context + ?current_type_name_path:positional_type_name_path + ~constr_name:constr p + in let typ = Ast_helper.Typ.constr ~loc:(mk_loc start_pos p.prev_end_pos) @@ -4643,7 +4720,10 @@ and parse_type_parameter p = let typ = parse_type_alias p typ in Some {attrs = []; label = Nolabel; typ; start_pos}) | _ -> - let typ = parse_typ_expr p in + let typ = + parse_typ_expr ?current_type_name_path:positional_type_name_path + ?inline_types_context p + in let typ_with_attributes = {typ with ptyp_attributes = List.concat [attrs; typ.ptyp_attributes]} in @@ -4651,7 +4731,7 @@ and parse_type_parameter p = else None (* (int, ~x:string, float) *) -and parse_type_parameters p = +and parse_type_parameters ?current_type_name_path ?inline_types_context p = let start_pos = p.Parser.start_pos in Parser.expect Lparen p; match p.Parser.token with @@ -4662,21 +4742,41 @@ and parse_type_parameters p = let typ = Ast_helper.Typ.constr unit_constr [] in [{attrs = []; label = Nolabel; typ; start_pos}] | _ -> + let positional_index = ref 0 in let params = parse_comma_delimited_region ~grammar:Grammar.TypeParameters - ~closing:Rparen ~f:parse_type_parameter p + ~closing:Rparen + ~f:(fun p -> + let positional_type_name_path = + make_inline_record_argument_type_name current_type_name_path + inline_types_context !positional_index + in + let parameter = + parse_type_parameter ?current_type_name_path ?inline_types_context + ?positional_type_name_path p + in + (match parameter with + | Some _ -> incr positional_index + | None -> ()); + parameter) + p in Parser.expect Rparen p; params -and parse_es6_arrow_type ~attrs p = +and parse_es6_arrow_type ?current_type_name_path ?inline_types_context ~attrs p + = let start_pos = p.Parser.start_pos in match p.Parser.token with | Tilde -> Parser.next p; let name, label_loc = parse_lident p in Parser.expect ~grammar:Grammar.TypeExpression Colon p; - let typ = parse_typ_expr ~alias:false ~es6_arrow:false p in + let arg_path = extend_current_type_name_path current_type_name_path name in + let typ = + parse_typ_expr ~alias:false ~es6_arrow:false + ?current_type_name_path:arg_path ?inline_types_context p + in let lbl = match p.Parser.token with | Equal -> @@ -4686,14 +4786,30 @@ and parse_es6_arrow_type ~attrs p = | _ -> Asttypes.Labelled {txt = name; loc = label_loc} in Parser.expect EqualGreater p; - let return_type = parse_typ_expr ~alias:false p in + let return_path = + make_inline_record_return_type_name current_type_name_path + inline_types_context + in + let return_type = + parse_typ_expr ~alias:false ?current_type_name_path:return_path + ?inline_types_context p + in let loc = mk_loc start_pos p.prev_end_pos in Ast_helper.Typ.arrow ~loc ~arity:None {attrs; lbl; typ} return_type | DocComment _ -> assert false | _ -> - let parameters = parse_type_parameters p in + let parameters = + parse_type_parameters ?current_type_name_path ?inline_types_context p + in Parser.expect EqualGreater p; - let return_type = parse_typ_expr ~alias:false p in + let return_path = + make_inline_record_return_type_name current_type_name_path + inline_types_context + in + let return_type = + parse_typ_expr ~alias:false ?current_type_name_path:return_path + ?inline_types_context p + in let end_pos = p.prev_end_pos in let return_type_arity = 0 in let _paramNum, typ, _arity = @@ -4761,25 +4877,36 @@ and parse_typ_expr ?current_type_name_path ?inline_types_context ?attrs | None -> parse_attributes p in let typ = - if es6_arrow && is_es6_arrow_type p then parse_es6_arrow_type ~attrs p + if es6_arrow && is_es6_arrow_type p then + parse_es6_arrow_type ~attrs ?current_type_name_path ?inline_types_context + p else let typ = parse_atomic_typ_expr ?current_type_name_path ?inline_types_context ~attrs p in - parse_arrow_type_rest ~es6_arrow ~start_pos typ p + parse_arrow_type_rest ?current_type_name_path ?inline_types_context + ~es6_arrow ~start_pos typ p in let typ = if alias then parse_type_alias p typ else typ in (* Parser.eatBreadcrumb p; *) typ -and parse_arrow_type_rest ~es6_arrow ~start_pos typ p = +and parse_arrow_type_rest ?current_type_name_path ?inline_types_context + ~es6_arrow ~start_pos typ p = match p.Parser.token with | (EqualGreater | MinusGreater) as token when es6_arrow == true -> (* error recovery *) if token = MinusGreater then Parser.expect EqualGreater p; Parser.next p; - let return_type = parse_typ_expr ~alias:false p in + let return_path = + make_inline_record_return_type_name current_type_name_path + inline_types_context + in + let return_type = + parse_typ_expr ~alias:false ?current_type_name_path:return_path + ?inline_types_context p + in let loc = mk_loc start_pos p.prev_end_pos in Ast_helper.Typ.arrow ~loc ~arity:(Some 1) {attrs = []; lbl = Nolabel; typ} @@ -4790,6 +4917,36 @@ and parse_typ_expr_region p = if Grammar.is_typ_expr_start p.Parser.token then Some (parse_typ_expr p) else None +and is_external_bare_arrow_type p = + Parser.lookahead p (fun state -> + let attrs = parse_attributes state in + match state.Parser.token with + | Lparen | Tilde | DocComment _ -> false + | _ -> ( + ignore (parse_atomic_typ_expr ~attrs state); + match state.Parser.token with + | EqualGreater | MinusGreater -> true + | _ -> false)) + +and parse_external_type_expr ~current_type_name_path ~inline_types_context p = + if is_external_bare_arrow_type p then + let start_pos = p.Parser.start_pos in + let attrs = parse_attributes p in + let arg_path = + make_inline_record_argument_type_name current_type_name_path + (Some inline_types_context) 0 + in + let typ = + parse_atomic_typ_expr ~attrs ?current_type_name_path:arg_path + ~inline_types_context p + in + let typ = + parse_arrow_type_rest ?current_type_name_path ~inline_types_context + ~es6_arrow:true ~start_pos typ p + in + parse_type_alias p typ + else parse_typ_expr ?current_type_name_path ~inline_types_context p + and parse_tuple_type ~attrs ~first ~start_pos p = let typexprs = first @@ -6193,7 +6350,9 @@ and parse_type_definition_or_extension ~attrs p = |> Diagnostics.message) in let current_type_name_path = Longident.flatten name.txt in - let inline_types_context = {found_inline_types = []; params} in + let inline_types_context = + {found_inline_types = []; params; collect_free_type_vars = false} + in let type_defs = parse_type_definitions ~inline_types_context ~current_type_name_path ~attrs ~name ~params ~start_pos p @@ -6205,11 +6364,11 @@ and parse_type_definition_or_extension ~attrs p = in let inline_types = inline_types_context.found_inline_types - |> List.map (fun (inline_type_name, loc, kind) -> - Ast_helper.Type.mk ~params + |> List.map (fun inline_type -> + Ast_helper.Type.mk ~params:inline_type.params ~attrs:[(Location.mknoloc "res.inlineRecordDefinition", PStr [])] - ~loc ~kind - {name with txt = inline_type_name}) + ~loc:inline_type.loc ~kind:inline_type.kind + {name with txt = inline_type.name}) in TypeDef {rec_flag; types = inline_types @ type_defs} @@ -6218,31 +6377,50 @@ and parse_external_def ~attrs ~start_pos p = let in_external = !InExternal.status in InExternal.status := true; Parser.leave_breadcrumb p Grammar.External; - Parser.expect Token.External p; - let name, loc = parse_lident p in - let name = Location.mkloc name loc in - Parser.expect ~grammar:Grammar.TypeExpression Colon p; - let typ_expr = parse_typ_expr p in - let equal_start = p.start_pos in - let equal_end = p.end_pos in - Parser.expect Equal p; - let prim = - match p.token with - | String s -> - Parser.next p; - [s] - | _ -> - Parser.err ~start_pos:equal_start ~end_pos:equal_end p - (Diagnostics.message - ("An external requires the name of the JS value you're referring \ - to, like \"" ^ name.txt ^ "\".")); - [] - in - let loc = mk_loc start_pos p.prev_end_pos in - let vb = Ast_helper.Val.mk ~loc ~attrs ~prim name typ_expr in - Parser.eat_breadcrumb p; - InExternal.status := in_external; - vb + Fun.protect + ~finally:(fun () -> + Parser.eat_breadcrumb p; + InExternal.status := in_external) + (fun () -> + Parser.expect Token.External p; + let name, loc = parse_lident p in + let name = Location.mkloc name loc in + Parser.expect ~grammar:Grammar.TypeExpression Colon p; + let current_type_name_path = Some [name.txt] in + let inline_types_context = + {found_inline_types = []; params = []; collect_free_type_vars = true} + in + let typ_expr = + parse_external_type_expr ~current_type_name_path ~inline_types_context p + in + let equal_start = p.start_pos in + let equal_end = p.end_pos in + Parser.expect Equal p; + let prim = + match p.token with + | String s -> + Parser.next p; + [s] + | _ -> + Parser.err ~start_pos:equal_start ~end_pos:equal_end p + (Diagnostics.message + ("An external requires the name of the JS value you're \ + referring to, like \"" ^ name.txt ^ "\".")); + [] + in + let loc = mk_loc start_pos p.prev_end_pos in + let vb = Ast_helper.Val.mk ~loc ~attrs ~prim name typ_expr in + let inline_types = + inline_types_context.found_inline_types + |> List.rev_map (fun inline_type -> + Ast_helper.Type.mk ~params:inline_type.params + ~attrs: + [(Location.mknoloc "res.inlineRecordDefinition", PStr [])] + ~loc:inline_type.loc ~kind:inline_type.kind + {name with txt = inline_type.name}) + |> List.rev + in + (vb, inline_types)) (* constr-def ::= * | constr-decl @@ -6306,98 +6484,111 @@ and parse_newline_or_semicolon_structure p = newline") | _ -> () -and parse_structure_item_region p = +and parse_structure_item_region pending_structure_items p = let start_pos = p.Parser.start_pos in - let attrs = parse_attributes p in - match p.Parser.token with - | Open -> - let open_description = parse_open_description ~attrs p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.open_ ~loc open_description) - | Let {unwrap} -> - let rec_flag, let_bindings = - parse_let_bindings ~unwrap ~attrs ~start_pos p - in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.value ~loc rec_flag let_bindings) - | Typ -> ( - Parser.begin_region p; - match parse_type_definition_or_extension ~attrs p with - | TypeDef {rec_flag; types} -> + match !pending_structure_items with + | item :: rest -> + pending_structure_items := rest; + Some item + | [] -> ( + let attrs = parse_attributes p in + match p.Parser.token with + | Open -> + let open_description = parse_open_description ~attrs p in parse_newline_or_semicolon_structure p; let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some (Ast_helper.Str.type_ ~loc rec_flag types) - | TypeExt ext -> + Some (Ast_helper.Str.open_ ~loc open_description) + | Let {unwrap} -> + let rec_flag, let_bindings = + parse_let_bindings ~unwrap ~attrs ~start_pos p + in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Str.value ~loc rec_flag let_bindings) + | Typ -> ( + Parser.begin_region p; + match parse_type_definition_or_extension ~attrs p with + | TypeDef {rec_flag; types} -> + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Str.type_ ~loc rec_flag types) + | TypeExt ext -> + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Str.type_extension ~loc ext)) + | External -> ( + let external_def, inline_types = parse_external_def ~attrs ~start_pos p in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + match inline_types with + | [] -> Some (Ast_helper.Str.primitive ~loc external_def) + | _ -> + let type_item = + Ast_helper.Str.type_ ~loc Asttypes.Recursive inline_types + in + let primitive_item = Ast_helper.Str.primitive ~loc external_def in + pending_structure_items := primitive_item :: !pending_structure_items; + Some type_item) + | Exception -> + let exception_def = parse_exception_def ~attrs p in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Str.exception_ ~loc exception_def) + | Include -> + let include_statement = parse_include_statement ~attrs p in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Str.include_ ~loc include_statement) + | Module -> + Parser.begin_region p; + let structure_item = + parse_module_or_module_type_impl_or_pack_expr ~attrs p + in parse_newline_or_semicolon_structure p; let loc = mk_loc start_pos p.prev_end_pos in Parser.end_region p; - Some (Ast_helper.Str.type_extension ~loc ext)) - | External -> - let external_def = parse_external_def ~attrs ~start_pos p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.primitive ~loc external_def) - | Exception -> - let exception_def = parse_exception_def ~attrs p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.exception_ ~loc exception_def) - | Include -> - let include_statement = parse_include_statement ~attrs p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.include_ ~loc include_statement) - | Module -> - Parser.begin_region p; - let structure_item = - parse_module_or_module_type_impl_or_pack_expr ~attrs p - in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some {structure_item with pstr_loc = loc} - | ModuleComment (loc, s) -> - Parser.next p; - Some - (Ast_helper.Str.attribute ~loc - ( {txt = "res.doc"; loc}, - PStr - [ - Ast_helper.Str.eval ~loc - (Ast_helper.Exp.constant ~loc (Pconst_string (s, None))); - ] )) - | AtAt -> - let attr = parse_standalone_attribute p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.attribute ~loc attr) - | PercentPercent -> - let extension = parse_extension ~module_language:true p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Str.extension ~attrs ~loc extension) - | token when Grammar.is_expr_start token -> - let prev_end_pos = p.Parser.end_pos in - let exp = parse_expr p in - parse_newline_or_semicolon_structure p; - let loc = mk_loc start_pos p.prev_end_pos in - Parser.check_progress ~prev_end_pos - ~result:(Ast_helper.Str.eval ~loc ~attrs exp) - p - | _ -> ( - match attrs with - | (({Asttypes.loc = attr_loc}, _) as attr) :: _ -> - Parser.err ~start_pos:attr_loc.loc_start ~end_pos:attr_loc.loc_end p - (Diagnostics.message (ErrorMessages.attribute_without_node attr)); - let expr = parse_expr p in + Some {structure_item with pstr_loc = loc} + | ModuleComment (loc, s) -> + Parser.next p; Some - (Ast_helper.Str.eval - ~loc:(mk_loc p.start_pos p.prev_end_pos) - ~attrs expr) - | _ -> None) + (Ast_helper.Str.attribute ~loc + ( {txt = "res.doc"; loc}, + PStr + [ + Ast_helper.Str.eval ~loc + (Ast_helper.Exp.constant ~loc (Pconst_string (s, None))); + ] )) + | AtAt -> + let attr = parse_standalone_attribute p in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Str.attribute ~loc attr) + | PercentPercent -> + let extension = parse_extension ~module_language:true p in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Str.extension ~attrs ~loc extension) + | token when Grammar.is_expr_start token -> + let prev_end_pos = p.Parser.end_pos in + let exp = parse_expr p in + parse_newline_or_semicolon_structure p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.check_progress ~prev_end_pos + ~result:(Ast_helper.Str.eval ~loc ~attrs exp) + p + | _ -> ( + match attrs with + | (({Asttypes.loc = attr_loc}, _) as attr) :: _ -> + Parser.err ~start_pos:attr_loc.loc_start ~end_pos:attr_loc.loc_end p + (Diagnostics.message (ErrorMessages.attribute_without_node attr)); + let expr = parse_expr p in + Some + (Ast_helper.Str.eval + ~loc:(mk_loc p.start_pos p.prev_end_pos) + ~attrs expr) + | _ -> None)) [@@progress Parser.next, Parser.expect] (* include-statement ::= include module-expr *) @@ -6416,10 +6607,12 @@ and parse_atomic_module_expr p = Ast_helper.Mod.ident ~loc:longident.loc longident | Lbrace -> Parser.next p; + let pending_structure_items = ref [] in let structure = Ast_helper.Mod.structure (parse_delimited_region ~grammar:Grammar.Structure ~closing:Rbrace - ~f:parse_structure_item_region p) + ~f:(parse_structure_item_region pending_structure_items) + p) in Parser.expect Rbrace p; let end_pos = p.prev_end_pos in @@ -6767,9 +6960,11 @@ and parse_atomic_module_type p = {mty with pmty_loc = mk_loc start_pos p.prev_end_pos} | Lbrace -> Parser.next p; + let pending_signature_items = ref [] in let spec = parse_delimited_region ~grammar:Grammar.Signature ~closing:Rbrace - ~f:parse_signature_item_region p + ~f:(parse_signature_item_region pending_signature_items) + p in Parser.expect Rbrace p; let loc = mk_loc start_pos p.prev_end_pos in @@ -6943,113 +7138,126 @@ and parse_newline_or_semicolon_signature p = newline") | _ -> () -and parse_signature_item_region p = +and parse_signature_item_region pending_signature_items p = let start_pos = p.Parser.start_pos in - let attrs = parse_attributes p in - match p.Parser.token with - | Let {unwrap} -> - if unwrap then ( - Parser.err ~start_pos ~end_pos:p.Parser.end_pos p - (Diagnostics.message ErrorMessages.experimental_let_unwrap_sig); - Parser.next p); - Parser.begin_region p; - let value_desc = parse_sign_let_desc ~attrs p in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some (Ast_helper.Sig.value ~loc value_desc) - | Typ -> ( - Parser.begin_region p; - match parse_type_definition_or_extension ~attrs p with - | TypeDef {rec_flag; types} -> + match !pending_signature_items with + | item :: rest -> + pending_signature_items := rest; + Some item + | [] -> ( + let attrs = parse_attributes p in + match p.Parser.token with + | Let {unwrap} -> + if unwrap then ( + Parser.err ~start_pos ~end_pos:p.Parser.end_pos p + (Diagnostics.message ErrorMessages.experimental_let_unwrap_sig); + Parser.next p); + Parser.begin_region p; + let value_desc = parse_sign_let_desc ~attrs p in parse_newline_or_semicolon_signature p; let loc = mk_loc start_pos p.prev_end_pos in Parser.end_region p; - Some (Ast_helper.Sig.type_ ~loc rec_flag types) - | TypeExt ext -> + Some (Ast_helper.Sig.value ~loc value_desc) + | Typ -> ( + Parser.begin_region p; + match parse_type_definition_or_extension ~attrs p with + | TypeDef {rec_flag; types} -> + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Sig.type_ ~loc rec_flag types) + | TypeExt ext -> + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Sig.type_extension ~loc ext)) + | External -> ( + let external_def, inline_types = parse_external_def ~attrs ~start_pos p in parse_newline_or_semicolon_signature p; let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some (Ast_helper.Sig.type_extension ~loc ext)) - | External -> - let external_def = parse_external_def ~attrs ~start_pos p in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Sig.value ~loc external_def) - | Exception -> - let exception_def = parse_exception_def ~attrs p in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Sig.exception_ ~loc exception_def) - | Open -> - let open_description = parse_open_description ~attrs p in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Sig.open_ ~loc open_description) - | Include -> - Parser.next p; - let module_type = parse_module_type p in - let include_description = - Ast_helper.Incl.mk - ~loc:(mk_loc start_pos p.prev_end_pos) - ~attrs module_type - in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Sig.include_ ~loc include_description) - | Module -> ( - Parser.begin_region p; - Parser.next p; - match p.Parser.token with - | Uident _ -> - let mod_decl = parse_module_declaration_or_alias ~attrs p in + match inline_types with + | [] -> Some (Ast_helper.Sig.value ~loc external_def) + | _ -> + let type_item = + Ast_helper.Sig.type_ ~loc Asttypes.Recursive inline_types + in + let value_item = Ast_helper.Sig.value ~loc external_def in + pending_signature_items := value_item :: !pending_signature_items; + Some type_item) + | Exception -> + let exception_def = parse_exception_def ~attrs p in parse_newline_or_semicolon_signature p; let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some (Ast_helper.Sig.module_ ~loc mod_decl) - | Rec -> - let rec_module = parse_rec_module_spec ~attrs ~start_pos p in + Some (Ast_helper.Sig.exception_ ~loc exception_def) + | Open -> + let open_description = parse_open_description ~attrs p in parse_newline_or_semicolon_signature p; let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some (Ast_helper.Sig.rec_module ~loc rec_module) - | Typ -> - let mod_type_decl = parse_module_type_declaration ~attrs ~start_pos p in - Parser.end_region p; - Some mod_type_decl - | _t -> - let mod_decl = parse_module_declaration_or_alias ~attrs p in + Some (Ast_helper.Sig.open_ ~loc open_description) + | Include -> + Parser.next p; + let module_type = parse_module_type p in + let include_description = + Ast_helper.Incl.mk + ~loc:(mk_loc start_pos p.prev_end_pos) + ~attrs module_type + in parse_newline_or_semicolon_signature p; let loc = mk_loc start_pos p.prev_end_pos in - Parser.end_region p; - Some (Ast_helper.Sig.module_ ~loc mod_decl)) - | AtAt -> - let attr = parse_standalone_attribute p in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Sig.attribute ~loc attr) - | ModuleComment (loc, s) -> - Parser.next p; - Some - (Ast_helper.Sig.attribute ~loc - ( {txt = "res.doc"; loc}, - PStr - [ - Ast_helper.Str.eval ~loc - (Ast_helper.Exp.constant ~loc (Pconst_string (s, None))); - ] )) - | PercentPercent -> - let extension = parse_extension ~module_language:true p in - parse_newline_or_semicolon_signature p; - let loc = mk_loc start_pos p.prev_end_pos in - Some (Ast_helper.Sig.extension ~attrs ~loc extension) - | _ -> ( - match attrs with - | (({Asttypes.loc = attr_loc}, _) as attr) :: _ -> - Parser.err ~start_pos:attr_loc.loc_start ~end_pos:attr_loc.loc_end p - (Diagnostics.message (ErrorMessages.attribute_without_node attr)); - Some Recover.default_signature_item - | _ -> None) + Some (Ast_helper.Sig.include_ ~loc include_description) + | Module -> ( + Parser.begin_region p; + Parser.next p; + match p.Parser.token with + | Uident _ -> + let mod_decl = parse_module_declaration_or_alias ~attrs p in + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Sig.module_ ~loc mod_decl) + | Rec -> + let rec_module = parse_rec_module_spec ~attrs ~start_pos p in + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Sig.rec_module ~loc rec_module) + | Typ -> + let mod_type_decl = parse_module_type_declaration ~attrs ~start_pos p in + Parser.end_region p; + Some mod_type_decl + | _t -> + let mod_decl = parse_module_declaration_or_alias ~attrs p in + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Parser.end_region p; + Some (Ast_helper.Sig.module_ ~loc mod_decl)) + | AtAt -> + let attr = parse_standalone_attribute p in + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Sig.attribute ~loc attr) + | ModuleComment (loc, s) -> + Parser.next p; + Some + (Ast_helper.Sig.attribute ~loc + ( {txt = "res.doc"; loc}, + PStr + [ + Ast_helper.Str.eval ~loc + (Ast_helper.Exp.constant ~loc (Pconst_string (s, None))); + ] )) + | PercentPercent -> + let extension = parse_extension ~module_language:true p in + parse_newline_or_semicolon_signature p; + let loc = mk_loc start_pos p.prev_end_pos in + Some (Ast_helper.Sig.extension ~attrs ~loc extension) + | _ -> ( + match attrs with + | (({Asttypes.loc = attr_loc}, _) as attr) :: _ -> + Parser.err ~start_pos:attr_loc.loc_start ~end_pos:attr_loc.loc_end p + (Diagnostics.message (ErrorMessages.attribute_without_node attr)); + Some Recover.default_signature_item + | _ -> None)) [@@progress Parser.next, Parser.expect] (* module rec module-name : module-type { and module-name: module-type } *) @@ -7209,9 +7417,11 @@ and parse_payload p = Parser.next p; let payload = if Grammar.is_signature_item_start p.token then + let pending_signature_items = ref [] in Parsetree.PSig (parse_delimited_region ~grammar:Grammar.Signature ~closing:Rparen - ~f:parse_signature_item_region p) + ~f:(parse_signature_item_region pending_signature_items) + p) else Parsetree.PTyp (parse_typ_expr p) in Parser.expect Rparen p; @@ -7231,9 +7441,11 @@ and parse_payload p = Parser.eat_breadcrumb p; Parsetree.PPat (pattern, expr) | _ -> + let pending_structure_items = ref [] in let items = parse_delimited_region ~grammar:Grammar.Structure ~closing:Rparen - ~f:parse_structure_item_region p + ~f:(parse_structure_item_region pending_structure_items) + p in Parser.expect Rparen p; Parser.eat_breadcrumb p; @@ -7320,8 +7532,12 @@ and parse_extension ?(module_language = false) p = (* module signature on the file level *) let parse_specification p : Parsetree.signature = - parse_region p ~grammar:Grammar.Specification ~f:parse_signature_item_region + let pending_signature_items = ref [] in + parse_region p ~grammar:Grammar.Specification + ~f:(parse_signature_item_region pending_signature_items) (* module structure on the file level *) let parse_implementation p : Parsetree.structure = - parse_region p ~grammar:Grammar.Implementation ~f:parse_structure_item_region + let pending_structure_items = ref [] in + parse_region p ~grammar:Grammar.Implementation + ~f:(parse_structure_item_region pending_structure_items) diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 2010d23f6dd..a8351bfe5fb 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -319,10 +319,12 @@ let print_comments doc (tbl : CommentTable.t) loc = let doc_with_leading_comments = print_leading_comments doc tbl.leading loc in print_trailing_comments doc_with_leading_comments tbl.trailing loc +let is_empty_doc doc = doc = Doc.nil + let print_list ~get_loc ~nodes ~print ?(force_break = false) t = - let rec loop (prev_loc : Location.t) acc nodes = + let rec loop first_loc_opt prev_loc_opt acc nodes = match nodes with - | [] -> (prev_loc, Doc.concat (List.rev acc)) + | [] -> (first_loc_opt, prev_loc_opt, Doc.concat (List.rev acc)) | node :: nodes -> let loc = get_loc node in let start_pos = @@ -330,30 +332,42 @@ let print_list ~get_loc ~nodes ~print ?(force_break = false) t = | None -> loc.loc_start | Some comment -> (Comment.loc comment).loc_start in - let sep = - if start_pos.pos_lnum - prev_loc.loc_end.pos_lnum > 1 then - Doc.concat [Doc.hard_line; Doc.hard_line] - else Doc.hard_line - in - let doc = print_comments (print node t) t loc in - loop loc (doc :: sep :: acc) nodes + let raw_doc = print node t in + if is_empty_doc raw_doc then loop first_loc_opt prev_loc_opt acc nodes + else + let doc = print_comments raw_doc t loc in + let acc = + match prev_loc_opt with + | None -> [doc] + | Some prev_loc -> + let sep = + if start_pos.pos_lnum - prev_loc.Location.loc_end.pos_lnum > 1 + then Doc.concat [Doc.hard_line; Doc.hard_line] + else Doc.hard_line + in + doc :: sep :: acc + in + let first_loc_opt = + match first_loc_opt with + | None -> Some loc + | Some _ -> first_loc_opt + in + loop first_loc_opt (Some loc) acc nodes in - match nodes with - | [] -> Doc.nil - | node :: nodes -> - let first_loc = get_loc node in - let doc = print_comments (print node t) t first_loc in - let last_loc, docs = loop first_loc [doc] nodes in + let first_loc_opt, last_loc_opt, docs = loop None None [] nodes in + match (first_loc_opt, last_loc_opt) with + | Some first_loc, Some last_loc -> let force_break = force_break || first_loc.loc_start.pos_lnum != last_loc.loc_end.pos_lnum in Doc.breakable_group ~force_break docs + | _ -> Doc.nil let print_listi ~get_loc ~nodes ~print ?(ignore_empty_lines = false) ?(force_break = false) t = - let rec loop i (prev_loc : Location.t) acc nodes = + let rec loop i first_loc_opt prev_loc_opt acc nodes = match nodes with - | [] -> (prev_loc, Doc.concat (List.rev acc)) + | [] -> (first_loc_opt, prev_loc_opt, Doc.concat (List.rev acc)) | node :: nodes -> let loc = get_loc node in let start_pos = @@ -361,26 +375,38 @@ let print_listi ~get_loc ~nodes ~print ?(ignore_empty_lines = false) | None -> loc.loc_start | Some comment -> (Comment.loc comment).loc_start in - let sep = - if - start_pos.pos_lnum - prev_loc.loc_end.pos_lnum > 1 - && not ignore_empty_lines - then Doc.concat [Doc.hard_line; Doc.hard_line] - else Doc.line - in - let doc = print_comments (print node t i) t loc in - loop (i + 1) loc (doc :: sep :: acc) nodes + let raw_doc = print node t i in + if is_empty_doc raw_doc then loop i first_loc_opt prev_loc_opt acc nodes + else + let doc = print_comments raw_doc t loc in + let acc = + match prev_loc_opt with + | None -> [doc] + | Some prev_loc -> + let sep = + if + start_pos.pos_lnum - prev_loc.Location.loc_end.pos_lnum > 1 + && not ignore_empty_lines + then Doc.concat [Doc.hard_line; Doc.hard_line] + else Doc.line + in + doc :: sep :: acc + in + let first_loc_opt = + match first_loc_opt with + | None -> Some loc + | Some _ -> first_loc_opt + in + loop (i + 1) first_loc_opt (Some loc) acc nodes in - match nodes with - | [] -> Doc.nil - | node :: nodes -> - let first_loc = get_loc node in - let doc = print_comments (print node t 0) t first_loc in - let last_loc, docs = loop 1 first_loc [doc] nodes in + let first_loc_opt, last_loc_opt, docs = loop 0 None None [] nodes in + match (first_loc_opt, last_loc_opt) with + | Some first_loc, Some last_loc -> let force_break = force_break || first_loc.loc_start.pos_lnum != last_loc.loc_end.pos_lnum in Doc.breakable_group ~force_break docs + | _ -> Doc.nil let rec print_longident_aux accu = function | Longident.Lident s -> Doc.text s :: accu @@ -475,6 +501,28 @@ let find_inline_record_definition inline_record_name |> List.find_opt (fun (r : Parsetree.type_declaration) -> r.ptype_name.txt = inline_record_name) +let pending_inline_record_definitions inline_record_definitions = + let external_name_of_type_declaration + (type_declaration : Parsetree.type_declaration) = + match String.index_opt type_declaration.ptype_name.txt '.' with + | None -> None + | Some index -> Some (String.sub type_declaration.ptype_name.txt 0 index) + in + match inline_record_definitions with + | [] -> None + | first :: rest -> ( + match external_name_of_type_declaration first with + | None -> None + | Some external_name -> + if + List.for_all + (fun type_declaration -> + external_name_of_type_declaration type_declaration + = Some external_name) + rest + then Some (external_name, inline_record_definitions) + else None) + let print_lident l = let flat_lid_opt lid = let rec flat accu = function @@ -566,11 +614,26 @@ let print_constant ?(template_literal = false) c = module State = struct let custom_layout_threshold = 2 - type t = {custom_layout: int} + type pending_inline_record_definitions = { + external_name: string; + definitions: Parsetree.type_declaration list; + } - let init () = {custom_layout = 0} + type t = { + custom_layout: int; + mutable pending_inline_record_defs_for_external: + pending_inline_record_definitions option; + } - let next_custom_layout t = {custom_layout = t.custom_layout + 1} + let init () = + {custom_layout = 0; pending_inline_record_defs_for_external = None} + + let next_custom_layout t = + { + custom_layout = t.custom_layout + 1; + pending_inline_record_defs_for_external = + t.pending_inline_record_defs_for_external; + } let should_break_callback t = t.custom_layout > custom_layout_threshold end @@ -586,8 +649,12 @@ let rec print_structure ~state (s : Parsetree.structure) t = t and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = + let clear_pending () = + state.State.pending_inline_record_defs_for_external <- None + in match si.pstr_desc with | Pstr_value (rec_flag, value_bindings) -> + clear_pending (); let rec_flag = match rec_flag with | Asttypes.Nonrecursive -> Doc.nil @@ -599,6 +666,7 @@ and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = | Pstr_primitive value_description -> print_value_description ~state value_description cmt_tbl | Pstr_eval (expr, attrs) -> + clear_pending (); let expr_doc = let doc = print_expression_with_comments ~state expr cmt_tbl in match Parens.structure_expr expr with @@ -608,8 +676,10 @@ and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = in Doc.concat [print_attributes ~state attrs cmt_tbl; expr_doc] | Pstr_attribute attr -> + clear_pending (); fst (print_attribute ~state ~standalone:true attr cmt_tbl) | Pstr_extension (extension, attrs) -> + clear_pending (); Doc.concat [ print_attributes ~state attrs cmt_tbl; @@ -617,22 +687,29 @@ and print_structure_item ~state (si : Parsetree.structure_item) cmt_tbl = [print_extension ~state ~at_module_lvl:true extension cmt_tbl]; ] | Pstr_include include_declaration -> + clear_pending (); print_include_declaration ~state include_declaration cmt_tbl | Pstr_open open_description -> + clear_pending (); print_open_description ~state open_description cmt_tbl | Pstr_modtype mod_type_decl -> + clear_pending (); print_module_type_declaration ~state mod_type_decl cmt_tbl | Pstr_module module_binding -> + clear_pending (); print_module_binding ~state ~is_rec:false module_binding cmt_tbl 0 | Pstr_recmodule module_bindings -> + clear_pending (); print_listi ~get_loc:(fun mb -> mb.Parsetree.pmb_loc) ~nodes:module_bindings ~print:(print_module_binding ~state ~is_rec:true) cmt_tbl | Pstr_exception extension_constructor -> + clear_pending (); print_exception_def ~state extension_constructor cmt_tbl | Pstr_typext type_extension -> + clear_pending (); print_type_extension ~state type_extension cmt_tbl and print_type_extension ~state (te : Parsetree.type_extension) cmt_tbl = @@ -966,28 +1043,40 @@ and print_signature ~state signature cmt_tbl = cmt_tbl and print_signature_item ~state (si : Parsetree.signature_item) cmt_tbl = + let clear_pending () = + state.State.pending_inline_record_defs_for_external <- None + in match si.psig_desc with | Parsetree.Psig_value value_description -> print_value_description ~state value_description cmt_tbl | Psig_type (rec_flag, type_declarations) -> print_type_declarations ~state ~rec_flag type_declarations cmt_tbl | Psig_typext type_extension -> + clear_pending (); print_type_extension ~state type_extension cmt_tbl | Psig_exception extension_constructor -> + clear_pending (); print_exception_def ~state extension_constructor cmt_tbl | Psig_module module_declaration -> + clear_pending (); print_module_declaration ~state module_declaration cmt_tbl | Psig_recmodule module_declarations -> + clear_pending (); print_rec_module_declarations ~state module_declarations cmt_tbl | Psig_modtype mod_type_decl -> + clear_pending (); print_module_type_declaration ~state mod_type_decl cmt_tbl | Psig_open open_description -> + clear_pending (); print_open_description ~state open_description cmt_tbl | Psig_include include_description -> + clear_pending (); print_include_description ~state include_description cmt_tbl | Psig_attribute attr -> + clear_pending (); fst (print_attribute ~state ~standalone:true attr cmt_tbl) | Psig_extension (extension, attrs) -> + clear_pending (); Doc.concat [ print_attributes ~state attrs cmt_tbl; @@ -1141,6 +1230,17 @@ and print_value_description ~state value_description cmt_tbl = value_description.pval_attributes cmt_tbl in let header = if is_external then "external " else "let " in + let inline_record_definitions = + match state.State.pending_inline_record_defs_for_external with + | Some {external_name; definitions} + when is_external && external_name = value_description.pval_name.txt -> + state.State.pending_inline_record_defs_for_external <- None; + Some definitions + | Some _ -> + state.State.pending_inline_record_defs_for_external <- None; + None + | None -> None + in Doc.group (Doc.concat [ @@ -1150,7 +1250,8 @@ and print_value_description ~state value_description cmt_tbl = (print_ident_like value_description.pval_name.txt) cmt_tbl value_description.pval_name.loc; Doc.text ": "; - print_typ_expr ~state value_description.pval_type cmt_tbl; + print_typ_expr ?inline_record_definitions ~state + value_description.pval_type cmt_tbl; (if is_external then Doc.group (Doc.concat @@ -1172,28 +1273,49 @@ and print_value_description ~state value_description cmt_tbl = ]) and print_type_declarations ~state ~rec_flag type_declarations cmt_tbl = - if has_inline_type_definitions type_declarations then + if has_inline_type_definitions type_declarations then ( let inline_record_definitions, regular_declarations = type_declarations |> List.partition (fun (td : Parsetree.type_declaration) -> Res_parsetree_viewer.has_inline_record_definition_attribute td.ptype_attributes) in - let adjusted_rec_flag = - match rec_flag with - | Recursive -> - if List.length regular_declarations > 1 then Doc.text "rec " - else Doc.nil - | Nonrecursive -> Doc.nil - in - print_listi - ~get_loc:(fun n -> n.Parsetree.ptype_loc) - ~nodes:regular_declarations - ~print: - (print_type_declaration2 ~inline_record_definitions ~state - ~rec_flag:adjusted_rec_flag) - cmt_tbl - else + match regular_declarations with + | [] -> ( + match pending_inline_record_definitions inline_record_definitions with + | Some (external_name, definitions) -> + state.State.pending_inline_record_defs_for_external <- + Some State.{external_name; definitions}; + Doc.nil + | None -> + print_listi + ~get_loc:(fun n -> n.Parsetree.ptype_loc) + ~nodes:type_declarations + ~print: + (print_type_declaration2 ~state + ~rec_flag: + (match rec_flag with + | Nonrecursive -> Doc.nil + | Recursive -> Doc.text "rec ")) + cmt_tbl) + | _ -> + state.State.pending_inline_record_defs_for_external <- None; + let adjusted_rec_flag = + match rec_flag with + | Recursive -> + if List.length regular_declarations > 1 then Doc.text "rec " + else Doc.nil + | Nonrecursive -> Doc.nil + in + print_listi + ~get_loc:(fun n -> n.Parsetree.ptype_loc) + ~nodes:regular_declarations + ~print: + (print_type_declaration2 ~inline_record_definitions ~state + ~rec_flag:adjusted_rec_flag) + cmt_tbl) + else ( + state.State.pending_inline_record_defs_for_external <- None; print_listi ~get_loc:(fun n -> n.Parsetree.ptype_loc) ~nodes:type_declarations @@ -1203,7 +1325,7 @@ and print_type_declarations ~state ~rec_flag type_declarations cmt_tbl = (match rec_flag with | Nonrecursive -> Doc.nil | Recursive -> Doc.text "rec ")) - cmt_tbl + cmt_tbl) (* * type_declaration = { @@ -1719,7 +1841,9 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) | _ -> false in let return_doc = - let doc = print_typ_expr ~state return_type cmt_tbl in + let doc = + print_typ_expr ?inline_record_definitions ~state return_type cmt_tbl + in if return_type_needs_parens then Doc.concat [Doc.lparen; doc; Doc.rparen] else doc in @@ -1733,7 +1857,9 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) else Doc.nil in let typ_doc = - let doc = print_typ_expr ~state typ cmt_tbl in + let doc = + print_typ_expr ?inline_record_definitions ~state typ cmt_tbl + in match typ.ptyp_desc with | Ptyp_arrow _ | Ptyp_tuple _ | Ptyp_alias _ -> add_parens doc | _ -> doc @@ -1769,7 +1895,9 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) Doc.join ~sep:(Doc.concat [Doc.comma; Doc.line]) (List.map - (fun tp -> print_type_parameter ~state tp cmt_tbl) + (fun tp -> + print_type_parameter ?inline_record_definitions ~state + tp cmt_tbl) args); ]); Doc.trailing_comma; @@ -1797,7 +1925,9 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) | Ptyp_arrow _ -> true | _ -> false in - let doc = print_typ_expr ~state typ cmt_tbl in + let doc = + print_typ_expr ?inline_record_definitions ~state typ cmt_tbl + in if needs_parens then Doc.concat [Doc.lparen; doc; Doc.rparen] else doc in Doc.concat @@ -1840,7 +1970,8 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) [ constr_name; Doc.less_than; - print_tuple_type ~state ~inline:true tuple cmt_tbl; + print_tuple_type ?inline_record_definitions ~state ~inline:true + tuple cmt_tbl; Doc.greater_than; ]) | Ptyp_constr (longident_loc, constr_args) -> ( @@ -1869,7 +2000,9 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) Doc.soft_line; Doc.greater_than; ])) - | Ptyp_tuple types -> print_tuple_type ~state ~inline:false types cmt_tbl + | Ptyp_tuple types -> + print_tuple_type ?inline_record_definitions ~state ~inline:false types + cmt_tbl | Ptyp_poly ([], typ) -> print_typ_expr ?inline_record_definitions ~state typ cmt_tbl | Ptyp_poly (string_locs, typ) -> @@ -1883,7 +2016,7 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) string_locs); Doc.dot; Doc.space; - print_typ_expr ~state typ cmt_tbl; + print_typ_expr ?inline_record_definitions ~state typ cmt_tbl; ] | Ptyp_package package_type -> print_package_type ~state ~print_module_keyword_and_parens:true @@ -1933,10 +2066,15 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) in let do_type t = match t.Parsetree.ptyp_desc with - | Ptyp_tuple _ -> print_typ_expr ~state t cmt_tbl + | Ptyp_tuple _ -> + print_typ_expr ?inline_record_definitions ~state t cmt_tbl | _ -> Doc.concat - [Doc.lparen; print_typ_expr ~state t cmt_tbl; Doc.rparen] + [ + Doc.lparen; + print_typ_expr ?inline_record_definitions ~state t cmt_tbl; + Doc.rparen; + ] in let printed_types = List.map do_type types in let cases = @@ -1960,7 +2098,11 @@ and print_typ_expr ?inline_record_definitions ~(state : State.t) if i > 0 then Doc.text "| " else Doc.if_breaks (Doc.text "| ") Doc.nil in - Doc.concat [bar; print_typ_expr ~state core_type cmt_tbl] + Doc.concat + [ + bar; + print_typ_expr ?inline_record_definitions ~state core_type cmt_tbl; + ] in let docs = List.mapi print_row_field row_fields in let cases = Doc.join ~sep:Doc.line docs in @@ -2061,7 +2203,8 @@ and print_object ~state ~inline fields open_flag cmt_tbl = in if inline then doc else Doc.group doc -and print_tuple_type ~state ~inline (types : Parsetree.core_type list) cmt_tbl = +and print_tuple_type ?inline_record_definitions ~state ~inline + (types : Parsetree.core_type list) cmt_tbl = let tuple = Doc.concat [ @@ -2073,7 +2216,9 @@ and print_tuple_type ~state ~inline (types : Parsetree.core_type list) cmt_tbl = Doc.join ~sep:(Doc.concat [Doc.comma; Doc.line]) (List.map - (fun typexpr -> print_typ_expr ~state typexpr cmt_tbl) + (fun typexpr -> + print_typ_expr ?inline_record_definitions ~state typexpr + cmt_tbl) types); ]); Doc.trailing_comma; @@ -2107,7 +2252,8 @@ and print_object_field ~state (field : Parsetree.object_field) cmt_tbl = (* es6 arrow type arg * type t = (~foo: string, ~bar: float=?, unit) => unit * i.e. ~foo: string, ~bar: float *) -and print_type_parameter ~state {attrs; lbl; typ} cmt_tbl = +and print_type_parameter ?inline_record_definitions ~state {attrs; lbl; typ} + cmt_tbl = (* Converting .ml code to .res requires processing uncurried attributes *) let attrs = print_attributes ~state attrs cmt_tbl in let label = @@ -2127,7 +2273,12 @@ and print_type_parameter ~state {attrs; lbl; typ} cmt_tbl = let doc = Doc.group (Doc.concat - [attrs; label; print_typ_expr ~state typ cmt_tbl; optional_indicator]) + [ + attrs; + label; + print_typ_expr ?inline_record_definitions ~state typ cmt_tbl; + optional_indicator; + ]) in print_comments doc cmt_tbl loc diff --git a/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecord.res.txt b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecord.res.txt new file mode 100644 index 00000000000..49d5b86ddc9 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecord.res.txt @@ -0,0 +1,11 @@ +type f1.a = { + x: int }[@@res.inlineRecordDefinition ] +external f1 : a:f1.a -> unit (a:1) = "f1" +type f2.return.type = { + id: string }[@@res.inlineRecordDefinition ] +external f2 : int -> f2.return.type (a:1) = "f2" +type f3.return.type = { + b: int }[@@res.inlineRecordDefinition ] +and f3.returnType = { + a: int }[@@res.inlineRecordDefinition ] +external f3 : returnType:f3.returnType -> f3.return.type (a:1) = "f3" \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordAnonymous.res.txt b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordAnonymous.res.txt new file mode 100644 index 00000000000..1d5ebf45475 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordAnonymous.res.txt @@ -0,0 +1,18 @@ +type f1.arg0 = { + x: int }[@@res.inlineRecordDefinition ] +external f1 : f1.arg0 -> unit (a:1) = "f1" +type f2.arg0 = { + x: int }[@@res.inlineRecordDefinition ] +external f2 : f2.arg0 -> string -> unit (a:2) = "f2" +type f3.return.type = { + done: bool }[@@res.inlineRecordDefinition ] +and f3.arg1 = { + y: string }[@@res.inlineRecordDefinition ] +and f3.arg0 = { + x: int }[@@res.inlineRecordDefinition ] +external f3 : f3.arg0 option -> f3.arg1 -> f3.return.type option (a:2) = "f3" +type f4.arg1 = { + y: string }[@@res.inlineRecordDefinition ] +and f4.named = { + x: int }[@@res.inlineRecordDefinition ] +external f4 : named:f4.named -> f4.arg1 -> unit (a:2) = "f4" \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordCollision.res.txt b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordCollision.res.txt new file mode 100644 index 00000000000..2e7ceb66a72 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordCollision.res.txt @@ -0,0 +1,4 @@ +type nonrec f = string +type f.arg0 = { + z: int }[@@res.inlineRecordDefinition ] +external f : f.arg0 -> unit (a:1) = "f" \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/structure/expected/inlineRecordCollision.res.txt b/tests/syntax_tests/data/parsing/grammar/structure/expected/inlineRecordCollision.res.txt new file mode 100644 index 00000000000..31d0b2297ee --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/expected/inlineRecordCollision.res.txt @@ -0,0 +1,5 @@ +type nonrec foo = string +type t.foo = { + x: int }[@@res.inlineRecordDefinition ] +and t = { + foo: t.foo } \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecord.res b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecord.res new file mode 100644 index 00000000000..2e01e6a7cac --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecord.res @@ -0,0 +1,4 @@ +external f1: (~a: {x: int}) => unit = "f1" +external f2: int => {id: string} = "f2" + +external f3: (~returnType: {a:int}) => {b:int} = "f3" diff --git a/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordAnonymous.res b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordAnonymous.res new file mode 100644 index 00000000000..cae7f645eae --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordAnonymous.res @@ -0,0 +1,4 @@ +external f1: {x: int} => unit = "f1" +external f2: ({x: int}, string) => unit = "f2" +external f3: (option<{x: int}>, {y: string}) => option<{done: bool}> = "f3" +external f4: (~named: {x: int}, {y: string}) => unit = "f4" diff --git a/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordCollision.res b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordCollision.res new file mode 100644 index 00000000000..c7bd2741ff6 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordCollision.res @@ -0,0 +1,2 @@ +type f = string +external f: {z: int} => unit = "f" diff --git a/tests/syntax_tests/data/parsing/grammar/structure/inlineRecordCollision.res b/tests/syntax_tests/data/parsing/grammar/structure/inlineRecordCollision.res new file mode 100644 index 00000000000..46cdd573286 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/inlineRecordCollision.res @@ -0,0 +1,2 @@ +type foo = string +type t = {foo: {x: int}} diff --git a/tests/syntax_tests/data/printer/signature/expected/externalInlineRecord.resi.txt b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecord.resi.txt new file mode 100644 index 00000000000..1c3704437a1 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecord.resi.txt @@ -0,0 +1,11 @@ +@module("node:fs") +external readFileSync: ( + string, + ~options: { + encoding?: [#utf8 | #ascii | #base64], + flag?: string, + misc?: { + mode?: int, + }, + }, +) => option<{filename: string, size: string}> = "fs.readFileSync" diff --git a/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordAnonymous.resi.txt b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordAnonymous.resi.txt new file mode 100644 index 00000000000..cae7f645eae --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordAnonymous.resi.txt @@ -0,0 +1,4 @@ +external f1: {x: int} => unit = "f1" +external f2: ({x: int}, string) => unit = "f2" +external f3: (option<{x: int}>, {y: string}) => option<{done: bool}> = "f3" +external f4: (~named: {x: int}, {y: string}) => unit = "f4" diff --git a/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordBareArrow.resi.txt b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordBareArrow.resi.txt new file mode 100644 index 00000000000..9118f3b79d0 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordBareArrow.resi.txt @@ -0,0 +1 @@ +external getMeta: int => {id: string} = "getMeta" diff --git a/tests/syntax_tests/data/printer/signature/expected/externalObjectSpread.resi.txt b/tests/syntax_tests/data/printer/signature/expected/externalObjectSpread.resi.txt new file mode 100644 index 00000000000..3e3e77034b8 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/expected/externalObjectSpread.resi.txt @@ -0,0 +1,3 @@ +type user = {"name": string} +@val +external steve: {...user, "age": int} = "steve" diff --git a/tests/syntax_tests/data/printer/signature/externalInlineRecord.resi b/tests/syntax_tests/data/printer/signature/externalInlineRecord.resi new file mode 100644 index 00000000000..f71df8a2ce9 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/externalInlineRecord.resi @@ -0,0 +1,9 @@ +@module("node:fs") +external readFileSync: (string, ~options: { + encoding?: [#utf8 | #ascii | #base64], + flag?: string, + misc?: { + mode?: int + } +}) => option<{filename: string, size: string}> = "fs.readFileSync" + diff --git a/tests/syntax_tests/data/printer/signature/externalInlineRecordAnonymous.resi b/tests/syntax_tests/data/printer/signature/externalInlineRecordAnonymous.resi new file mode 100644 index 00000000000..cae7f645eae --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/externalInlineRecordAnonymous.resi @@ -0,0 +1,4 @@ +external f1: {x: int} => unit = "f1" +external f2: ({x: int}, string) => unit = "f2" +external f3: (option<{x: int}>, {y: string}) => option<{done: bool}> = "f3" +external f4: (~named: {x: int}, {y: string}) => unit = "f4" diff --git a/tests/syntax_tests/data/printer/signature/externalInlineRecordBareArrow.resi b/tests/syntax_tests/data/printer/signature/externalInlineRecordBareArrow.resi new file mode 100644 index 00000000000..abd8d0ee712 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/externalInlineRecordBareArrow.resi @@ -0,0 +1,2 @@ +external getMeta: int => {id: string} = "getMeta" + diff --git a/tests/syntax_tests/data/printer/signature/externalObjectSpread.resi b/tests/syntax_tests/data/printer/signature/externalObjectSpread.resi new file mode 100644 index 00000000000..8635b865914 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/externalObjectSpread.resi @@ -0,0 +1,4 @@ +type user = {"name": string} +@val +external steve: {...user, "age": int} = "steve" + diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecord.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecord.res.txt new file mode 100644 index 00000000000..1c3704437a1 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecord.res.txt @@ -0,0 +1,11 @@ +@module("node:fs") +external readFileSync: ( + string, + ~options: { + encoding?: [#utf8 | #ascii | #base64], + flag?: string, + misc?: { + mode?: int, + }, + }, +) => option<{filename: string, size: string}> = "fs.readFileSync" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAll.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAll.res.txt new file mode 100644 index 00000000000..35e4deb1d96 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAll.res.txt @@ -0,0 +1,23 @@ +@module("m1") +external f1: (~a: {x: int}, ~b: {y: string}) => unit = "f1" + +@module("m2") +external f2: (~opt: {z: int}=?) => unit = "f2" + +@module("m3") +external f3: ( + ~options: { + misc?: { + details: { + n: int, + }, + }, + }, +) => unit = "f3" + +@module("m4") +external f4: int => {id2: string} = "f4" + +// Non-arrow external should not derive inline records +@val +external s1: {...user, "age": int} = "s1" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAnonymous.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAnonymous.res.txt new file mode 100644 index 00000000000..eec77a7ecf9 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordAnonymous.res.txt @@ -0,0 +1,7 @@ +external f1: {x: int} => unit = "f1" +external f2: ({x: int}, string) => unit = "f2" +external f3: (option<{x: int}>, {y: string}) => option<{done: bool}> = "f3" +external f4: (~named: {x: int}, {y: string}) => unit = "f4" + +type f = string +external f: {z: int} => unit = "f" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordBareArrow.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordBareArrow.res.txt new file mode 100644 index 00000000000..9118f3b79d0 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordBareArrow.res.txt @@ -0,0 +1 @@ +external getMeta: int => {id: string} = "getMeta" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordComments.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordComments.res.txt new file mode 100644 index 00000000000..bdd55f99e39 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordComments.res.txt @@ -0,0 +1,3 @@ +// preserved +@module("m") +external read: (~options: {flag?: string}) => unit = "read" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalObjectSpread.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalObjectSpread.res.txt new file mode 100644 index 00000000000..3e3e77034b8 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalObjectSpread.res.txt @@ -0,0 +1,3 @@ +type user = {"name": string} +@val +external steve: {...user, "age": int} = "steve" diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecord.res b/tests/syntax_tests/data/printer/structure/externalInlineRecord.res new file mode 100644 index 00000000000..f71df8a2ce9 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecord.res @@ -0,0 +1,9 @@ +@module("node:fs") +external readFileSync: (string, ~options: { + encoding?: [#utf8 | #ascii | #base64], + flag?: string, + misc?: { + mode?: int + } +}) => option<{filename: string, size: string}> = "fs.readFileSync" + diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecordAll.res b/tests/syntax_tests/data/printer/structure/externalInlineRecordAll.res new file mode 100644 index 00000000000..060c5d87233 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecordAll.res @@ -0,0 +1,21 @@ +@module("m1") +external f1: (~a: {x: int}, ~b: {y: string}) => unit = "f1" + +@module("m2") +external f2: (~opt: {z: int}=?) => unit = "f2" + +@module("m3") +external f3: (~options: { + misc?: { + details: { + n: int + } + } +}) => unit = "f3" + +@module("m4") +external f4: int => {id2: string} = "f4" + +// Non-arrow external should not derive inline records +@val +external s1: {...user, "age": int} = "s1" diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecordAnonymous.res b/tests/syntax_tests/data/printer/structure/externalInlineRecordAnonymous.res new file mode 100644 index 00000000000..eec77a7ecf9 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecordAnonymous.res @@ -0,0 +1,7 @@ +external f1: {x: int} => unit = "f1" +external f2: ({x: int}, string) => unit = "f2" +external f3: (option<{x: int}>, {y: string}) => option<{done: bool}> = "f3" +external f4: (~named: {x: int}, {y: string}) => unit = "f4" + +type f = string +external f: {z: int} => unit = "f" diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecordBareArrow.res b/tests/syntax_tests/data/printer/structure/externalInlineRecordBareArrow.res new file mode 100644 index 00000000000..9118f3b79d0 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecordBareArrow.res @@ -0,0 +1 @@ +external getMeta: int => {id: string} = "getMeta" diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecordComments.res b/tests/syntax_tests/data/printer/structure/externalInlineRecordComments.res new file mode 100644 index 00000000000..bdd55f99e39 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecordComments.res @@ -0,0 +1,3 @@ +// preserved +@module("m") +external read: (~options: {flag?: string}) => unit = "read" diff --git a/tests/syntax_tests/data/printer/structure/externalObjectSpread.res b/tests/syntax_tests/data/printer/structure/externalObjectSpread.res new file mode 100644 index 00000000000..8635b865914 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalObjectSpread.res @@ -0,0 +1,4 @@ +type user = {"name": string} +@val +external steve: {...user, "age": int} = "steve" + diff --git a/tests/syntax_tests/data/printer/typeDef/expected/inlineRecordCollision.res.txt b/tests/syntax_tests/data/printer/typeDef/expected/inlineRecordCollision.res.txt new file mode 100644 index 00000000000..46cdd573286 --- /dev/null +++ b/tests/syntax_tests/data/printer/typeDef/expected/inlineRecordCollision.res.txt @@ -0,0 +1,2 @@ +type foo = string +type t = {foo: {x: int}} diff --git a/tests/syntax_tests/data/printer/typeDef/inlineRecordCollision.res b/tests/syntax_tests/data/printer/typeDef/inlineRecordCollision.res new file mode 100644 index 00000000000..46cdd573286 --- /dev/null +++ b/tests/syntax_tests/data/printer/typeDef/inlineRecordCollision.res @@ -0,0 +1,2 @@ +type foo = string +type t = {foo: {x: int}} From 6e96d33cd34e78423f20efea99df58b6624efd9e Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 18 Mar 2026 11:34:31 +0100 Subject: [PATCH 2/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3126ba3d180..714861587df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Reanalyze: add glob pattern support for suppress/unsuppress configurations (e.g., `"src/generated/**"`). https://github.com/rescript-lang/rescript/pull/8277 - Add optional `~locales` and `~options` parameters to `String.localeCompare`. https://github.com/rescript-lang/rescript/pull/8287 +- Support inline records in external definitions. https://github.com/rescript-lang/rescript/pull/8304 #### :bug: Bug fix From d2d95f3a725d7fa49f05eca0a697607948cf7943 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 18 Mar 2026 12:52:24 +0100 Subject: [PATCH 3/3] review fixes --- compiler/syntax/src/res_core.ml | 10 ++++++++-- .../expected/externalInlineRecordNestedArrow.res.txt | 5 +++++ .../expected/externalInlineRecordNonArrow.res.txt | 2 ++ .../structure/externalInlineRecordNestedArrow.res | 1 + .../grammar/structure/externalInlineRecordNonArrow.res | 5 +++++ .../expected/externalInlineRecordNestedArrow.resi.txt | 1 + .../signature/externalInlineRecordNestedArrow.resi | 1 + .../expected/externalInlineRecordNestedArrow.res.txt | 1 + .../expected/externalInlineRecordNonArrow.res.txt | 7 +++++++ .../structure/externalInlineRecordNestedArrow.res | 1 + .../printer/structure/externalInlineRecordNonArrow.res | 5 +++++ 11 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNestedArrow.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNonArrow.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNestedArrow.res create mode 100644 tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNonArrow.res create mode 100644 tests/syntax_tests/data/printer/signature/expected/externalInlineRecordNestedArrow.resi.txt create mode 100644 tests/syntax_tests/data/printer/signature/externalInlineRecordNestedArrow.resi create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNestedArrow.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNonArrow.res.txt create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecordNestedArrow.res create mode 100644 tests/syntax_tests/data/printer/structure/externalInlineRecordNonArrow.res diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index 05bcf981da1..366c444effc 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -4716,7 +4716,11 @@ and parse_type_parameter ?current_type_name_path ?inline_types_context ~attrs constr args in - let typ = parse_arrow_type_rest ~es6_arrow:true ~start_pos typ p in + let typ = + parse_arrow_type_rest + ?current_type_name_path:positional_type_name_path + ?inline_types_context ~es6_arrow:true ~start_pos typ p + in let typ = parse_type_alias p typ in Some {attrs = []; label = Nolabel; typ; start_pos}) | _ -> @@ -4945,7 +4949,9 @@ and parse_external_type_expr ~current_type_name_path ~inline_types_context p = ~es6_arrow:true ~start_pos typ p in parse_type_alias p typ - else parse_typ_expr ?current_type_name_path ~inline_types_context p + else if is_es6_arrow_type p then + parse_typ_expr ?current_type_name_path ~inline_types_context p + else parse_typ_expr p and parse_tuple_type ~attrs ~first ~start_pos p = let typexprs = diff --git a/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNestedArrow.res.txt b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNestedArrow.res.txt new file mode 100644 index 00000000000..7be54399246 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNestedArrow.res.txt @@ -0,0 +1,5 @@ +type withTransform.arg0.return.type = { + id: string }[@@res.inlineRecordDefinition ] +external withTransform : + (int -> withTransform.arg0.return.type (a:1)) -> unit (a:1) = + "withTransform" \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNonArrow.res.txt b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNonArrow.res.txt new file mode 100644 index 00000000000..e35f0e46874 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/expected/externalInlineRecordNonArrow.res.txt @@ -0,0 +1,2 @@ +external defaults : < x: int > = "defaults"[@@val ] +external maybeDefaults : < x: int > option = "defaults"[@@val ] \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNestedArrow.res b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNestedArrow.res new file mode 100644 index 00000000000..6649d10ac84 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNestedArrow.res @@ -0,0 +1 @@ +external withTransform: (int => {id: string}) => unit = "withTransform" diff --git a/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNonArrow.res b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNonArrow.res new file mode 100644 index 00000000000..cb09664c532 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/structure/externalInlineRecordNonArrow.res @@ -0,0 +1,5 @@ +@val +external defaults: {x: int} = "defaults" + +@val +external maybeDefaults: option<{x: int}> = "defaults" diff --git a/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordNestedArrow.resi.txt b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordNestedArrow.resi.txt new file mode 100644 index 00000000000..6649d10ac84 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/expected/externalInlineRecordNestedArrow.resi.txt @@ -0,0 +1 @@ +external withTransform: (int => {id: string}) => unit = "withTransform" diff --git a/tests/syntax_tests/data/printer/signature/externalInlineRecordNestedArrow.resi b/tests/syntax_tests/data/printer/signature/externalInlineRecordNestedArrow.resi new file mode 100644 index 00000000000..6649d10ac84 --- /dev/null +++ b/tests/syntax_tests/data/printer/signature/externalInlineRecordNestedArrow.resi @@ -0,0 +1 @@ +external withTransform: (int => {id: string}) => unit = "withTransform" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNestedArrow.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNestedArrow.res.txt new file mode 100644 index 00000000000..6649d10ac84 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNestedArrow.res.txt @@ -0,0 +1 @@ +external withTransform: (int => {id: string}) => unit = "withTransform" diff --git a/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNonArrow.res.txt b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNonArrow.res.txt new file mode 100644 index 00000000000..ee9c70bffde --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/expected/externalInlineRecordNonArrow.res.txt @@ -0,0 +1,7 @@ +@val +external defaults: {"x": int} = "defaults" + +@val +external maybeDefaults: option<{ + "x": int, +}> = "defaults" diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecordNestedArrow.res b/tests/syntax_tests/data/printer/structure/externalInlineRecordNestedArrow.res new file mode 100644 index 00000000000..6649d10ac84 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecordNestedArrow.res @@ -0,0 +1 @@ +external withTransform: (int => {id: string}) => unit = "withTransform" diff --git a/tests/syntax_tests/data/printer/structure/externalInlineRecordNonArrow.res b/tests/syntax_tests/data/printer/structure/externalInlineRecordNonArrow.res new file mode 100644 index 00000000000..cb09664c532 --- /dev/null +++ b/tests/syntax_tests/data/printer/structure/externalInlineRecordNonArrow.res @@ -0,0 +1,5 @@ +@val +external defaults: {x: int} = "defaults" + +@val +external maybeDefaults: option<{x: int}> = "defaults"