Skip to content

Commit ab19aea

Browse files
cristianocclaude
andcommitted
Represent optional parameters with defaults structurally instead of by 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
1 parent 9e7c571 commit ab19aea

10 files changed

Lines changed: 119 additions & 97 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
#### :house: Internal
5353

5454
- 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
55+
- 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
5556
- Sync the platform npm package's compiler binaries (`packages/@rescript/<platform>/bin`) via dune promotion on every `dune build`, instead of Makefile/CI copy steps that only ran when make did: a plain `dune build` can no longer leave `cli/*.js` and the test harnesses running a stale compiler. https://github.com/rescript-lang/rescript/pull/8560
5657
- Remove unused compiler IR definitions, modules, helpers, error variants, and Typedtree fields. https://github.com/rescript-lang/rescript/pull/8551 https://github.com/rescript-lang/rescript/pull/8555
5758
- Make locally abstract value constraints (`let f: type a. t = value`) structural in the parsetree, remove the obsolete `Pexp_newtype` and `Texp_newtype` wrapper metadata, and keep the old encoding confined to the frozen external-PPX bridge. The CMT magic number is bumped to `Caml1999T024`. https://github.com/rescript-lang/rescript/pull/8575

compiler/core/lam_convert.ml

Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -320,40 +320,6 @@ let lam_prim ~primitive:(p : Lambda.primitive) ~args loc : Lam.t =
320320

321321
let may_depend = Lam_module_ident.Hash_set.add
322322

323-
let is_opt_param_name name =
324-
(* [*opt*] historically; [*opt_<label>*] with the n-ary representation *)
325-
String.length name >= 5
326-
&& String.sub name 0 4 = "*opt"
327-
&& String.get name (String.length name - 1) = '*'
328-
329-
let rec rename_optional_parameters map params (body : Lambda.lambda) =
330-
match body with
331-
| Llet
332-
( k,
333-
value_kind,
334-
id,
335-
Lifthenelse
336-
( Lprim (p, [Lvar ({name = opt_name} as opt)], p_loc),
337-
Lprim (p1, [Lvar ({name = opt_name2} as opt2)], x_loc),
338-
f ),
339-
rest )
340-
when is_opt_param_name opt_name
341-
&& is_opt_param_name opt_name2
342-
&& Ident.same opt opt2 && List.mem opt params ->
343-
let map, rest = rename_optional_parameters map params rest in
344-
let new_id = Ident.create (id.name ^ "Opt") in
345-
( Map_ident.add map opt new_id,
346-
Lambda.Llet
347-
( k,
348-
value_kind,
349-
id,
350-
Lifthenelse
351-
( Lprim (p, [Lvar new_id], p_loc),
352-
Lprim (p1, [Lvar new_id], x_loc),
353-
f ),
354-
rest ) )
355-
| _ -> (map, body)
356-
357323
let convert (exports : Set_ident.t) (lam : Lambda.lambda) :
358324
Lam.t * Lam_module_ident.Hash_set.t =
359325
let alias_tbl = Hash_ident.create 64 in
@@ -415,18 +381,8 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) :
415381
{ap_loc = loc; ap_inlined; ap_status = App_uncurry}
416382
~ap_transformed_jsx
417383
| Lfunction {params; body; attr; loc} ->
418-
let new_map, body =
419-
rename_optional_parameters Map_ident.empty params body
420-
in
421-
if Map_ident.is_empty new_map then
422-
Lam.function_ ~loc ~attr ~arity:(List.length params) ~params
423-
~body:(convert_aux body)
424-
else
425-
let params =
426-
Ext_list.map params (fun x -> Map_ident.find_default new_map x x)
427-
in
428-
Lam.function_ ~loc ~attr ~arity:(List.length params) ~params
429-
~body:(convert_aux body)
384+
Lam.function_ ~loc ~attr ~arity:(List.length params) ~params
385+
~body:(convert_aux body)
430386
| Llet (_, _, _, Lprim (Pgetglobal id, args, _), _body) when dynamic_import
431387
->
432388
(*

compiler/core/lam_stats_export.ml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,6 @@
2323
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)
2424

2525
(* let pp = Format.fprintf *)
26-
(* we should exclude meaninglist names and do the convert as well *)
27-
28-
(* let meaningless_names = ["*opt*"; "param";] *)
2926

3027
let single_na = Js_cmj_format.single_na
3128

compiler/ext/config.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,6 @@ and ast0_impl_magic_number = "Caml1999M022"
1313

1414
and ast0_intf_magic_number = "Caml1999N022"
1515

16-
and cmt_magic_number = "Caml1999T024"
16+
and cmt_magic_number = "Caml1999T025"
1717

1818
let load_path = ref ([] : string list)

