diff --git a/CHANGES.md b/CHANGES.md index 6cb5bf928b..f6abf2df86 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,6 +7,7 @@ - Support OxCaml 5.2.0minus39 (@jonludlam, #1469) - Support for OxCaml modes (@art-w, #1454) - Fix OxCaml with-bounds for arbitrary types (@art-w, #1466) +- Support for OxCaml parameterized libraries (@art-w, #1464) ### Fixed - Remove requirement for ppx_expect in tests (@jonludlam, #1445) diff --git a/src/document/doctree.ml b/src/document/doctree.ml index 02eb782266..81ee5ad05e 100644 --- a/src/document/doctree.ml +++ b/src/document/doctree.ml @@ -322,14 +322,15 @@ module PageTitle : sig val render_src_title : Source_page.t -> Item.t list end = struct - let format_title ~source_anchor kind name preamble = - let mk title = - let level = 0 and label = None in - [ Types.Item.Heading { level; label; title; source_anchor } ] - in - let prefix s = - mk (Types.inline (Text (s ^ " ")) :: Codefmt.code (Codefmt.txt name)) + let prefixed_title ~source_anchor name s = + let level = 0 and label = None in + let title = + Types.inline (Text (s ^ " ")) :: Codefmt.code (Codefmt.txt name) in + [ Types.Item.Heading { level; label; title; source_anchor } ] + + let format_title ~source_anchor kind name preamble = + let prefix = prefixed_title ~source_anchor name in match kind with | `Module -> (prefix "Module", preamble) | `Parameter _ -> (prefix "Parameter", preamble) @@ -349,9 +350,10 @@ end = struct | Some p -> Printf.sprintf "%s.%s" p.name name let render_title ?source_anchor (p : Page.t) = - format_title ~source_anchor p.url.kind - (make_name_from_path p.url) - p.preamble + let name = make_name_from_path p.url in + if p.library_parameter then + (prefixed_title ~source_anchor name "Library parameter", p.preamble) + else format_title ~source_anchor p.url.kind name p.preamble let render_src_title (p : Source_page.t) = format_title ~source_anchor:None p.url.kind (make_name_from_path p.url) [] diff --git a/src/document/generator.ml b/src/document/generator.ml index f8efd42ef8..d7abc0d4f9 100644 --- a/src/document/generator.ml +++ b/src/document/generator.ml @@ -84,10 +84,11 @@ let prepare_preamble comment items = in (Comment.standalone preamble, Comment.standalone first_comment @ items) -let make_expansion_page ~source_anchor url comments items = +let make_expansion_page ?(library_parameter = false) ~source_anchor url comments + items = let comment = List.concat comments in let preamble, items = prepare_preamble comment items in - { Page.preamble; items; url; source_anchor } + { Page.preamble; items; url; source_anchor; library_parameter } include Generator_signatures @@ -95,12 +96,39 @@ module Make (Syntax : SYNTAX) = struct module Link : sig val from_path : Paths.Path.t -> text + val href_of_path : Paths.Path.t -> Url.t option + val from_fragment : Paths.Fragment.leaf -> text val render_fragment_any : Paths.Fragment.t -> string end = struct open Paths + let href_of_resolved rp = + (* If the path is pointing to an opaque module or module type + there won't be a page generated - so we stop before; at + the parent page, and link instead to the anchor representing + the declaration of the opaque module(_type) *) + let stop_before = + match rp with + | `OpaqueModule _ | `OpaqueModuleType _ -> true + | _ -> false + in + match Paths.Path.Resolved.identifier rp with + | Some id -> Some (Url.from_identifier ~stop_before id) + | None -> None + + let rec href_of_path : Path.t -> Url.t option = + fun path -> + match path with + | `Substituted m -> href_of_path (m :> Path.t) + | `SubstitutedMT m -> href_of_path (m :> Path.t) + | `SubstitutedT m -> href_of_path (m :> Path.t) + | `SubstitutedCT m -> href_of_path (m :> Path.t) + | `Resolved _ when Paths.Path.is_hidden path -> None + | `Resolved rp -> href_of_resolved rp + | _ -> None + let rec from_path : Path.t -> text = fun path -> match path with @@ -129,24 +157,18 @@ module Make (Syntax : SYNTAX) = struct let link1 = from_path (p1 :> Path.t) in let link2 = from_path (p2 :> Path.t) in link1 ++ O.txt "(" ++ link2 ++ O.txt ")" + | `ApplyParam (p1, p2, p3) -> + let link1 = from_path (p1 :> Path.t) in + let link2 = from_path (p2 :> Path.t) in + let link3 = from_path (p3 :> Path.t) in + link1 ++ O.txt "[" ++ link2 ++ O.txt ":" ++ link3 ++ O.txt "]" | `Resolved _ when Paths.Path.is_hidden path -> let txt = Url.render_path path in unresolved [ inline @@ Text txt ] | `Resolved rp -> ( - (* If the path is pointing to an opaque module or module type - there won't be a page generated - so we stop before; at - the parent page, and link instead to the anchor representing - the declaration of the opaque module(_type) *) - let stop_before = - match rp with - | `OpaqueModule _ | `OpaqueModuleType _ -> true - | _ -> false - in let txt = [ inline @@ Text (Url.render_path path) ] in - match Paths.Path.Resolved.identifier rp with - | Some id -> - let href = Url.from_identifier ~stop_before id in - resolved href txt + match href_of_resolved rp with + | Some href -> resolved href txt | None -> O.elt txt) let dot prefix suffix = prefix ^ "." ^ suffix @@ -1978,6 +2000,63 @@ module Make (Syntax : SYNTAX) = struct in List.map f t + let parameterisation_items + (p : Odoc_model.Lang.Compilation_unit.Parameterisation.t) = + let text s : Inline.t = [ inline (Inline.Text s) ] in + let link (path : Paths.Path.Module.t) : Inline.t = + let path = (path :> Paths.Path.t) in + let content = O.code (O.txt (Url.render_path path)) in + match Link.href_of_path path with + | Some href -> + [ + inline + (Inline.Link + { + target = Internal (Resolved href); + content; + tooltip = None; + }); + ] + | None -> content + in + let para parts = + Item.Text [ block (Block.Paragraph (List.concat parts)) ] + in + let implements = + match p.argument_for with + | None -> [] + | Some path -> + [ + para + [ + text "Implements the library parameter "; link path; text "."; + ]; + ] + in + let parameters = + match p.parameters with + | [] -> [] + | parameters -> + let decl_of_parameter (path : Paths.Path.Module.t) = + let path = (path :> Paths.Path.t) in + let content = + O.documentedSrc (O.keyword "parameter " ++ Link.from_path path) + in + Item.Declaration + { + content; + anchor = None; + attr = [ "parameter" ]; + doc = []; + source_anchor = None; + } + in + mk_heading ~label:"library-parameters" "Library parameters" + :: List.map decl_of_parameter parameters + @ [ mk_heading ~label:"signature" "Signature" ] + in + implements @ parameters + let compilation_unit (t : Odoc_model.Lang.Compilation_unit.t) = let url = Url.Path.from_identifier t.id in let unit_doc, items = @@ -1985,8 +2064,12 @@ module Make (Syntax : SYNTAX) = struct | Module sign -> signature sign | Pack packed -> ([], pack packed) in + let items = parameterisation_items t.parameterisation @ items in let source_anchor = source_anchor t.source_loc in - let page = make_expansion_page ~source_anchor url [ unit_doc ] items in + let page = + make_expansion_page ~library_parameter:t.parameterisation.is_parameter + ~source_anchor url [ unit_doc ] items + in Document.Page page let page (t : Odoc_model.Lang.Page.t) = @@ -1997,7 +2080,8 @@ module Make (Syntax : SYNTAX) = struct let url = Url.Path.from_identifier t.name in let preamble, items = Sectioning.docs t.content.elements in let source_anchor = None in - Document.Page { Page.preamble; items; url; source_anchor } + Document.Page + { Page.preamble; items; url; source_anchor; library_parameter = false } let implementation (v : Odoc_model.Lang.Implementation.t) syntax_info source_code = diff --git a/src/document/types.ml b/src/document/types.ml index 0a4a4d42e5..06460e632a 100644 --- a/src/document/types.ml +++ b/src/document/types.ml @@ -185,6 +185,7 @@ and Page : sig source_anchor : Url.t option; (** Url to the corresponding source code. Might be a whole source file or a sub part. *) + library_parameter : bool; } end = Page diff --git a/src/document/url.ml b/src/document/url.ml index 2b982cf14e..43a60e5289 100644 --- a/src/document/url.ml +++ b/src/document/url.ml @@ -58,6 +58,13 @@ let render_path : Path.t -> string = | `DotV (p, s) -> dot p (ValueName.to_string s) | `Apply (p1, p2) -> render_path (p1 :> Path.t) ^ "(" ^ render_path (p2 :> Path.t) ^ ")" + | `ApplyParam (p1, p2, p3) -> + render_path (p1 :> Path.t) + ^ "[" + ^ render_path (p2 :> Path.t) + ^ ":" + ^ render_path (p3 :> Path.t) + ^ "]" | `Resolved rp -> render_resolved rp | `Substituted m -> render_path (m :> Path.t) | `SubstitutedMT m -> render_path (m :> Path.t) diff --git a/src/loader/ident_env.ml b/src/loader/ident_env.ml index b52647e6a4..5b43df0c7a 100644 --- a/src/loader/ident_env.ml +++ b/src/loader/ident_env.ml @@ -709,8 +709,31 @@ let is_shadowed List.mem id env.shadowed module Path = struct +let module_of_id id = `Root (ModuleName.of_ident id) + +#if defined OXCAML + let rec read_global_name (n : Global_module.Name.t) : Paths.Path.Module.t = + (* OxCaml parameterized library application "Lib[Param:Impl][P2:I2]" *) + let base = `Root (ModuleName.make_std n.head) in + List.fold_left + (fun acc (arg : Global_module.Name.argument) -> + let param = + `Root + (ModuleName.make_std + (Global_module.Parameter_name.to_string arg.param)) + in + `ApplyParam (acc, param, read_global_name arg.value)) + base n.args + + let module_of_id id = + match Ident.to_global id with + | Some ({ Global_module.Name.args = _ :: _; _ } as n) -> read_global_name n + | _ -> module_of_id id +#endif + let read_module_ident env id = - if ident_is_global_or_predef id then `Root (ModuleName.of_ident id) + if ident_is_global_or_predef id then + module_of_id id else try find_module env id with Not_found -> assert false diff --git a/src/loader/odoc_loader.ml b/src/loader/odoc_loader.ml index 002e2d0489..f491a09143 100644 --- a/src/loader/odoc_loader.ml +++ b/src/loader/odoc_loader.ml @@ -42,6 +42,45 @@ exception Not_an_interface exception Make_root_error of string +let no_parameterisation = + { + Odoc_model.Lang.Compilation_unit.Parameterisation.is_parameter = false; + parameters = []; + argument_for = None; + } + +#if defined OXCAML +let parameterisation_of_cmi ~cmi_kind ~cmi_params = + let root name = `Root (Odoc_model.Names.ModuleName.make_std name) in + let param p = root (Global_module.Parameter_name.to_string p) in + let is_parameter, argument_for = + match (cmi_kind : Cmi_format.kind) with + | Parameter -> (true, None) + | Normal { cmi_arg_for; _ } -> (false, Option.map param cmi_arg_for) + in + { + Odoc_model.Lang.Compilation_unit.Parameterisation.is_parameter; + parameters = List.map param cmi_params; + argument_for; + } + +let read_cmt_and_parameterisation filename = + let cmi, cmt = Cmt_format.read filename in + let parameterisation = + match cmi with + | Some cmi -> + parameterisation_of_cmi ~cmi_kind:cmi.Cmi_format.cmi_kind + ~cmi_params:cmi.Cmi_format.cmi_params + | None -> no_parameterisation + in + match cmt with + | Some cmt -> (parameterisation, cmt) + | None -> raise (Cmt_format.Error (Cmt_format.Not_a_typedtree filename)) +#else +let read_cmt_and_parameterisation filename = + (no_parameterisation, Cmt_format.read_cmt filename) +#endif + let read_cmt_infos source_id ~filename root digest imports () = match Cmt_format.read_cmt filename with | exception Cmi_format.Error _ -> raise Corrupted @@ -56,7 +95,7 @@ let read_cmt_infos source_id ~filename root digest imports () = | _ -> raise Not_an_implementation) let make_compilation_unit ~make_root ~imports ~interface ?sourcefile ~name ~id - ?canonical content = + ?canonical ?(parameterisation = no_parameterisation) content = let open Odoc_model.Lang.Compilation_unit in let interface, digest = match interface with @@ -93,13 +132,14 @@ let make_compilation_unit ~make_root ~imports ~interface ?sourcefile ~name ~id linked = false; canonical; source_loc = None; + parameterisation; } let compilation_unit_of_sig ~make_root ~imports ~interface ?sourcefile ~name ~id - ?canonical sg = + ?canonical ?parameterisation sg = let content = Odoc_model.Lang.Compilation_unit.Module sg in make_compilation_unit ~make_root ~imports ~interface ?sourcefile ~name ~id - ?canonical content + ?canonical ?parameterisation content #if defined OXCAML let unit_name_as_string = Compilation_unit.name_as_string @@ -110,7 +150,7 @@ let name_to_string x = x #endif let read_cmti ~make_root ~parent ~filename ~warnings_tag () = - let cmt_info = Cmt_format.read_cmt filename in + let parameterisation, cmt_info = read_cmt_and_parameterisation filename in match cmt_info.cmt_annots with | Interface intf -> ( match cmt_info.cmt_interface_digest with @@ -141,15 +181,15 @@ let read_cmti ~make_root ~parent ~filename ~warnings_tag () = #else let imports = cmt_info.cmt_imports in #endif - compilation_unit_of_sig ~make_root ~imports - ~interface ~sourcefile ~name ~id ?canonical sg) + compilation_unit_of_sig ~make_root ~imports ~interface ~sourcefile + ~name ~id ?canonical ~parameterisation sg) | _ -> raise Not_an_interface let read_cmt ~make_root ~parent ~filename ~warnings_tag () = - match Cmt_format.read_cmt filename with + match read_cmt_and_parameterisation filename with | exception Cmi_format.Error (Not_an_interface _) -> raise Not_an_implementation - | cmt_info -> ( + | parameterisation, cmt_info -> ( let name = cmt_info.cmt_modname |> unit_name_as_string in let sourcefile = ( cmt_info.cmt_sourcefile, @@ -213,7 +253,7 @@ let read_cmt ~make_root ~parent ~filename ~warnings_tag () = Cmt.read_implementation parent name ~warnings_tag impl in compilation_unit_of_sig ~make_root ~imports ~interface ~sourcefile - ~name ~id ?canonical sg + ~name ~id ?canonical ~parameterisation sg | _ -> raise Not_an_implementation) #if defined OXCAML @@ -257,8 +297,15 @@ let read_cmi ~make_root ~parent ~filename ~warnings_tag () = compilation_unit_of_import_info info_opt) in let interface = interface |> Option.map snd in + let parameterisation = + parameterisation_of_cmi ~cmi_kind:cmi_info.Cmi_format.cmi_kind + ~cmi_params:cmi_info.Cmi_format.cmi_params + in +#else + let parameterisation = no_parameterisation in #endif - compilation_unit_of_sig ~make_root ~imports ~interface ~name ~id sg + compilation_unit_of_sig ~make_root ~imports ~interface ~name ~id + ~parameterisation sg | _ -> raise Corrupted let read_impl ~make_root ~filename ~source_id () = diff --git a/src/model/lang.ml b/src/model/lang.ml index 9dabb70404..5bc04d93a4 100644 --- a/src/model/lang.ml +++ b/src/model/lang.ml @@ -539,6 +539,14 @@ module rec Compilation_unit : sig type content = Module of Signature.t | Pack of Packed.t + module Parameterisation : sig + type t = { + is_parameter : bool; + parameters : Path.Module.t list; + argument_for : Path.Module.t option; + } + end + type t = { id : Identifier.RootModule.t; root : Root.t; @@ -552,6 +560,7 @@ module rec Compilation_unit : sig linked : bool; (** Whether this unit has been linked. *) source_loc : Identifier.SourceLocation.t option; canonical : Path.Module.t option; + parameterisation : Parameterisation.t; } end = Compilation_unit diff --git a/src/model/paths.ml b/src/model/paths.ml index 408479d8d6..4ac61a19d6 100644 --- a/src/model/paths.ml +++ b/src/model/paths.ml @@ -757,6 +757,10 @@ module Path = struct | `Apply (p1, p2) -> is_path_hidden (p1 : module_ :> any) || is_path_hidden (p2 : module_ :> any) + | `ApplyParam (p1, p2, p3) -> + is_path_hidden (p1 : module_ :> any) + || is_path_hidden (p2 : module_ :> any) + || is_path_hidden (p3 : module_ :> any) module Resolved = struct type t = Paths_types.Resolved_path.any diff --git a/src/model/paths_types.ml b/src/model/paths_types.ml index 197949c687..c0fbe330ef 100644 --- a/src/model/paths_types.ml +++ b/src/model/paths_types.ml @@ -333,7 +333,8 @@ module rec Path : sig | `Root of ModuleName.t | `Forward of string | `Dot of module_ * ModuleName.t - | `Apply of module_ * module_ ] + | `Apply of module_ * module_ + | `ApplyParam of module_ * module_ * module_ ] (** @canonical Odoc_model.Paths.Path.Module.t *) type module_type = @@ -378,6 +379,7 @@ module rec Path : sig | `DotMT of module_ * ModuleTypeName.t | `DotV of module_ * ValueName.t | `Apply of module_ * module_ + | `ApplyParam of module_ * module_ * module_ | `Unbox of type_ ] (** @canonical Odoc_model.Paths.Path.t *) end = diff --git a/src/model_desc/lang_desc.ml b/src/model_desc/lang_desc.ml index f09fad15bb..64d7f08e37 100644 --- a/src/model_desc/lang_desc.ml +++ b/src/model_desc/lang_desc.ml @@ -747,6 +747,18 @@ and compilation_unit_content = | Module x -> C ("Module", x, signature_t) | Pack x -> C ("Pack", x, compilation_unit_packed)) +and compilation_unit_parameterisation = + let open Lang.Compilation_unit.Parameterisation in + Record + [ + F ("is_parameter", (fun t -> t.is_parameter), bool); + F ("parameters", (fun t -> (t.parameters :> Paths.Path.t list)), List path); + F + ( "argument_for", + (fun t -> (t.argument_for :> Paths.Path.t option)), + Option path ); + ] + and compilation_unit_t = let open Lang.Compilation_unit in Record @@ -764,6 +776,10 @@ and compilation_unit_t = ( "canonical", (fun t -> (t.canonical :> Paths.Path.t option)), Option path ); + F + ( "parameterisation", + (fun t -> t.parameterisation), + compilation_unit_parameterisation ); ] (** {3 Page} *) diff --git a/src/model_desc/paths_desc.ml b/src/model_desc/paths_desc.ml index 5082f02206..922171c563 100644 --- a/src/model_desc/paths_desc.ml +++ b/src/model_desc/paths_desc.ml @@ -231,6 +231,11 @@ module General_paths = struct C ("`DotV", ((x1 :> p), x2), Pair (path, Names.valuename)) | `Apply (x1, x2) -> C ("`Apply", ((x1 :> p), (x2 :> p)), Pair (path, path)) + | `ApplyParam (x1, x2, x3) -> + C + ( "`ApplyParam", + ((x1 :> p), (x2 :> p), (x3 :> p)), + Triple (path, path, path) ) | `Substituted m -> C ("`Substituted", (m :> p), path) | `SubstitutedMT m -> C ("`SubstitutedMT", (m :> p), path) | `SubstitutedT m -> C ("`SubstitutedT", (m :> p), path) diff --git a/src/xref2/component.ml b/src/xref2/component.ml index f132b56729..7994110f1a 100644 --- a/src/xref2/component.ml +++ b/src/xref2/component.ml @@ -1264,6 +1264,9 @@ module Fmt = struct Format.fprintf ppf "%a.%a" (resolved_parent_path c) p ModuleName.fmt n | `Apply (p1, p2) -> Format.fprintf ppf "%a(%a)" (module_path c) p1 (module_path c) p2 + | `ApplyParam (i, p, a) -> + Format.fprintf ppf "%a[%a:%a]" (module_path c) i (module_path c) p + (module_path c) a | `Identifier (id, b) -> wrap2 c "identifier" model_identifier bool ppf (id :> id) b | `Local (id, b) -> wrap2 c "local" ident_fmt bool ppf id b @@ -1436,6 +1439,13 @@ module Fmt = struct (func :> path) (model_path c) (arg :> path) + | `ApplyParam (inst, param, arg) -> + Format.fprintf ppf "%a[%a:%a]" (model_path c) + (inst :> path) + (model_path c) + (param :> path) + (model_path c) + (arg :> path) | `Substituted m -> wrap c "substituted" model_path ppf (m :> Odoc_model.Paths.Path.t) | `SubstitutedMT m -> @@ -2114,6 +2124,11 @@ module Of_Lang = struct | `Dot (path', x) -> `Dot (module_path ident_map path', x) | `Apply (p1, p2) -> `Apply (module_path ident_map p1, module_path ident_map p2) + | `ApplyParam (p1, p2, p3) -> + `ApplyParam + ( module_path ident_map p1, + module_path ident_map p2, + module_path ident_map p3 ) | `Forward str -> `Forward str | `Root str -> `Root str diff --git a/src/xref2/cpath.ml b/src/xref2/cpath.ml index e59bbf2a52..790eb2b402 100644 --- a/src/xref2/cpath.ml +++ b/src/xref2/cpath.ml @@ -60,7 +60,8 @@ and Cpath : sig | `Forward of string | `Dot of module_ * ModuleName.t | `Module of Resolved.parent * ModuleName.t (* Like dot, but typed *) - | `Apply of module_ * module_ ] + | `Apply of module_ * module_ + | `ApplyParam of module_ * module_ * module_ ] and module_type = [ `Resolved of Resolved.module_type @@ -147,7 +148,8 @@ let rec is_module_substituted : module_ -> bool = function | `Identifier _ -> false | `Local _ -> false | `Substituted _ -> true - | `Dot (a, _) | `Apply (a, _) -> is_module_substituted a + | `Dot (a, _) | `Apply (a, _) | `ApplyParam (a, _, _) -> + is_module_substituted a | `Forward _ -> false | `Root _ -> false | `Module (a, _) -> is_resolved_parent_substituted a @@ -184,12 +186,14 @@ let rec is_module_forward : module_ -> bool = function | `Root _ -> false | `Identifier _ -> false | `Local _ -> false - | `Substituted p | `Dot (p, _) | `Apply (p, _) -> is_module_forward p + | `Substituted p | `Dot (p, _) | `Apply (p, _) | `ApplyParam (p, _, _) -> + is_module_forward p | `Module (_, _) -> false let rec is_module_hidden : module_ -> bool = function | `Resolved r -> is_resolved_module_hidden ~weak_canonical_test:false r - | `Substituted p | `Dot (p, _) | `Apply (p, _) -> is_module_hidden p + | `Substituted p | `Dot (p, _) | `Apply (p, _) | `ApplyParam (p, _, _) -> + is_module_hidden p | `Identifier (_, b) -> b | `Local (_, b) -> b | `Forward _ -> false @@ -354,6 +358,11 @@ and unresolve_module_path : module_ -> module_ = function | `Dot (p, x) -> `Dot (unresolve_module_path p, x) | `Module (p, x) -> `Dot (unresolve_resolved_parent_path p, x) | `Apply (x, y) -> `Apply (unresolve_module_path x, unresolve_module_path y) + | `ApplyParam (i, p, a) -> + `ApplyParam + ( unresolve_module_path i, + unresolve_module_path p, + unresolve_module_path a ) and unresolve_resolved_module_type_path : Resolved.module_type -> module_type = function diff --git a/src/xref2/errors.ml b/src/xref2/errors.ml index afa9f8f255..2a4b36f4cd 100644 --- a/src/xref2/errors.ml +++ b/src/xref2/errors.ml @@ -41,6 +41,8 @@ module Tools_error = struct `Lookup_failure of Identifier.Path.Module.t (* Could not find the module in the environment *) | `Lookup_failure_root of ModuleName.t (* Could not find the root module *) + | `ParameterizedInstance + (* Instances of parameterized libraries have no expansion *) | `Parent of parent_lookup_error ] and simple_module_type_expr_of_module_error = @@ -209,6 +211,9 @@ module Tools_error = struct Format.fprintf fmt "Lookup failure (value): %a" (model_identifier c) (m :> Odoc_model.Paths.Identifier.t) | `ApplyNotFunctor -> Format.fprintf fmt "Apply module is not a functor" + | `ParameterizedInstance -> + Format.fprintf fmt + "Instance of a parameterized library has no expansion" | `Class_replaced -> Format.fprintf fmt "Class replaced" | `Parent p -> pp fmt (p :> any) | `Parent_sig e -> Format.fprintf fmt "Parent_sig: %a" pp (e :> any) @@ -245,6 +250,10 @@ let rec kind_of_module_cpath = function match kind_of_module_cpath a with | Some _ as a -> a | None -> kind_of_module_cpath b) + | `ApplyParam (a, _param, b) -> ( + match kind_of_module_cpath a with + | Some _ as a -> a + | None -> kind_of_module_cpath b) | _ -> None let rec kind_of_module_type_cpath = function diff --git a/src/xref2/lang_of.ml b/src/xref2/lang_of.ml index 796bad42e3..fa953ff4ce 100644 --- a/src/xref2/lang_of.ml +++ b/src/xref2/lang_of.ml @@ -76,6 +76,8 @@ module Path = struct | `Dot (p, s) -> `Dot (module_ map p, s) | `Forward s -> `Forward s | `Apply (m1, m2) -> `Apply (module_ map m1, module_ map m2) + | `ApplyParam (i, p, a) -> + `ApplyParam (module_ map i, module_ map p, module_ map a) | `Module (`Module p, n) -> `Dot (`Resolved (resolved_module map p), n) | `Module (_, _) -> failwith "Probably shouldn't happen" diff --git a/src/xref2/link.ml b/src/xref2/link.ml index 2620a49065..7107331a99 100644 --- a/src/xref2/link.ml +++ b/src/xref2/link.ml @@ -98,6 +98,7 @@ let rec is_forward : Paths.Path.Module.t -> bool = function | `Identifier _ -> false | `Dot (p, _) -> is_forward p | `Apply (p1, p2) -> is_forward p1 || is_forward p2 + | `ApplyParam (p1, p2, p3) -> is_forward p1 || is_forward p2 || is_forward p3 | `Substituted s -> is_forward s let rec should_reresolve : Paths.Path.Resolved.t -> bool = @@ -157,7 +158,29 @@ let type_path : Env.t -> Paths.Path.Type.t -> Paths.Path.Type.t = Errors.report ~what:(`Type_path cp) ~tools_error:e `Lookup; p) -let value_path : Env.t -> Paths.Path.Value.t -> Paths.Path.Value.t = +let rec is_instance : Paths.Path.Module.t -> bool = function + | `ApplyParam _ -> true + | `Dot (parent, _) -> is_instance parent + | _ -> false + +let rec unfold_alias env (m : Paths.Path.Module.t) : Paths.Path.Module.t = + let alias_target = + let cp = Component.Of_Lang.(module_path (empty ()) m) in + match Tools.resolve_module env cp with + | Ok (_, md) -> ( + match (Component.Delayed.get md).Component.Module.type_ with + | Alias (target, _) -> Some Lang_of.(Path.module_ (empty ()) target) + | ModuleType _ -> None) + | Error _ -> None + in + match alias_target with + | Some target -> target + | None -> ( + match m with + | `Dot (parent, name) -> `Dot (unfold_alias env parent, name) + | _ -> m) + +let rec value_path : Env.t -> Paths.Path.Value.t -> Paths.Path.Value.t = fun env p -> if not (should_resolve (p :> Paths.Path.t)) then p else @@ -171,11 +194,20 @@ let value_path : Env.t -> Paths.Path.Value.t -> Paths.Path.Value.t = | Ok p' -> let result = Tools.reresolve_value env p' in `Resolved Lang_of.(Path.resolved_value (empty ()) result) - | Error e -> - Errors.report ~what:(`Value_path cp) ~tools_error:e `Lookup; - p) - -let class_type_path : Env.t -> Paths.Path.ClassType.t -> Paths.Path.ClassType.t + | Error e -> ( + let instance = + match p with + | `DotV (m, v) -> + instance_prefix env m |> Opt.map (fun m -> `DotV (m, v)) + | _ -> None + in + match instance with + | Some p -> p + | None -> + Errors.report ~what:(`Value_path cp) ~tools_error:e `Lookup; + p)) + +and class_type_path : Env.t -> Paths.Path.ClassType.t -> Paths.Path.ClassType.t = fun env p -> if not (should_resolve (p :> Paths.Path.t)) then p @@ -209,9 +241,19 @@ and module_type_path : | Ok p' -> let result = Tools.reresolve_module_type env p' in `Resolved Lang_of.(Path.resolved_module_type (empty ()) result) - | Error e -> - Errors.report ~what:(`Module_type_path cp) ~tools_error:e `Resolve; - p) + | Error e -> ( + let instance = + match p with + | `DotMT (m, mt) -> + instance_prefix env m |> Opt.map (fun m -> `DotMT (m, mt)) + | _ -> None + in + match instance with + | Some p -> p + | None -> + Errors.report ~what:(`Module_type_path cp) ~tools_error:e + `Resolve; + p)) and module_path : Env.t -> Paths.Path.Module.t -> Paths.Path.Module.t = fun env p -> @@ -228,9 +270,39 @@ and module_path : Env.t -> Paths.Path.Module.t -> Paths.Path.Module.t = let result = Tools.reresolve_module env p' in `Resolved Lang_of.(Path.resolved_module (empty ()) result) | Error _ when is_forward p -> p - | Error e -> - Errors.report ~what:(`Module_path cp) ~tools_error:e `Resolve; - p) + | Error _ when is_instance p -> p + | Error e -> ( + match instance_prefix env p with + | Some p -> p + | None -> + Errors.report ~what:(`Module_path cp) ~tools_error:e `Resolve; + p)) + +and resolve_module_path_parts env (m : Paths.Path.Module.t) : + Paths.Path.Module.t = + match m with + | `Apply (fn, arg) -> + `Apply + (resolve_module_path_parts env fn, resolve_module_path_parts env arg) + | `ApplyParam (inst, param, arg) -> + `ApplyParam + ( resolve_module_path_parts env inst, + resolve_module_path_parts env param, + resolve_module_path_parts env arg ) + | _ -> module_path env m + +and instance_prefix env (m : Paths.Path.Module.t) : Paths.Path.Module.t option = + let unfolded = unfold_alias env m in + if unfolded = m || not (is_instance unfolded) then None + else Some (resolve_module_path_parts env unfolded) + +let resolve_type_path_prefix env (p : Paths.Path.Type.t) : Paths.Path.Type.t = + match p with + | `DotT (m, t) -> ( + match instance_prefix env m with + | Some m -> `DotT (m, t) + | None -> `DotT (resolve_module_path_parts env m, t)) + | _ -> p let rec comment_inline_element : loc:_ -> @@ -502,7 +574,16 @@ let rec unit env t = | Pack _ as p -> p in let source_loc = source_loc env t.id t.source_loc in - { t with content; linked = true; source_loc } + let parameterisation = + let open Compilation_unit.Parameterisation in + let p = t.parameterisation in + { + p with + parameters = List.map (module_path env) p.parameters; + argument_for = Option.map (module_path env) p.argument_for; + } + in + { t with content; linked = true; source_loc; parameterisation } and value_ env parent t = let open Value in @@ -1199,7 +1280,7 @@ and type_expression : Env.t -> Id.Signature.t -> _ -> _ = | Ok (_cp, `FType_removed (_, x, _eq)) -> (* Type variables ? *) Lang_of.(type_expr (empty ()) (parent :> Id.LabelParent.t) x) - | Error _ -> Constr (path', ts)) + | Error _ -> Constr (resolve_type_path_prefix env path', ts)) | Polymorphic_variant v -> Polymorphic_variant (type_expression_polyvar env parent visited v) | Object o -> Object (type_expression_object env parent visited o) diff --git a/src/xref2/shape_tools.cppo.ml b/src/xref2/shape_tools.cppo.ml index 7ddacd386f..22f1ede19d 100644 --- a/src/xref2/shape_tools.cppo.ml +++ b/src/xref2/shape_tools.cppo.ml @@ -81,6 +81,7 @@ let rec shape_of_module_path env : _ -> Shape.t option = >>= fun parent -> shape_of_module_path env (arg :> Odoc_model.Paths.Path.Module.t) >>= fun arg -> Some (Shape.app parent ~arg) + | `ApplyParam _ -> None | `Identifier (id, _) -> shape_of_id env (id :> Odoc_model.Paths.Identifier.NonSrc.t) | `Substituted m -> @@ -109,7 +110,8 @@ let rec shape_of_kind_path env kind : | `Forward _ | `Dot _ | `Root _ - | `Apply _ -> None + | `Apply _ + | `ApplyParam _ -> None module MkId = Identifier.Mk diff --git a/src/xref2/subst.ml b/src/xref2/subst.ml index 5a90ef98cb..5b63926d80 100644 --- a/src/xref2/subst.ml +++ b/src/xref2/subst.ml @@ -278,6 +278,8 @@ and module_path : t -> Cpath.module_ -> Cpath.module_ = | `Dot (p', str) -> `Dot (module_path s p', str) | `Module (p', str) -> `Module (resolved_parent_path s p', str) | `Apply (p1, p2) -> `Apply (module_path s p1, module_path s p2) + | `ApplyParam (i, p, a) -> + `ApplyParam (module_path s i, module_path s p, module_path s a) | `Local (id, b) -> ( match try Some (ModuleMap.find (id :> Ident.module_) s.module_) diff --git a/src/xref2/tools.ml b/src/xref2/tools.ml index 8f3964c584..d5ef062082 100644 --- a/src/xref2/tools.ml +++ b/src/xref2/tools.ml @@ -917,6 +917,7 @@ and resolve_module : Env.t -> Cpath.module_ -> resolve_module_result = |> map_error (fun e -> (e :> simple_module_lookup_error)) >>= fun (parent_sig, sub) -> handle_module_lookup env id parent parent_sig sub + | `ApplyParam _ -> Error `ParameterizedInstance | `Apply (m1, m2) -> ( let func = resolve_module env m1 in let arg = resolve_module env m2 in diff --git a/test/integration/dune b/test/integration/dune index fcdabf23ba..2d9bafb116 100644 --- a/test/integration/dune +++ b/test/integration/dune @@ -18,3 +18,7 @@ (applies_to html_support_files) (enabled_if (> %{ocaml_version} 4.14.0))) + +(cram + (applies_to parameterized) + (enabled_if %{ocaml-config:ox})) diff --git a/test/integration/parameterized.t/run.t b/test/integration/parameterized.t/run.t new file mode 100644 index 0000000000..8a2a995c8f --- /dev/null +++ b/test/integration/parameterized.t/run.t @@ -0,0 +1,516 @@ +OxCaml parameterized libraries: check that odoc renders documentation for +library parameters, their implementations, parameterized libraries and the +instantiations of parameterized libraries types. + +The library graph built below is: + +- `A_param` and `B_param`: library parameters. +- `A1` and `A2`: implementations of `A_param`. +- `B1`: implementation of `B_param`. +- `A_of_b`: parameterised by `B_param`, implements `A_param`. +- `Only_a`: parameterised by `A_param`. +- `Both_ab`: parameterised by `A_param` and `B_param`, uses `Only_a(A1)` and `Only_a(A_of_b)`. +- `Final`: uses `Both_ab(A1)(B1)` and `Both_ab(A2)(B1)`. + +The support for OxCaml parameterized library in Dune requires: + + $ cat > dune-project < (lang dune 3.20) + > (using oxcaml 0.1) + > EOF + +The library parameters: + + $ mkdir a_param + $ cat > a_param/dune < (library_parameter + > (name a_param)) + > EOF + $ cat > a_param/a_param.mli < (** The [A_param] library parameter. *) + > + > type t + > (** Abstract elements. *) + > + > val name : string + > + > val make : int -> t + > + > val to_string : t -> string + > + > module Sub : sig + > type s + > val zero : s + > end + > + > module type ORDER = sig + > type o + > val compare : o -> o -> int + > end + > EOF + + $ mkdir b_param + $ cat > b_param/dune < (library_parameter + > (name b_param)) + > EOF + $ cat > b_param/b_param.mli < (** The [B_param] library parameter. *) + > + > type u + > + > val tag : u + > + > val of_int : int -> u + > + > val combine : u -> u -> u + > + > val show : u -> string + > EOF + +Two implementations of [A_param]: + + $ mkdir a1 + $ cat > a1/dune < (library + > (name a1) + > (implements a_param)) + > EOF + $ cat > a1/a1.ml < type t = int + > let name = "a1" + > let make n = n + > let to_string = string_of_int + > module Sub = struct type s = unit let zero = () end + > module type ORDER = sig type o val compare : o -> o -> int end + > EOF + + $ mkdir a2 + $ cat > a2/dune < (library + > (name a2) + > (implements a_param)) + > EOF + $ cat > a2/a2.ml < type t = string + > let name = "a2" + > let make = string_of_int + > let to_string s = s + > module Sub = struct type s = bool let zero = false end + > module type ORDER = sig type o val compare : o -> o -> int end + > EOF + +One implementation of [B_param]: + + $ mkdir b1 + $ cat > b1/dune < (library + > (name b1) + > (implements b_param)) + > EOF + $ cat > b1/b1.ml < type u = int + > let tag = 0 + > let of_int n = n + > let combine = (+) + > let show = string_of_int + > EOF + +[A_of_b] is parameterised by [B_param] and implements [A_param]: + + $ mkdir a_of_b + $ cat > a_of_b/dune < (library + > (name a_of_b) + > (parameters b_param) + > (implements a_param)) + > EOF + $ cat > a_of_b/a_of_b.ml < type t = B_param.u + > let name = "a_of_b" + > let make n = B_param.of_int n + > let to_string t = B_param.show t + > module Sub = struct type s = B_param.u let zero = B_param.tag end + > module type ORDER = sig type o val compare : o -> o -> int end + > EOF + +[Only_a] is parameterised by [A_param]. It exercises types, submodules, module +aliases, functors and first-class module arguments referring to the parameter: + + $ mkdir only_a + $ cat > only_a/dune < (library + > (name only_a) + > (parameters a_param)) + > EOF + $ cat > only_a/only_a.ml < (** Helpers built on top of the {!A_param} parameter. *) + > + > type wrapped = { value : A_param.t; label : string } + > + > let wrap value = { value; label = A_param.name } + > + > let default n = wrap (A_param.make n) + > + > let show w = A_param.to_string w.value + > + > module Alias = A_param.Sub + > + > module Make (O : A_param.ORDER) = struct + > let min a b = if O.compare a b <= 0 then a else b + > end + > + > module type S = sig type q end + > + > module Inner = struct type i = int let v = 0 end + > + > let pick (type a) (module O : A_param.ORDER with type o = a) (x : a) (y : a) = + > if O.compare x y <= 0 then x else y + > EOF + +[Both_ab] is parameterised by [A_param] and [B_param], depends on [Only_a] and +instantiates it as [Only_a(A1)] and [Only_a(A_of_b)] (the latter implicitly +using [Both_ab]'s [B_param]): + + $ mkdir both_ab + $ cat > both_ab/dune < (library + > (name both_ab) + > (parameters a_param b_param) + > (libraries + > (instantiate only_a a1 :as only_a1) + > (instantiate only_a a_of_b :as only_a_of_b))) + > EOF + $ cat > both_ab/both_ab.ml < type combined = { a : A_param.t; b : B_param.u } + > + > let make i = { a = A_param.make i; b = B_param.of_int i } + > + > let demo_a1 : Only_a1.wrapped = Only_a1.default 1 + > + > let demo_a_of_b : Only_a_of_b.wrapped = Only_a_of_b.default 2 + > + > module Nested = Only_a1.Inner + > + > let nested : Only_a1.Inner.i = 0 + > + > module type Sig = Only_a1.S + > + > let packed : (module Only_a1.S) option = None + > EOF + +[Final] depends on the full instantiations [Both_ab(A1)(B1)] and +[Both_ab(A2)(B1)]: + + $ mkdir final + $ cat > final/dune < (library + > (name final) + > (libraries + > (instantiate both_ab a1 b1 :as both_a1_b1) + > (instantiate both_ab a2 b1 :as both_a2_b1))) + > EOF + $ cat > final/final.ml < let x = Both_a1_b1.make 1 + > let y = Both_a2_b1.make 2 + > let combos : Both_a1_b1.combined * Both_a2_b1.combined = (x, y) + > EOF + +Everything builds and odoc generates documentation for all the libraries: + + $ dune build @doc-private 2>&1 + +Render to markdown for inspection: + + $ for f in $(find _build/default/_doc/_odocls -name '*.odocl' | sort); do + > odoc markdown-generate "$f" -o markdown 2>&1 + > done + +The library names below are suffixed with an opaque hash by dune; we normalise +it away so the output is stable. + + $ md() { cat markdown/$1@*/$2.md | sed 's/@[0-9a-f]*/@HASH/g'; } + +The library parameters are reported as such: + + $ md a_param A_param + + # Library parameter `A_param` + + The `A_param` library parameter. + + ```ocaml + type t + ``` + Abstract elements. + + ```ocaml + val name : string + ``` + ```ocaml + val make : int -> t + ``` + ```ocaml + val to_string : t -> string + ``` + ```ocaml + module Sub : sig ... end + ``` + ```ocaml + module type ORDER = sig ... end + ``` + $ md b_param B_param + + # Library parameter `B_param` + + The `B_param` library parameter. + + ```ocaml + type u + ``` + ```ocaml + val tag : u + ``` + ```ocaml + val of_int : int -> u + ``` + ```ocaml + val combine : u -> u -> u + ``` + ```ocaml + val show : u -> string + ``` + +The implementations show which parameter they implement, with a link to the +parameter documentation: + + $ md a1 A1 + + # Module `A1` + + Implements the library parameter [`A_param`](./../a_param@HASH/A_param.md). + + ```ocaml + type t = int + ``` + ```ocaml + val name : string + ``` + ```ocaml + val make : 'a -> 'a + ``` + ```ocaml + val to_string : int -> string + ``` + ```ocaml + module Sub : sig ... end + ``` + ```ocaml + module type ORDER = sig ... end + ``` + $ md a2 A2 + + # Module `A2` + + Implements the library parameter [`A_param`](./../a_param@HASH/A_param.md). + + ```ocaml + type t = string + ``` + ```ocaml + val name : string + ``` + ```ocaml + val make : int -> string + ``` + ```ocaml + val to_string : 'a -> 'a + ``` + ```ocaml + module Sub : sig ... end + ``` + ```ocaml + module type ORDER = sig ... end + ``` + $ md b1 B1 + + # Module `B1` + + Implements the library parameter [`B_param`](./../b_param@HASH/B_param.md). + + ```ocaml + type u = int + ``` + ```ocaml + val tag : int + ``` + ```ocaml + val of_int : 'a -> 'a + ``` + ```ocaml + val combine : int -> int -> int + ``` + ```ocaml + val show : int -> string + ``` + $ md a_of_b A_of_b + + # Module `A_of_b` + + Implements the library parameter [`A_param`](./../a_param@HASH/A_param.md). + + + ## Library parameters + + ```ocaml + parameter B_param + ``` + + ## Signature + + ```ocaml + type t = B_param.u + ``` + ```ocaml + val name : string + ``` + ```ocaml + val make : int -> B_param.u + ``` + ```ocaml + val to_string : B_param.u -> string + ``` + ```ocaml + module Sub : sig ... end + ``` + ```ocaml + module type ORDER = sig ... end + ``` + +The parameterised libraries list the parameters they are parameterised by, with +links to the parameter documentation. Instantiations are rendered in OxCaml +instance syntax as `Only_a[A_param:A1]` and `Both_ab[A_param:A1][B_param:B1]` +(the base library, each parameter and each argument keeping their own link) +rather than through the internal wrapper modules: + + $ md only_a Only_a + + # Module `Only_a` + + Helpers built on top of the [`A_param`](./../a_param@HASH/A_param.md) parameter. + + + ## Library parameters + + ```ocaml + parameter A_param + ``` + + ## Signature + + ```ocaml + type wrapped = { + value : A_param.t; + label : string; + } + ``` + ```ocaml + val wrap : A_param.t -> wrapped + ``` + ```ocaml + val default : int -> wrapped + ``` + ```ocaml + val show : wrapped -> string + ``` + ```ocaml + module Alias = A_param.Sub + ``` + ```ocaml + module Make (O : A_param.ORDER) : sig ... end + ``` + ```ocaml + module type S = sig ... end + ``` + ```ocaml + module Inner : sig ... end + ``` + ```ocaml + val pick : (module A_param.ORDER with type o = 'a) -> 'a -> 'a -> 'a + ``` + $ md both_ab Both_ab + + # Module `Both_ab` + + + ## Library parameters + + ```ocaml + parameter A_param + ``` + ```ocaml + parameter B_param + ``` + + ## Signature + + ```ocaml + type combined = { + a : A_param.t; + b : B_param.u; + } + ``` + ```ocaml + val make : int -> combined + ``` + ```ocaml + val demo_a1 : Only_a[A_param:A1].wrapped + ``` + ```ocaml + val demo_a_of_b : Only_a[A_param:A_of_b].wrapped + ``` + ```ocaml + module Nested = Only_a[A_param:A1].Inner + ``` + ```ocaml + val nested : Only_a[A_param:A1].Inner.i + ``` + ```ocaml + module type Sig = Only_a[A_param:A1].S + ``` + ```ocaml + val packed : (module Only_a[A_param:A1].S) option + ``` + $ md final Final + + # Module `Final` + + ```ocaml + val x : Both_ab[A_param:A1][B_param:B1].combined + ``` + ```ocaml + val y : Both_ab[A_param:A2][B_param:B1].combined + ``` + ```ocaml + val combos : + Both_ab[A_param:A1][B_param:B1].combined + * Both_ab[A_param:A2][B_param:B1].combined + ``` + +The page of a library parameter keeps the url derived from its identifier, so +the sidebar marks it as the current unit and keeps its children, exactly like a +plain module: + + $ odoc compile-index --root _build/default/_doc/_odocls/ + $ odoc sidebar-generate index.odoc-index + $ for lib in a_param a1; do + > odoc html-generate --indent --sidebar sidebar.odoc-sidebar -o html \ + > $(find _build/default/_doc/_odocls -iname "$lib.odocl") + > done + $ grep -rl 'current_unit' html/ | sed 's/@[0-9a-f]*/@HASH/g' | sort + html/a1@HASH/A1/Sub/index.html + html/a1@HASH/A1/index.html + html/a1@HASH/A1/module-type-ORDER/index.html + html/a_param@HASH/A_param/Sub/index.html + html/a_param@HASH/A_param/index.html + html/a_param@HASH/A_param/module-type-ORDER/index.html diff --git a/test/xref2/lib/common.cppo.ml b/test/xref2/lib/common.cppo.ml index 3cc3a1dd37..699c75c155 100644 --- a/test/xref2/lib/common.cppo.ml +++ b/test/xref2/lib/common.cppo.ml @@ -580,6 +580,7 @@ module LangUtils = struct | `DotT (parent,s) -> Format.fprintf ppf "%a.%a" path (parent :> Odoc_model.Paths.Path.t) TypeName.fmt s | `DotV (parent,s) -> Format.fprintf ppf "%a.%a" path (parent :> Odoc_model.Paths.Path.t) ValueName.fmt s | `Apply (func,arg) -> Format.fprintf ppf "%a(%a)" path (func :> Odoc_model.Paths.Path.t) path (arg :> Odoc_model.Paths.Path.t) + | `ApplyParam (inst,param,arg) -> Format.fprintf ppf "%a[%a:%a]" path (inst :> Odoc_model.Paths.Path.t) path (param :> Odoc_model.Paths.Path.t) path (arg :> Odoc_model.Paths.Path.t) | `SubstitutedT _|`SubstitutedMT _|`Substituted _|`SubstitutedCT _|`Unbox _ -> Format.fprintf ppf "Unimplemented path" and model_fragment ppf (f : Odoc_model.Paths.Fragment.t) = @@ -614,6 +615,8 @@ let my_compilation_unit id (s : Odoc_model.Lang.Signature.t) = ; linked = false ; canonical = None ; source_loc = None + ; parameterisation = + { is_parameter = false; parameters = []; argument_for = None } } let mkresolver () =