Skip to content

Represent optional parameters with defaults structurally instead of by name sniffing - #8580

Merged
cristianoc merged 2 commits into
masterfrom
structural-optional-params
Aug 26, 2026
Merged

Represent optional parameters with defaults structurally instead of by name sniffing#8580
cristianoc merged 2 commits into
masterfrom
structural-optional-params

Conversation

@cristianoc

Copy link
Copy Markdown
Collaborator

The desugaring of an optional parameter with a default (~x=3) binds the option-carrying parameter to a synthetic *opt_<label>* variable, and that fact was recovered downstream by pattern-matching on the variable name in four places across three compiler layers:

  • typecore's is_fake_let matched the pre-n-ary name *opt* exactly, so it had silently rotted into dead code when the naming changed — nothing failed, the behavior just disappeared.
  • matching had a peephole keyed on the name *sth*.
  • lam_convert's rename_optional_parameters sniffed the *opt prefix and re-recognized the entire compiled body shape (Llet of Lifthenelse(Pis_not_none, …)) to rename the parameter to xOpt for JS output.
  • gentype sniffed the *opt prefix to show the label in TS output.

What this PR does

Represents the fact structurally and deletes all the name sniffing:

  • Typedtree.function_param gains an fp_has_default field.
  • type_function replaces the carrier ident right after typing: fp_param becomes a fresh ident named <label>Opt, substituted at the carrier's only two occurrences (the parameter pattern's binder and the synthetic match's scrutinee) — both nodes the desugaring itself generated a few lines earlier. The unspellable name still exists during typing, where names must be impossible to capture or shadow, but dies before the typedtree leaves the function. The Lambda IR is born with the final parameter name and no residual binding, so lam_convert performs no work for this feature (a step toward Lambda and Lam converging).
  • lam_convert's rename_optional_parameters / is_opt_param_name are deleted; the Lfunction case is a plain conversion.
  • matching's peephole is generalized to eliminate any alias binding of the form let v = arg in v, with no name test.
  • gentype uses fp_has_default instead of sniffing the ident name.
  • is_fake_let now keys on the #optional_arg_default attribute the desugaring plants, restoring its intended behavior: an unused defaulted parameter warns as an unused parameter (27), not an unused let (26).
  • The write-only #default attribute is removed, along with its dead parsetree consumer.

Visible improvements

Parameter names in emitted JS now consistently derive from the label, including cases the old shape-match silently missed and leaked mangled names for — mario_game.mjs's make$2 had $staropt_id$star as a parameter name; it is now idOpt. ~label as name=default parameters are now named after the label (bb_offOpt) rather than the inner binder.

Tests

make test (ounit, build tests, super_errors, runtime), make test-gentype, make test-analysis, make test-tools, make test-syntax all pass; make checkformat clean. Stdlib and Belt recompile with zero JS output diffs; the only snapshot change is the mario_game.mjs improvement above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PCtQiaDijUqA2fujQXvKUw

@cristianoc
cristianoc force-pushed the structural-optional-params branch from 7dfce46 to 45e154a Compare August 25, 2026 12:04

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

Copy link
Copy Markdown

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 automated review suggestions for this pull request.

Reviewed commit: 7dfce46e10

ℹ️ 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
  • 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 address that feedback".

Comment thread compiler/ml/typedtree.ml
@cristianoc
cristianoc force-pushed the structural-optional-params branch from 45e154a to cf9f04c Compare August 25, 2026 12:08
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 75.93%. Comparing base (9e7c571) to head (4ff007d).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #8580   +/-   ##
=======================================
  Coverage   75.93%   75.93%           
=======================================
  Files         474      474           
  Lines       62857    62842   -15     
=======================================
- Hits        47729    47718   -11     
+ Misses      15128    15124    -4     
Files with missing lines Coverage Δ
compiler/core/lam_convert.ml 77.47% <100.00%> (-1.10%) ⬇️
compiler/core/lam_stats_export.ml 90.69% <ø> (ø)
compiler/gentype/translate_structure.ml 72.00% <100.00%> (+0.10%) ⬆️
compiler/ml/matching.ml 76.97% <100.00%> (ø)
compiler/ml/typecore.ml 85.97% <100.00%> (+0.16%) ⬆️
compiler/ml/typedtree.ml 91.48% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cristianoc
cristianoc force-pushed the structural-optional-params branch from cf9f04c to 070c955 Compare August 25, 2026 18:55
@cristianoc
cristianoc changed the base branch from master to cleanup/fold-bs-ast-mapper August 25, 2026 19:06
@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

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