compiler/gentype/translate_structure.ml

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,15 @@ let rec addAnnotationsToTypes_ ~config ~(expr : Typedtree.expression)
77
let rec zip (params : Typedtree.function_param list)
88
(arg_types : arg_type list) =
99
match (params, arg_types) with
10-
| {fp_lbl; fp_param} :: rest_params, {a_type} :: rest_types ->
11-
let a_name = Ident.name fp_param in
10+
| ( {fp_lbl; fp_param; fp_has_default} :: rest_params,
11+
{a_type} :: rest_types ) ->
1212
let a_name =
13-
(* optional parameters with a default are bound to a synthetic
14-
[*opt_<label>*] variable; show the label instead *)
15-
if String.length a_name >= 4 && String.sub a_name 0 4 = "*opt" then
16-
match fp_lbl with
17-
| Optional {txt = l} -> l
18-
| _ -> "" (* should not happen *)
19-
else a_name
13+
match fp_lbl with
14+
| Optional {txt = l} when fp_has_default ->
15+
(* the compiled parameter is a synthetic option carrier; show the
16+
label instead *)
17+
l
18+
| _ -> Ident.name fp_param
2019
in
2120
{a_name; a_type} :: zip rest_params rest_types
2221
| [], rest -> rest |> addAnnotationsToTypes_ ~config ~expr:body

compiler/ml/matching.ml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2404,8 +2404,10 @@ let rec lower_bind v arg lam =
24042404
| Llet (Alias, k, vv, lv, l) ->
24052405
if approx_present v lv then bind Alias v arg lam
24062406
else Llet (Alias, k, vv, lv, lower_bind v arg l)
2407-
| Lvar u when Ident.same u v && Ident.name u = "*sth*" ->
2408-
arg (* eliminate let *sth* = from_option x in *sth* *)
2407+
| Lvar u when Ident.same u v ->
2408+
(* eliminate [let v = arg in v]; [lower_bind] is only used for alias
2409+
bindings, so [arg] is pure *)
2410+
arg
24092411
| _ -> bind Alias v arg lam
24102412

24112413
let bind_check str v arg lam =

compiler/ml/typecore.ml

Lines changed: 84 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2441,10 +2441,9 @@ and type_expect_ ?deprecated_context ~context ?(recarg = Rejected) env sexp
24412441
ty_expected
24422442
| Pexp_let (rec_flag, spat_sexp_list, sbody) ->
24432443
let scp =
2444-
match (sexp.pexp_attributes, rec_flag) with
2445-
| [({txt = "#default"}, _)], _ -> None
2446-
| _, Recursive -> Some (Annot.Idef loc)
2447-
| _, Nonrecursive -> Some (Annot.Idef sbody.pexp_loc)
2444+
match rec_flag with
2445+
| Recursive -> Some (Annot.Idef loc)
2446+
| Nonrecursive -> Some (Annot.Idef sbody.pexp_loc)
24482447
in
24492448
let pat_exp_list, new_env, unpacks =
24502449
type_let ~context:None env rec_flag spat_sexp_list scp true
@@ -3530,7 +3529,7 @@ and type_function ~async loc attrs env ty_expected_
35303529
(sparams : Parsetree.fun_param list) sbody =
35313530
(* Desugar optional-parameter defaults: the parameter becomes a fresh
35323531
[*opt_<label>*] variable and the original pattern is bound in a
3533-
[#default]-annotated let at the head of the body. The let syntax is kept
3532+
let at the head of the body. The let syntax is kept
35343533
alongside its parameter rather than wrapped into the body here: each
35353534
binding is typed while the parameter environment accumulates left to
35363535
right, so a default only sees the parameters before it (typing it as
@@ -3722,20 +3721,77 @@ and type_function ~async loc attrs env ty_expected_
37223721
parameter enters the environment, mirroring the curried per-level
37233722
typing this replaced: a default sees only the parameters to its
37243723
left, and its pattern's bindings are in scope for what follows. *)
3725-
let ext_env, unpacks, defaults_acc =
3724+
let pat, ext_env, unpacks, defaults_acc, opt_param =
37263725
match default_binding with
3727-
| None -> (ext_env, unpacks, defaults_acc)
3726+
| None -> (pat, ext_env, unpacks, defaults_acc, None)
37283727
| Some vb ->
37293728
let let_env = ext_env in
37303729
let pat_exp_list, ext_env, let_unpacks =
37313730
type_let ~context:None ext_env Nonrecursive [vb] None true
37323731
in
3733-
( ext_env,
3732+
(* The pattern binds the option carrier under an unspellable name
3733+
([*opt_<label>*]), which typing needs so that user code can
3734+
neither reference nor shadow it. Everything mentioning the
3735+
carrier has been typed at this point: replace it with a fresh
3736+
user-legible parameter ident derived from the label, both at its
3737+
binder and at its single use, the scrutinee of the synthetic
3738+
match. The carrier then never reaches the lambda layer. *)
3739+
let param_id = Ident.create (label_name p.p_lbl ^ "Opt") in
3740+
let pat =
3741+
match pat.pat_desc with
3742+
| Tpat_var (_, name) ->
3743+
{
3744+
pat with
3745+
pat_desc =
3746+
Tpat_var (param_id, {name with txt = Ident.name param_id});
3747+
}
3748+
| _ -> assert false
3749+
in
3750+
let pat_exp_list =
3751+
match pat_exp_list with
3752+
| [
3753+
({
3754+
vb_expr =
3755+
{
3756+
exp_desc =
3757+
Texp_match
3758+
( ({exp_desc = Texp_ident (Path.Pident _, lid, vd)} as
3759+
scrut),
3760+
cases,
3761+
exn_cases,
3762+
m_partial );
3763+
} as vb_expr;
3764+
} as vb);
3765+
] ->
3766+
[
3767+
{
3768+
vb with
3769+
vb_expr =
3770+
{
3771+
vb_expr with
3772+
exp_desc =
3773+
Texp_match
3774+
( {
3775+
scrut with
3776+
exp_desc =
3777+
Texp_ident (Path.Pident param_id, lid, vd);
3778+
},
3779+
cases,
3780+
exn_cases,
3781+
m_partial );
3782+
};
3783+
};
3784+
]
3785+
| _ -> assert false
3786+
in
3787+
( pat,
3788+
ext_env,
37343789
unpacks @ let_unpacks,
3735-
(pat_exp_list, let_env) :: defaults_acc )
3790+
(pat_exp_list, let_env) :: defaults_acc,
3791+
Some param_id )
37363792
in
37373793
type_params
3738-
((p, pat, ty_arg_c) :: typed_acc)
3794+
((p, opt_param, pat, ty_arg_c) :: typed_acc)
37393795
ext_env (unpacks_acc @ unpacks) defaults_acc rest_params rest_tys
37403796
| _ -> assert false
37413797
in
@@ -3754,8 +3810,8 @@ and type_function ~async loc attrs env ty_expected_
37543810
let ty_res' = instance env ty_res in
37553811
unify_exp ~context:None env body_exp ty_res');
37563812
(* Stack the typed default bindings back onto the body, leftmost parameter
3757-
outermost — the same [#default] let shape [type_expect] would have
3758-
produced had the lets been part of the body's syntax. *)
3813+
outermost — the same let shape [type_expect] would have produced had the
3814+
lets been part of the body's syntax. *)
37593815
let body_exp =
37603816
List.fold_right
37613817
(fun (pat_exp_list, let_env) inner ->
@@ -3765,22 +3821,22 @@ and type_function ~async loc attrs env ty_expected_
37653821
exp_loc = loc;
37663822
exp_extra = [];
37673823
exp_type = inner.exp_type;
3768-
exp_attributes = [(mknoloc "#default", Parsetree.PStr [])];
3824+
exp_attributes = [];
37693825
exp_env = let_env;
37703826
})
37713827
default_lets body_exp
37723828
in
37733829
let needs_exhaust_check =
37743830
List.exists
3775-
(fun ((p : Parsetree.fun_param), _, _) -> not (is_var p.p_pat))
3831+
(fun ((p : Parsetree.fun_param), _, _, _) -> not (is_var p.p_pat))
37763832
typed_params
37773833
in
37783834
let do_init = has_gadts || needs_exhaust_check in
37793835
let lev, env = if do_init && not has_gadts then init_env () else (lev, env) in
37803836
ignore lev;
37813837
let tparams =
37823838
List.map
3783-
(fun ((p : Parsetree.fun_param), pat, ty_arg_c) ->
3839+
(fun ((p : Parsetree.fun_param), opt_param, pat, ty_arg_c) ->
37843840
let case = {c_lhs = pat; c_guard = None; c_rhs = body_exp} in
37853841
let ty_arg_check =
37863842
if do_init then
@@ -3796,11 +3852,17 @@ and type_function ~async loc attrs env ty_expected_
37963852
if contains_polyvars || do_init then
37973853
Delayed_checks.add_delayed_check unused_check
37983854
else unused_check ();
3855+
let fp_param =
3856+
match opt_param with
3857+
| Some id -> id
3858+
| None -> name_pattern "param" [case]
3859+
in
37993860
{
38003861
fp_lbl = p.p_lbl;
3801-
fp_param = name_pattern "param" [case];
3862+
fp_param;
38023863
fp_pat = pat;
38033864
fp_partial = partial;
3865+
fp_has_default = opt_param <> None;
38043866
})
38053867
typed_params
38063868
in
@@ -4516,18 +4578,13 @@ and type_let ~context ?(check = fun s -> Warnings.Unused_var s)
45164578
in
45174579
begin_def ();
45184580
let is_fake_let =
4581+
(* the synthetic let-declaration introduced for an optional parameter's
4582+
default: its variables are parameters from the user's point of view,
4583+
so an unused one gets the strict (parameter) warning *)
45194584
match spat_sexp_list with
4520-
| [
4521-
{
4522-
pvb_expr =
4523-
{
4524-
pexp_desc =
4525-
Pexp_match
4526-
({pexp_desc = Pexp_ident {txt = Longident.Lident "*opt*"}}, _);
4527-
};
4528-
};
4529-
] ->
4530-
true (* the fake let-declaration introduced by fun ?(x = e) -> ... *)
4585+
| [{pvb_expr = {pexp_attributes}}] ->
4586+
Ext_list.exists pexp_attributes (fun ({txt}, _) ->
4587+
txt = "#optional_arg_default")
45314588
| _ -> false
45324589
in
45334590
let check = if is_fake_let then check_strict else check in

compiler/ml/typedtree.ml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ and function_param = {
144144
fp_param: Ident.t; (* the name the compiled parameter binds to *)
145145
fp_pat: pattern;
146146
fp_partial: partial; (* whether [fp_pat] is exhaustive *)
147+
fp_has_default: bool;
148+
(* optional parameter with a default value. [fp_pat] then binds the
149+
option carrier directly to [fp_param] -- a fresh ident named after
150+
the label -- and the user's pattern is bound by a let at the head
151+
of the body. *)
147152
}
148153

149154
and record_label_definition =

compiler/ml/typedtree.mli

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ and function_param = {
245245
fp_param: Ident.t; (* the name the compiled parameter binds to *)
246246
fp_pat: pattern;
247247
fp_partial: partial; (* whether [fp_pat] is exhaustive *)
248+
fp_has_default: bool;
249+
(* optional parameter with a default value. [fp_pat] then binds the
250+
option carrier directly to [fp_param] -- a fresh ident named after
251+
the label -- and the user's pattern is bound by a let at the head
252+
of the body. *)
248253
}
249254

250255
and record_label_definition =

tests/tests/src/mario_game.mjs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@ let Actors = {};
2525

2626
let Dom_html = {};
2727

28-
function setup_sprite(loopOpt, bbox_offsetOpt, bbox_sizeOpt, img_src, max_frames, max_ticks, frame_size, src_offset) {
28+
function setup_sprite(loopOpt, bb_offOpt, bb_szOpt, img_src, max_frames, max_ticks, frame_size, src_offset) {
2929
let loop = loopOpt !== undefined ? loopOpt : true;
30-
let bbox_offset = bbox_offsetOpt !== undefined ? bbox_offsetOpt : [
30+
let bbox_offset = bb_offOpt !== undefined ? bb_offOpt : [
3131
0,
3232
0
3333
];
34-
let bbox_size = bbox_sizeOpt !== undefined ? bbox_sizeOpt : [
34+
let bbox_size = bb_szOpt !== undefined ? bb_szOpt : [
3535
0,
3636
0
3737
];
@@ -794,9 +794,9 @@ let id_counter = {
794794
contents: Stdlib_Int.Constants.minValue
795795
};
796796

797-
function setup_obj(has_gravityOpt, speedOpt, param) {
798-
let has_gravity = has_gravityOpt !== undefined ? has_gravityOpt : true;
799-
let speed = speedOpt !== undefined ? speedOpt : 1;
797+
function setup_obj(gOpt, spdOpt, param) {
798+
let has_gravity = gOpt !== undefined ? gOpt : true;
799+
let speed = spdOpt !== undefined ? spdOpt : 1;
800800
return {
801801
has_gravity: has_gravity,
802802
speed: speed
@@ -847,9 +847,9 @@ function new_id() {
847847
return id_counter.contents;
848848
}
849849

850-
function make$2($staropt_id$star, $staropt_dir$star, spawnable, context, param) {
851-
let id = $staropt_id$star !== undefined ? Primitive_option.valFromOption($staropt_id$star) : undefined;
852-
let dir = $staropt_dir$star !== undefined ? $staropt_dir$star : "Left";
850+
function make$2(idOpt, dirOpt, spawnable, context, param) {
851+
let id = idOpt !== undefined ? Primitive_option.valFromOption(idOpt) : undefined;
852+
let dir = dirOpt !== undefined ? dirOpt : "Left";
853853
let spr = make(spawnable, dir, context);
854854
let params = make_type$2(spawnable);
855855
let id$1 = id !== undefined ? id : new_id();

0 commit comments

Comments
 (0)