Proposal for nested record definitions#1
Closed
davesnx wants to merge 13 commits into
Closed
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Since OCaml 4.03, inline records in variant constructors have proven to be a valuable feature, this PR extends the same mechanism to record fields:
In the current OCaml, the user must introduce a new type:
While the extra type version is valid, it incurs a cost: it appears in the module signature and can be constructed independently.
Inline record fields enforce locality of definition: they are anonymous and scoped to their parent field, and can only be accessed through the parent. I found it to be a good feature to introduce to OCaml, and considerably inspired by ReScript v12 (rescript-lang/rescript#7241), but with a different design.
Design
The implementation extends the existing inline record mechanism (
Pextra_ty/cstr_inlined) to record fields. To my surprise, no new type-checking changes are required.Path system
A new
Path.extra_tyconstructor calledPfld_tyto represent inline record field types:and extra_ty = | Pcstr_ty of string | Pext_ty + | Pfld_ty of stringFor
type t = { address : { street : string } }, the inner record has theinternal path
Pextra_ty (Pident "t", Pfld_ty "address").Parser
The ast (
Parsetree.label_declaration,Ast_mapper.label_declaration) gains an optional fieldpld_inline_record: label_declaration list option;.Some fieldsrepresents an inline record with the given fields, theNonecase it is the existing behaviour.Type declarations
In
typedecl.ml,transl_labelsdetectspld_inline_recordand creates an anonymoustype_declarationfor the inner record, which follows the same pattern asconstructor_argsindatarepr.mlfor constructor inline records. The declaration is stored in a newld_inlined : type_declaration optionfield onTypes.label_declaration, mirroringcstr_inlinedon constructor descriptions.Environment
The inner record fields are not registered as top-level labels in the environment. This means the standalone construction of an inner record is rejected:
Env.find_type_datahandlesPfld_typaths by looking up the parent record's label descriptions and returning the inline type declaration.Nesting depth
Arbitrary nesting is supported:
Type params
The anonymous type declaration's parameters are computed from the free type variables of the inner fields (via
Ctype.free_variables_list)Limitations and future work
Inline records inside constructor inline records are not implemented (
Foo of { inner : { x : int } }does not work becausetransl_constructor_argumentsdoes not pass a type path totransl_labels). Do we want this? Is it reasonable to add as another PR?Inline record types in type expressions: the inline record type cannot be named in standalone type positions. For example, if
val get_address : person -> { street : string; city : string }is the inferred signature, there is no surface syntax to write that return type i an.mlifile —{ ... }is not a validcore_type. This is the same inherent property as constructor inline records. The type declaration itself (type person = { address : { ... } }) works in.mlifilesodoc_sig.mlhas only been made the compiler happy.Disclaimer
This PR was developed with AI-assisted coding with Cursor, even though I had little experience with the repository after @nojb's fun-ocaml workshop. The implementation and this (great) description were produced by both
Note
High Risk
Touches core compiler parsing,
Path/environment lookup, and type-declaration translation; regressions could affect type resolution and record handling across the compiler toolchain.Overview
Adds language support for inline/nested record definitions inside record fields (e.g.
{ headers : { ... } }), using the existing inline-record type infrastructure previously limited to variant constructors.This introduces a new
Path.extra_tycasePfld_tyto name field-scoped anonymous record types, extends the AST (Parsetree.label_declaration) withpld_inline_record, and updates typing (typedecl.ml,Types.label_declaration) to synthesize and carry an internaltype_declaration(ld_inlined) for the field’s anonymous record while keeping its labels out of the top-level environment.Tooling is updated to understand/traverse the new AST shape and typed representation, including dependency analysis (
parsing/depend.ml) and ocamldoc signature extraction (ocamldoc/odoc_sig.ml), plus mapper/iterator/helper updates to construct and walkpld_inline_record.Written by Cursor Bugbot for commit e50e88f. This will update automatically on new commits. Configure here.