@rescript/belt

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

@rescript/darwin-arm64

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

@rescript/darwin-x64

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

@rescript/linux-arm64

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

@rescript/linux-x64

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

@rescript/runtime

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

@rescript/win32-x64

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

commit: 4ff007d

Base automatically changed from cleanup/fold-bs-ast-mapper to master August 25, 2026 19:18
…y name sniffing

The desugaring of an optional parameter with a default (~x=3) binds the
option-carrying parameter to a synthetic *opt_<label>* variable, and that
fact was recovered downstream by pattern-matching on the variable name in
four places across three compiler layers: typecore's is_fake_let (matching
the pre-n-ary name "*opt*" exactly, so it had silently rotted into dead
code), matching's *sth* let-elimination peephole, lam_convert's
rename_optional_parameters (which also had to re-recognize the whole
compiled body shape to rename the parameter to xOpt for JS output), and
gentype's *opt prefix check.

Represent the fact structurally instead:

- Typedtree.function_param gains an fp_has_default field.
- type_function replaces the carrier ident right after typing: fp_param
  becomes a fresh ident named <label>Opt, substituted at the carrier's
  only two occurrences (the parameter pattern's binder and the synthetic
  match's scrutinee), both nodes the desugaring itself generated. The
  unspellable name still exists during typing, where names must be
  impossible to capture or shadow, but dies before the typedtree leaves
  the function. The Lambda IR is born with the final parameter name and
  no residual binding.
- lam_convert's rename_optional_parameters and is_opt_param_name are
  deleted; the Lfunction case is a plain conversion.
- matching's *sth* peephole is generalized to eliminate any alias binding
  of the form let v = arg in v, with no name test.
- gentype uses fp_has_default instead of sniffing the ident name.
- is_fake_let now keys on the #optional_arg_default attribute the
  desugaring plants, restoring its intended behavior: an unused defaulted
  parameter warns as an unused parameter (27), not an unused let (26).
- The write-only #default attribute is removed, along with its dead
  parsetree consumer.

Visible improvements: parameter names in emitted JS now consistently
derive from the label, including cases the old shape-match silently
missed and leaked mangled names for (mario_game.mjs's make$2 had
$staropt_id$star as a parameter; it is now idOpt).

Signed-Off-By: Cristiano Calcagno <ccrisccris@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PCtQiaDijUqA2fujQXvKUw
@cristianoc
cristianoc force-pushed the structural-optional-params branch from 070c955 to ab19aea Compare August 25, 2026 19:22
@github-actions

Copy link
Copy Markdown

@cristianoc
cristianoc requested a review from cknitt August 25, 2026 20:11

@cknitt cknitt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice improvement to the JS output! Tested successfully in a large project.

Comment thread CHANGELOG.md Outdated
#### :house: Internal

- Give nominal variants one canonical runtime layout: compute their JavaScript representation once after typing each declaration, replace positional constructor tags with semantic runtime descriptors, and make construction, matching, coercion, printing, diagnostics, and GenType consume the stored representation instead of reinterpreting annotations. Pattern matching keeps occurrence-specific plans local without adding another Lambda or Lam expression form. https://github.com/rescript-lang/rescript/pull/8579
- Represent optional parameters with defaults structurally instead of recovering them downstream by pattern-matching on the synthetic `*opt_<label>*`/`*sth*` variable names: `Typedtree.function_param` gains an `fp_has_default` field, the type checker gives the compiled parameter its final `<label>Opt` ident before the typedtree leaves the function, and the name-sniffing consumers (`lam_convert`'s shape-matching rename pass, `matching`'s `*sth*` peephole, gentype's prefix check, and typecore's dead `is_fake_let`/`#default` vestiges) are deleted or generalized. Parameter names in emitted JS now consistently derive from the label, including cases the old shape-match silently missed and leaked mangled names for (`$staropt_id$star` → `idOpt`), and an unused defaulted parameter warns as an unused parameter (27) rather than an unused let binding (26). The CMT magic number is bumped to `Caml1999T025`. https://github.com/rescript-lang/rescript/pull/8580

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This changelog entry is a bit lengthy, could be trimmed down a bit.

@cristianoc
cristianoc enabled auto-merge (rebase) August 26, 2026 07:16
@cristianoc
cristianoc merged commit 104e41c into master Aug 26, 2026
29 checks passed
@cristianoc
cristianoc deleted the structural-optional-params branch August 26, 2026 07:38
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.

2 participants