Skip to content

Commit 2bcece4

Browse files
committed
Infer record vs object at type checking time.
Some syntax is ambiguous: `type t = {...t1, ...t2}` could be either a record or an object, and there's no way to tell. This PR assumes it is a record and at type-checking time detects if it is not, in which case it type checks the definition as an object. This is an attempt to get around ambiguity in the syntax that does not seem to be solvable given how crowded the syntax real estate of the language already is.
1 parent 0202d6e commit 2bcece4

File tree

14 files changed

+161
-85
lines changed

14 files changed

+161
-85
lines changed

jscomp/frontend/ast_attributes.ml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,6 @@ let iter_process_bs_string_or_int_as (attrs : Parsetree.attributes) =
332332
| _ -> ());
333333
!st
334334

335-
336335
let locg = Location.none
337336
(* let bs : attr
338337
= {txt = "bs" ; loc = locg}, Ast_payload.empty *)

jscomp/frontend/lam_constant.ml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
* along with this program; if not, write to the Free Software
2323
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)
2424

25-
type constructor_tag = {cstr_name: Ast_untagged_variants.literal; const: int; non_const: int}
25+
type constructor_tag = {
26+
cstr_name: Ast_untagged_variants.literal;
27+
const: int;
28+
non_const: int;
29+
}
2630

2731
type pointer_info =
2832
| None

jscomp/frontend/lam_constant.mli

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
* along with this program; if not, write to the Free Software
2323
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)
2424

25-
type constructor_tag = {cstr_name: Ast_untagged_variants.literal; const: int; non_const: int}
25+
type constructor_tag = {
26+
cstr_name: Ast_untagged_variants.literal;
27+
const: int;
28+
non_const: int;
29+
}
2630

2731
type pointer_info =
2832
| None

jscomp/ml/typedecl.ml

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ let make_constructor env type_path type_params sargs sret_type =
290290
*)
291291

292292

