Skip to content

Proposal for nested record definitions#1

Closed
davesnx wants to merge 13 commits into
trunkfrom
nested-record-def
Closed

Proposal for nested record definitions#1
davesnx wants to merge 13 commits into
trunkfrom
nested-record-def

Conversation

@davesnx

@davesnx davesnx commented Feb 6, 2026

Copy link
Copy Markdown
Owner

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:

type http_request = {
  url : string;
  headers : { content_type : string; authorization : string };
  body : string option;
}

In the current OCaml, the user must introduce a new type:

type http_headers = { content_type : string; authorization : string }
type http_request = {
  url : string;
  headers : http_headers;
  body : string option;
}

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_ty constructor called Pfld_ty to represent inline record field types:

and extra_ty =
  | Pcstr_ty of string
  | Pext_ty
+ | Pfld_ty of string

For type t = { address : { street : string } }, the inner record has the
internal path Pextra_ty (Pident "t", Pfld_ty "address").

Parser

The ast (Parsetree.label_declaration, Ast_mapper.label_declaration) gains an optional field pld_inline_record: label_declaration list option;. Some fields represents an inline record with the given fields, the None case it is the existing behaviour.

Type declarations

In typedecl.ml, transl_labels detects pld_inline_record and creates an anonymous type_declaration for the inner record, which follows the same pattern as constructor_args in datarepr.ml for constructor inline records. The declaration is stored in a new ld_inlined : type_declaration option field on Types.label_declaration, mirroring cstr_inlined on 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:

type config = {
  server : { host : string; port : int };
  database : { host : string; port : int };
}

let s = { host = "test"; port = 99 }
(* Error: Unbound record field host *)

Env.find_type_data handles Pfld_ty paths by looking up the parent record's label descriptions and returning the inline type declaration.

Nesting depth

Arbitrary nesting is supported:

type t = { a : { b : { c : int } } }

let x = { a = { b = { c = 42 } } }
let v = x.a.b.c (* : int *)

Type params

The anonymous type declaration's parameters are computed from the free type variables of the inner fields (via Ctype.free_variables_list)

type ('a, 'b) either_record = {
  left : { payload : 'a };
  right : { payload : 'b };
}

let either : (int, string) either_record =
  { left = { payload = 42 }; right = { payload = "hi" } }

let left_payload = either.left.payload (* : int *)
let right_payload = either.right.payload (* : string *)

Limitations and future work

  • Inline records inside constructor inline records are not implemented (Foo of { inner : { x : int } } does not work because transl_constructor_arguments does not pass a type path to transl_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 .mli file — { ... } is not a valid core_type. This is the same inherent property as constructor inline records. The type declaration itself (type person = { address : { ... } }) works in .mli files

  • odoc_sig.ml has 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_ty case Pfld_ty to name field-scoped anonymous record types, extends the AST (Parsetree.label_declaration) with pld_inline_record, and updates typing (typedecl.ml, Types.label_declaration) to synthesize and carry an internal type_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 walk pld_inline_record.

Written by Cursor Bugbot for commit e50e88f. This will update automatically on new commits. Configure here.

@davesnx davesnx changed the title Nested record definitions Proposal for nested record definitions Feb 6, 2026
@davesnx davesnx closed this Feb 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant