Skip to content

Conversation

zth
Copy link
Member

@zth zth commented Sep 9, 2025

This fixes a few gaps with record spreads in inline records:

  • Spreading a record (when at least 1 more record prop existed) in an inline record definition in a constructor would parse, but not expand in the type checker. So you'd be left with one or more \"..." fields. They're now expanded
  • Having only a spread would always parse/type check as an object type spread. We now apply the same heuristic we have elsewhere for this ambiguity, where we delay checking what the actual type is (record or object) til we know enough to make a decision in type checking
  • Also fixes a bug where mixing objects and records when spreading one and providing another field of the other was possible to do

Closes #6822

| _ -> None)
else []

let expand_labels_with_type_spreads ?(return_none_on_failure = false)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just extracted for reuse, since it's now used in 2 places.

Copy link

pkg-pr-new bot commented Sep 9, 2025

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript-lang/rescript@7859

@rescript/darwin-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-arm64@7859

@rescript/darwin-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/darwin-x64@7859

@rescript/linux-arm64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-arm64@7859

@rescript/linux-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/linux-x64@7859

@rescript/runtime

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/runtime@7859

@rescript/win32-x64

npm i https://pkg.pr.new/rescript-lang/rescript/@rescript/win32-x64@7859

commit: 5398f9d

@zth zth changed the title [WIP] Support record type spreads in inline records Support record type spreads in inline records Sep 9, 2025
@zth zth marked this pull request as ready for review September 9, 2025 18:07
@zth zth requested review from Copilot, cristianoc and shulhi September 9, 2025 18:08
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR fixes gaps with record type spreads in inline records by improving the parsing and type checking logic to properly handle spread operations in constructor definitions.

  • Removes parsing restrictions that prevented record spreads in inline records
  • Adds proper type expansion for record spreads in constructor arguments
  • Introduces heuristic-based disambiguation between record and object type spreads

Reviewed Changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated no comments.

Show a summary per file
File Description
tests/tests/src/record_type_spread.res Test cases for record type spreads in variant constructors
tests/tests/src/record_type_spread.mjs Generated JavaScript for record spread tests
tests/tests/src/object_type_spreads.res Test cases demonstrating object vs record spread disambiguation
tests/tests/src/object_type_spreads.mjs Generated JavaScript for object spread tests
tests/syntax_tests/data/parsing/errors/typexpr/expected/objectSpread.res.txt Updated parser error expectations removing old restrictions
tests/syntax_tests/data/parsing/errors/other/expected/spread.res.txt Updated parser output for spread syntax
compiler/syntax/src/res_core.ml Core parser changes for handling spreads in constructor arguments
compiler/ml/typedecl.ml Type declaration logic updates for record spread expansion
compiler/ml/record_type_spread.ml New utility function for expanding record spreads
CHANGELOG.md Changelog entry for the bug fix

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@zth zth requested a review from cknitt September 9, 2025 18:10
@cknitt
Copy link
Member

cknitt commented Sep 9, 2025

Nice!

Somewhat related issue: #6822

@zth
Copy link
Member Author

zth commented Sep 9, 2025

Nice!

Somewhat related issue: #6822

Was able to fix that in the latest commit as well.

Note for us for later though - we really need to first class the "ambigious spread that needs to be resolved in the type checker" concept soon though. This is all pretty fragile.

Copy link
Member

@shulhi shulhi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@cristianoc
Copy link
Collaborator

@codex check changes to the type checker:
1 top priority: existing programs still type check without anything changes
2 mid: if spreads in inline record are are used, then all code that does not use those records is unaffected
3 lower: check correctness of the type checker for spreads with inline records
4 lower: are there enough tests and examples
5 lower: can changes in the code be clearer/cleaner and better maintainable, less invasive

Copy link

@codex check changes to the type checker:
1 top priority: existing programs still type check without anything changes
2 mid: if spreads in inline record are are used, then all code that does not use those records is unaffected
3 lower: check correctness of the type checker for spreads with inline records
4 lower: are there enough tests and examples
5 lower: can changes in the code be clearer/cleaner and better maintainable, less invasive

For now, I can only help with PRs you've created.

@cristianoc
Copy link
Collaborator

@codex check changes to the type checker:
1 top priority: existing programs still type check without anything changes
2 mid: if spreads in inline record are are used, then all code that does not use those records is unaffected
3 lower: check correctness of the type checker for spreads with inline records
4 lower: are there enough tests and examples
5 lower: can changes in the code be clearer/cleaner and better maintainable, less invasive

For now, I can only help with PRs you've created.

OK will do offline and report this.

@cristianoc
Copy link
Collaborator

[CODEX] Automated review from Codex CLI.

Summary of type-checker changes per checklist:

  1. Backward compatibility (top priority)
  • Existing programs should continue to type check. Code paths for records/variants without spreads are unchanged. Ambiguous single {...t} is reinterpreted as an object type (no extra fields) for compatibility.
  1. Containment (mid)
  • Spread handling is gated and localized. Code not using inline record spreads is unaffected. Expansion runs only when a "..." field is present.
  1. Correctness for inline record spreads (lower)
  • Substitutes type variables from the spread site into the target record fields; expands fields into the destination; checks duplicates; preserves optional field behavior. Variant constructor spreads handled; single-spread record args can reinterpret as object tuple when needed.
  1. Tests and examples (lower)
  • Added tests cover record spreads, type parameter substitution, object spreads in variants, and super-error cases for mixing record fields with object spreads. Suggested follow-ups: nested spreads, mutable/optional interactions, recursive types, duplicate field conflict tests.
  1. Clarity/maintainability (lower)
  • Logic is isolated (compiler/ml/record_type_spread.ml) and call sites are minimal. Potential cleanups: remove small unused helpers, factor the ambiguous single-spread reinterpretation into a helper, and centralize the typedtree placeholder construction for clarity.

If helpful, I can run the full test suite on this branch and report results.

Copy link
Collaborator

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What Prof.5 said.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex Review: Here are some suggestions.

Reply with @codex fix comments to fix any unresolved comments.

About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you open a pull request for review, mark a draft as ready, or comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex fix this CI failure" or "@codex address that feedback".

Comment on lines 260 to 282
| Pcstr_tuple l ->
let l = List.map (transl_simple_type env closed) l in
(Types.Cstr_tuple (List.map (fun t -> t.ctyp_type) l), Cstr_tuple l)
| Pcstr_record l ->
| Pcstr_record l -> (
let lbls, lbls' = transl_labels env closed l in
(Types.Cstr_record lbls', Cstr_record lbls)
let expanded =
Record_type_spread.expand_labels_with_type_spreads env lbls lbls'
in
match expanded with
| Some (lbls, lbls') -> (Types.Cstr_record lbls', Cstr_record lbls)
| None -> (
match l with
| [{pld_name = {txt = "..."}; pld_type = spread_typ; _}] ->
(* Ambiguous `{...t}`: if only spread present and it doesn't resolve to a
record type, treat it as an object-typed tuple argument. *)
let obj_ty =
Ast_helper.Typ.object_ ~loc:spread_typ.ptyp_loc
[Parsetree.Oinherit spread_typ]
Asttypes.Closed
in
let cty = transl_simple_type env closed obj_ty in
(Types.Cstr_tuple [cty.ctyp_type], Cstr_tuple [cty])
| _ -> (Types.Cstr_record lbls', Cstr_record lbls)))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Reject object spreads mixed with record fields in constructor records

When an inline record constructor contains a spread that resolves to an object type and any additional record fields (e.g. type obj = {"name": string}; type t = V({...obj, label: string})), expand_labels_with_type_spreads returns None, but this branch falls through to the _ -> (Types.Cstr_record lbls', Cstr_record lbls) case. Unlike the type-declaration path, no error is raised and the inline record is left with a synthetic "..." field rather than expanding or reinterpreting the argument as an object. This allows object and record fields to be mixed silently, contradicting the commit goal of forbidding such combinations and producing a malformed record type whose spread fields are ignored. The constructor handling should mirror the declaration logic: either raise Object_spread_with_record_field or reinterpret a single spread as an object tuple to keep semantics consistent.

Useful? React with 👍 / 👎.

@cristianoc
Copy link
Collaborator

cristianoc commented Sep 10, 2025

[CODEX] Minimal repro for constructor record + object spread mix-up

Repro code (save as tmp/repro_inline_ctor_mix.res):

type obj = {"name": string}

type t = V({...obj, label: string})

Commands:

dune build
node scripts/copyExes.js --compiler
node ./cli/bsc.js -dparsetree tmp/repro_inline_ctor_mix.res
node ./cli/bsc.js tmp/repro_inline_ctor_mix.res

Observed:

  • Parses as Pcstr_record with a synthetic "..." field and an extra record field (see -dparsetree).
  • Compiles successfully; no error raised; JS output is empty (type-only).
    Expectation:
  • Mirror type-declaration behavior: reject mixing object spread with record fields (same error as declarations).
    Analysis pointer:
  • In constructor handling, after Record_type_spread.expand_labels_with_type_spreads returns None for {...obj, label: string}, the code falls through to (Types.Cstr_record lbls', Cstr_record lbls) without an error, unlike the declaration path that raises Object_spread_with_record_field.
  • Relevant spot: compiler/ml/typedecl.ml:258 (constructor args) and compiler/ml/typedecl.ml:604 (record decl handling and error path).
    Suggested fix:
  • In transl_constructor_arguments, when expanded = None:
    • If there is only a single spread field, reinterpret as an object-typed tuple (current special case).
    • Otherwise, raise Object_spread_with_record_field (same logic/message as declarations) to forbid mixing.

@cristianoc
Copy link
Collaborator

[CODEX] Constructor record/object spread mix: repro + validated fix

Repro (inline record constructor mixing object spread with record field):

type obj = {"name": string}

type t = V({...obj, label: string})

Before (on PR branch): compiles without error, allowing mix and leaving a synthetic "..." field in the constructor record args.

Suggested change (constructor args should mirror type-decl behavior):

(* compiler/ml/typedecl.ml :: transl_constructor_arguments, Pcstr_record branch *)
(* If expand_labels_with_type_spreads returns None: *)
match l with
| [{pld_name = {txt = "..."}; pld_type = spread_typ; _}] ->
  (* reinterpret single `{...t}` as object tuple arg *)
| _ ->
  (* raise Object_spread_with_record_field on first non-spread field *)

Validation (locally applied minimal change, rebuilt, re-ran repro with bsc):

We've found a bug for you!
tmp/repro_inline_ctor_mix.res:5:21-33

You cannot mix a record field with an object type spread.
Remove the record field or change it to an object field (e.g. "label": ...).

This keeps the existing special-case for a single ambiguous {...t} (no extra fields): still reinterpreted as an object-typed tuple argument for compatibility.

@zth
Copy link
Member Author

zth commented Sep 10, 2025

Fixed in f5f2f84

@zth zth force-pushed the inline-record-type-spreads branch from f5f2f84 to 699b429 Compare September 10, 2025 13:05
Copy link
Collaborator

@cristianoc cristianoc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ef337fe
looks good

@zth zth merged commit d711614 into master Sep 10, 2025
25 checks passed
@zth zth deleted the inline-record-type-spreads branch September 10, 2025 13:23
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.

Compiler doesn't prevent from spreading object types in a record type

4 participants