293-
let transl_declaration env sdecl id =
293+
let transl_declaration ~foundObject env sdecl id =
294294
(* Bind type parameters *)
295295
reset_type_variables();
296296
Ctype.begin_def ();
@@ -358,9 +358,9 @@ let transl_declaration env sdecl id =
358358
unboxed_false_default_false
359359
in
360360
let unbox = unboxed_status.unboxed in
361-
let (tkind, kind) =
361+
let (tkind, kind, sdecl) =
362362
match sdecl.ptype_kind with
363-
| Ptype_abstract -> Ttype_abstract, Type_abstract
363+
| Ptype_abstract -> Ttype_abstract, Type_abstract, sdecl
364364
| Ptype_variant scstrs ->
365365
assert (scstrs <> []);
366366
if List.exists (fun cstr -> cstr.pcd_res <> None) scstrs then begin
@@ -423,15 +423,15 @@ let transl_declaration env sdecl id =
423423
let tcstrs, cstrs = List.split (List.map make_cstr scstrs) in
424424
let isUntaggedDef = Ast_untagged_variants.has_untagged sdecl.ptype_attributes in
425425
Ast_untagged_variants.check_well_formed ~isUntaggedDef cstrs;
426-
Ttype_variant tcstrs, Type_variant cstrs
427-
| Ptype_record lbls ->
426+
Ttype_variant tcstrs, Type_variant cstrs, sdecl
427+
| Ptype_record lbls_ ->
428428
let has_optional attrs = Ext_list.exists attrs (fun ({txt },_) -> txt = "res.optional") in
429429
let optionalLabels =
430-
Ext_list.filter_map lbls
430+
Ext_list.filter_map lbls_
431431
(fun lbl -> if has_optional lbl.pld_attributes then Some lbl.pld_name.txt else None) in
432432
let lbls =
433-
if optionalLabels = [] then lbls
434-
else Ext_list.map lbls (fun lbl ->
433+
if optionalLabels = [] then lbls_
434+
else Ext_list.map lbls_ (fun lbl ->
435435
let typ = lbl.pld_type in
436436
let typ =
437437
if has_optional lbl.pld_attributes then
@@ -446,7 +446,7 @@ let transl_declaration env sdecl id =
446446
then Record_optional_labels optionalLabels
447447
else Record_regular
448448
in
449-
let lbls, lbls' = match lbls, lbls' with
449+
let lbls_opt = match lbls, lbls' with
450450
| {ld_name = {txt = "..."}; ld_type} :: _, _ :: _ ->
451451
let rec extract t = match t.desc with
452452
| Tpoly(t, []) -> extract t
@@ -464,21 +464,36 @@ let transl_declaration env sdecl id =
464464
(_p0, _p, {type_kind=Type_record (fields, _repr)}) ->
465465
process_lbls (fst acc @ (fields |> List.map mkLbl), snd acc @ fields) rest rest'
466466
| _ -> assert false
467-
| exception _ -> assert false)
467+
| exception _ -> None)
468468
| lbl::rest, lbl'::rest' -> process_lbls (fst acc @ [lbl], snd acc @ [lbl']) rest rest'
469-
| _ -> acc
469+
| _ -> Some acc
470470
in
471471
process_lbls ([], []) lbls lbls'
472-
| _ -> lbls, lbls' in
472+
| _ -> Some (lbls, lbls') in
473473
let rec check_duplicates (lbls : Typedtree.label_declaration list) seen = match lbls with
474474
| [] -> ()
475475
| lbl::rest ->
476476
let name = lbl.ld_id.name in
477477
if StringSet.mem name seen then raise(Error(lbl.ld_loc, Duplicate_label name));
478478
check_duplicates rest (StringSet.add name seen) in
479-
check_duplicates lbls StringSet.empty;
480-
Ttype_record lbls, Type_record(lbls', rep)
481-
| Ptype_open -> Ttype_open, Type_open
479+
(match lbls_opt with
480+
| Some (lbls, lbls') ->
481+
check_duplicates lbls StringSet.empty;
482+
Ttype_record lbls, Type_record(lbls', rep), sdecl
483+
| None ->
484+
(* Could not fine type decl for ...t: assume t is an object type and this is syntax ambiguity *)
485+
foundObject := true;
486+
let fields = Ext_list.map lbls_ (fun ld ->
487+
match ld.pld_name.txt with
488+
| "..." -> Parsetree.Oinherit ld.pld_type
489+
| _ -> Otag (ld.pld_name, ld.pld_attributes, ld.pld_type)) in
490+
let sdecl =
491+
{sdecl with
492+
ptype_kind = Ptype_abstract;
493+
ptype_manifest = Some (Ast_helper.Typ.object_ ~loc:sdecl.ptype_loc fields Closed);
494+
} in
495+
(Ttype_abstract, Type_abstract, sdecl))
496+
| Ptype_open -> Ttype_open, Type_open, sdecl
482497
in
483498
let (tman, man) = match sdecl.ptype_manifest with
484499
None -> None, None
@@ -587,7 +602,7 @@ let check_constraints_labels env visited l pl =
587602
check_constraints_rec env (get_loc (Ident.name name) pl) visited ty)
588603
l
589604

590-
let check_constraints env sdecl (_, decl) =
605+
let check_constraints ~foundObject env sdecl (_, decl) =
591606
let visited = ref TypeSet.empty in
592607
begin match decl.type_kind with
593608
| Type_abstract -> ()
@@ -636,10 +651,12 @@ let check_constraints env sdecl (_, decl) =
636651
begin match decl.type_manifest with
637652
| None -> ()
638653
| Some ty ->
654+
if not !foundObject then
639655
let sty =
640656
match sdecl.ptype_manifest with Some sty -> sty | _ -> assert false
641657
in
642658
check_constraints_rec env sty.ptyp_loc visited ty
659+
643660
end
644661

645662
(*
@@ -1294,14 +1311,15 @@ let transl_type_decl env rec_flag sdecl_list =
12941311
| Asttypes.Recursive | Asttypes.Nonrecursive ->
12951312
id, None
12961313
in
1314+
let foundObject = ref false in
12971315
let transl_declaration name_sdecl (id, slot) =
12981316
current_slot := slot;
12991317
Builtin_attributes.warning_scope
13001318
name_sdecl.ptype_attributes
13011319
(fun () -> transl_declaration temp_env name_sdecl id)
13021320
in
13031321
let tdecls =
1304-
List.map2 transl_declaration sdecl_list (List.map id_slots id_list) in
1322+
List.map2 (transl_declaration ~foundObject) sdecl_list (List.map id_slots id_list) in
13051323
let decls =
13061324
List.map (fun tdecl -> (tdecl.typ_id, tdecl.typ_type)) tdecls in
13071325
current_slot := None;
@@ -1349,7 +1367,7 @@ let transl_type_decl env rec_flag sdecl_list =
13491367
| None -> ())
13501368
sdecl_list tdecls;
13511369
(* Check that constraints are enforced *)
1352-
List.iter2 (check_constraints newenv) sdecl_list decls;
1370+
List.iter2 (check_constraints ~foundObject newenv) sdecl_list decls;
13531371
(* Name recursion *)
13541372
let decls =
13551373
List.map2 (fun sdecl (id, decl) -> id, name_recursion sdecl id decl)

jscomp/test/RecordOrObject.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
4+
var vxy = {
5+
x: 10,
6+
y: "abc"
7+
};
8+
9+
var xxi = {
10+
x: 10
11+
};
12+
13+
exports.vxy = vxy;
14+
exports.xxi = xxi;
15+
/* No side effect */

jscomp/test/RecordOrObject.res

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
type rx = {x: int}
2+
type ry = {y: string}
3+
type rxi = {...rx, i?:int}
4+
type rxy = {...rx, ...ry} // this is a record
5+
6+
let vxy: rxy = {x: 10, y: "abc"}
7+
let xxi : rxi = {x:10}
8+
9+
type ox = {"x": int}
10+
type oy = {"y": int}
11+
type oxz = {...ox, "z": int}
12+
type oxy = {...ox, ...oy} // this starts as a record but type checking infers that it is an object

jscomp/test/build.ninja

Lines changed: 2 additions & 1 deletion
Large diffs are not rendered by default.

res_syntax/src/res_core.ml

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4453,14 +4453,28 @@ and parseFieldDeclaration p =
44534453
let loc = mkLoc startPos typ.ptyp_loc.loc_end in
44544454
(optional, Ast_helper.Type.field ~attrs ~loc ~mut name typ)
44554455

4456-
and parseFieldDeclarationRegion p =
4456+
and parseFieldDeclarationRegion ?foundObjectField p =
44574457
let startPos = p.Parser.startPos in
44584458
let attrs = parseAttributes p in
44594459
let mut =
44604460
if Parser.optional p Token.Mutable then Asttypes.Mutable
44614461
else Asttypes.Immutable
44624462
in
44634463
match p.token with
4464+
| DotDotDot ->
4465+
Parser.next p;
4466+
let name = Location.mkloc "..." (mkLoc startPos p.prevEndPos) in
4467+
let typ = parsePolyTypeExpr p in
4468+
let loc = mkLoc startPos typ.ptyp_loc.loc_end in
4469+
Some (Ast_helper.Type.field ~attrs ~loc ~mut name typ)
4470+
| String s when foundObjectField <> None ->
4471+
Option.get foundObjectField := true;
4472+
Parser.next p;
4473+
let name = Location.mkloc s (mkLoc startPos p.prevEndPos) in
4474+
Parser.expect Colon p;
4475+
let typ = parsePolyTypeExpr p in
4476+
let loc = mkLoc startPos typ.ptyp_loc.loc_end in
4477+
Some (Ast_helper.Type.field ~attrs ~loc ~mut name typ)
44644478
| Lident _ ->
44654479
let lident, loc = parseLident p in
44664480
let name = Location.mkloc lident loc in
@@ -4969,7 +4983,7 @@ and parseRecordOrObjectDecl p =
49694983
in
49704984
let typ = parseArrowTypeRest ~es6Arrow:true ~startPos typ p in
49714985
(Some typ, Asttypes.Public, Parsetree.Ptype_abstract)
4972-
| DotDotDot ->
4986+
| DotDotDot -> (
49734987
let dotdotdotStart = p.startPos in
49744988
let dotdotdotEnd = p.endPos in
49754989
(* start of object type spreading, e.g. `type u = {...a, "u": int}` *)
@@ -4984,25 +4998,36 @@ and parseRecordOrObjectDecl p =
49844998
Parser.next p
49854999
| _ -> Parser.expect Comma p
49865000
in
4987-
let () =
4988-
match p.token with
4989-
| Lident _ ->
4990-
Parser.err ~startPos:dotdotdotStart ~endPos:dotdotdotEnd p
4991-
(Diagnostics.message ErrorMessages.spreadInRecordDeclaration)
4992-
| _ -> ()
4993-
in
4994-
let fields =
4995-
Parsetree.Oinherit typ
4996-
:: parseCommaDelimitedRegion ~grammar:Grammar.StringFieldDeclarations
4997-
~closing:Rbrace ~f:parseStringFieldDeclaration p
4998-
in
4999-
Parser.expect Rbrace p;
5000-
let loc = mkLoc startPos p.prevEndPos in
5001-
let typ =
5002-
Ast_helper.Typ.object_ ~loc fields Asttypes.Closed |> parseTypeAlias p
5003-
in
5004-
let typ = parseArrowTypeRest ~es6Arrow:true ~startPos typ p in
5005-
(Some typ, Asttypes.Public, Parsetree.Ptype_abstract)
5001+
match p.token with
5002+
| _ ->
5003+
let loc = mkLoc startPos p.prevEndPos in
5004+
let dotField =
5005+
Ast_helper.Type.field ~loc
5006+
{txt = "..."; loc = mkLoc dotdotdotStart dotdotdotEnd}
5007+
typ
5008+
in
5009+
let foundObjectField = ref false in
5010+
let fields =
5011+
parseCommaDelimitedRegion ~grammar:Grammar.RecordDecl ~closing:Rbrace
5012+
~f:(parseFieldDeclarationRegion ~foundObjectField)
5013+
p
5014+
in
5015+
Parser.expect Rbrace p;
5016+
if !foundObjectField then
5017+
let fields =
5018+
Ext_list.map fields (fun ld ->
5019+
match ld.pld_name.txt with
5020+
| "..." -> Parsetree.Oinherit ld.pld_type
5021+
| _ -> Otag (ld.pld_name, ld.pld_attributes, ld.pld_type))
5022+
in
5023+
let dotField = Parsetree.Oinherit typ in
5024+
let typ_obj = Ast_helper.Typ.object_ (dotField :: fields) Closed in
5025+
let typ_obj = parseTypeAlias p typ_obj in
5026+
let typ_obj = parseArrowTypeRest ~es6Arrow:true ~startPos typ_obj p in
5027+
(Some typ_obj, Public, Ptype_abstract)
5028+
else
5029+
let kind = Parsetree.Ptype_record (dotField :: fields) in
5030+
(None, Public, kind))
50065031
| _ -> (
50075032
let attrs = parseAttributes p in
50085033
match p.Parser.token with

res_syntax/src/res_grammar.ml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ let isFieldDeclStart = function
192192
| _ -> false
193193

194194
let isRecordDeclStart = function
195-
| Token.At | Mutable | Lident _ -> true
195+
| Token.At | Mutable | Lident _ | DotDotDot | String _ -> true
196196
| _ -> false
197197

198198
let isTypExprStart = function

res_syntax/src/res_printer.ml

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,9 +1531,12 @@ and printLabelDeclaration ~state (ld : Parsetree.label_declaration) cmtTbl =
15311531
| Mutable -> Doc.text "mutable "
15321532
| Immutable -> Doc.nil
15331533
in
1534-
let name =
1535-
let doc = printIdentLike ld.pld_name.txt in
1536-
printComments doc cmtTbl ld.pld_name.loc
1534+
let name, isDot =
1535+
let doc, isDot =
1536+
if ld.pld_name.txt = "..." then (Doc.text ld.pld_name.txt, true)
1537+
else (printIdentLike ld.pld_name.txt, false)
1538+
in
1539+
(printComments doc cmtTbl ld.pld_name.loc, isDot)
15371540
in
15381541
let optional = printOptionalLabel ld.pld_attributes in
15391542
Doc.group
@@ -1543,7 +1546,7 @@ and printLabelDeclaration ~state (ld : Parsetree.label_declaration) cmtTbl =
15431546
mutableFlag;
15441547
name;
15451548
optional;
1546-
Doc.text ": ";
1549+
(if isDot then Doc.nil else Doc.text ": ");
15471550
printTypExpr ~state ld.pld_type cmtTbl;
15481551
])
15491552

res_syntax/tests/parsing/errors/other/expected/spread.res.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ let [|arr;_|] = [|1;2;3|]
101101
let record = { x with y }
102102
let { x; y } = myRecord
103103
let x::y = myList
104-
type nonrec t = < a >
104+
type nonrec t = {
105+
...: a }
105106
type nonrec t =
106107
| Foo of < a >
107108
type nonrec t = (foo, < x > ) option

0 commit comments

Comments
 (0)