Skip to content

Commit 799a82f

Browse files
cknittclaude
andauthored
Parse attributes after = private and a doc comment before a manifest (#9115)
Why: `private @attr {x: int}` took the type expression path and rejected the record, and a doc comment right after `=` always started a constructor list, so `/** d */ int` failed. Both now use the lookahead that attributes after `=` already had: past attributes and doc comments, a record, `..`, `|` or constructor is a representation, anything else a manifest. AST modified (doc comment before a manifest): Example: type z = /** d */ int Before (diagnostic): Did you mean `Int` instead of `int`? After (parse): type_declaration "z" ptype_kind = Ptype_abstract ptype_private = Public ptype_manifest = Some core_type attribute "res.doc" … Ptyp_constr "int" Format modified (attribute after `private`): Example: type t = private @attr {x: int} type u = private /** d */ int Before (diagnostic): An inline record type declaration is only allowed in a variant constructor's declaration or nested inside of a record type declaration After (rescript): @attr type t = private {x: int} type u = private /** d */ int Review: Does a leading doc comment change any form that parsed before? No. After a doc comment the lookahead keeps the representation for every start the constructor and record parsers accept (`|`, a constructor, `...base`, `{...`, a record, `..`); only starts they rejected, such as `int`, `M.t`, `'a`, `(a, b)`, `[#A]`, `{.}` or `{"a": int}`, become a manifest. A differential run over 672 declarations (prefixes x bodies, with and without `private`, .res and .resi) gives the same tree as before for every one that parsed. Fixes #9106 Claude-Session: https://claude.ai/code/session_01Kbf3gGkPma9LAZimuQjnrP Signed-off-by: Christoph Knittel <ck@cca.io> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d7eb937 commit 799a82f

6 files changed

Lines changed: 218 additions & 39 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- Accept attributes and doc comments after `= private`, and a doc comment after `=` in front of a type expression such as `int`.

‎compiler/syntax/src/res_core.ml‎

Lines changed: 57 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5799,6 +5799,11 @@ and parse_private_eq_or_repr p =
57995799
| Bar | DotDot ->
58005800
let _, kind, attrs = parse_type_representation p in
58015801
(None, Asttypes.Private, kind, attrs)
5802+
| (At | DocComment _) when is_representation_after_attrs p ->
5803+
let _, kind, attrs = parse_type_representation p in
5804+
(None, Asttypes.Private, kind, attrs)
5805+
| DocComment _ ->
5806+
(Some (parse_typ_expr p), Asttypes.Private, Parsetree.Ptype_abstract, [])
58025807
| t when Grammar.is_typ_expr_start t ->
58035808
(Some (parse_typ_expr p), Asttypes.Private, Parsetree.Ptype_abstract, [])
58045809
| _ ->
@@ -6019,6 +6024,55 @@ and parse_polymorphic_variant_type_args p =
60196024
Parser.expect Rparen p;
60206025
Location.mkloc args (mk_loc start_pos (Parser.position p))
60216026

6027+
(* Attributes and doc comments after `=` (or `= private`) can start a
6028+
representation (variant/record/open variant) or a manifest. Look ahead past
6029+
them: if a representation-like token follows, it is a representation;
6030+
otherwise a manifest type expression. *)
6031+
and is_representation_after_attrs p =
6032+
let starts_with_doc_comment =
6033+
match Parser.peek p with
6034+
| DocComment _ -> true
6035+
| _ -> false
6036+
in
6037+
Parser.lookahead p (fun state ->
6038+
ignore (parse_attributes state);
6039+
(* optionally skip a run of doc comments before deciding *)
6040+
skip_doc_comments state;
6041+
match Parser.peek state with
6042+
| Lbrace -> (
6043+
(* Disambiguate record declaration vs object type.
6044+
Peek inside the braces; if it looks like an object (String/Dot/DotDot/DotDotDot),
6045+
then this is a manifest type expression, not a representation. If it looks like
6046+
a record field (e.g. Lident or attributes before one), treat as representation. *)
6047+
Parser.next state;
6048+
(* consume Lbrace *)
6049+
ignore (parse_attributes state);
6050+
skip_doc_comments state;
6051+
match Parser.peek state with
6052+
| String _ | Dot | DotDot -> false (* object type => manifest *)
6053+
| DotDotDot ->
6054+
(* A spread (`{...base, y: int}`) after a leading doc comment has
6055+
always been parsed as a record declaration, and after a leading
6056+
attribute as an object type. *)
6057+
starts_with_doc_comment
6058+
| _ -> true
6059+
(* record decl => representation *))
6060+
| Bar -> true (* variant constructor list *)
6061+
| DotDot -> true (* extensible/open variant ".." *)
6062+
| DotDotDot ->
6063+
(* A variant spread (`...base | C`) after a leading doc comment has
6064+
always been parsed as a constructor list. *)
6065+
starts_with_doc_comment
6066+
| Uident _ -> (
6067+
(* constructor vs module-qualified manifest *)
6068+
Parser.next state;
6069+
match Parser.peek state with
6070+
| Dot -> false (* M.t => manifest *)
6071+
| _ -> true
6072+
(* Uident starting a constructor *))
6073+
| DocComment _ -> true (* doc before constructor list *)
6074+
| _ -> false)
6075+
60226076
and parse_type_equation_and_representation ?current_type_name_path
60236077
?inline_types_context p =
60246078
match Parser.peek p with
@@ -6034,49 +6088,14 @@ and parse_type_equation_and_representation ?current_type_name_path
60346088
in
60356089
(manifest, priv, kind, [])
60366090
| Private -> parse_private_eq_or_repr p
6037-
| Bar | DotDot | DocComment _ ->
6091+
| Bar | DotDot ->
60386092
let priv, kind, attrs =
60396093
parse_type_representation ?current_type_name_path ?inline_types_context
60406094
p
60416095
in
60426096
(None, priv, kind, attrs)
6043-
| At -> (
6044-
(* Attributes can start a representation (variant/record/open variant) or a manifest.
6045-
Look ahead past attributes (and doc comments). If a representation-like token follows,
6046-
parse it as a representation; otherwise treat as a manifest. *)
6047-
let is_representation_after_attrs =
6048-
Parser.lookahead p (fun state ->
6049-
ignore (parse_attributes state);
6050-
(* optionally skip a run of doc comments before deciding *)
6051-
skip_doc_comments state;
6052-
match Parser.peek state with
6053-
| Lbrace -> (
6054-
(* Disambiguate record declaration vs object type.
6055-
Peek inside the braces; if it looks like an object (String/Dot/DotDot/DotDotDot),
6056-
then this is a manifest type expression, not a representation. If it looks like
6057-
a record field (e.g. Lident or attributes before one), treat as representation. *)
6058-
Parser.next state;
6059-
(* consume Lbrace *)
6060-
ignore (parse_attributes state);
6061-
skip_doc_comments state;
6062-
match Parser.peek state with
6063-
| String _ | Dot | DotDot | DotDotDot ->
6064-
false (* object type => manifest *)
6065-
| _ -> true
6066-
(* record decl => representation *))
6067-
| Bar -> true (* variant constructor list *)
6068-
| DotDot -> true (* extensible/open variant ".." *)
6069-
| Uident _ -> (
6070-
(* constructor vs module-qualified manifest *)
6071-
Parser.next state;
6072-
match Parser.peek state with
6073-
| Dot -> false (* M.t => manifest *)
6074-
| _ -> true
6075-
(* Uident starting a constructor *))
6076-
| DocComment _ -> true (* doc before constructor list *)
6077-
| _ -> false)
6078-
in
6079-
if is_representation_after_attrs then
6097+
| At | DocComment _ -> (
6098+
if is_representation_after_attrs p then
60806099
let priv, kind, attrs =
60816100
parse_type_representation ?current_type_name_path
60826101
?inline_types_context p

‎tests/syntax_tests/data/parsing/grammar/typedefinition/expected/typeRepresentation.res.txt‎

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,4 +79,73 @@ type nonrec t =
7979
| B [@res.doc {js|doc2|js}]
8080
type nonrec t =
8181
| A [@res.doc {js|doc|js}][@attr ]
82-
| B
82+
| B
83+
type nonrec t = private {
84+
x: int }[@@attr ]
85+
type nonrec t = private ..[@@attr ]
86+
type nonrec t = private
87+
| A [@attr ]
88+
| B
89+
type nonrec t = private ((int)[@attr ])
90+
type nonrec t = private ((< a: int > )[@attr ])
91+
type nonrec t = private ((int)[@res.doc {js|doc|js}])
92+
type nonrec t = private ((M.t)[@res.doc {js|doc|js}])
93+
type nonrec t = private
94+
| A [@res.doc {js|doc|js}]
95+
| B
96+
type nonrec t = private {
97+
x: int }[@@res.doc {js|doc|js}][@@attr ]
98+
type nonrec t = ((int)[@res.doc {js|doc|js}])
99+
type nonrec t = ((< a: int > )[@res.doc {js|doc|js}])
100+
type nonrec t = (((int * int))[@res.doc {js|doc|js}])
101+
type nonrec t = (([ `A ])[@res.doc {js|doc|js}])
102+
type nonrec t = ((M.t)[@res.doc {js|doc|js}])
103+
type nonrec t = ((M.t)[@res.doc {js|doc|js}]) =
104+
| A
105+
| B
106+
type nonrec t = ((int)[@res.doc {js|doc|js}][@attr ])
107+
type nonrec t = {
108+
...: base ;
109+
y: int }[@@res.doc {js|doc|js}]
110+
type nonrec t = {
111+
...: base }[@@res.doc {js|doc|js}]
112+
type nonrec t = {
113+
...: base ;
114+
y: int }[@@res.doc {js|doc|js}][@@attr ]
115+
type nonrec t = ((< .. > )[@res.doc {js|doc|js}])
116+
type nonrec t = ((< > )[@res.doc {js|doc|js}])
117+
type nonrec t = ((< a: int ;.. > )[@res.doc {js|doc|js}])
118+
type nonrec t = ((< base ;y: int > )[@attr ])
119+
type nonrec t = ((< base > )[@attr ])
120+
type nonrec t = private {
121+
...: base ;
122+
y: int }[@@res.doc {js|doc|js}]
123+
type nonrec t = private {
124+
...: base }[@@res.doc {js|doc|js}]
125+
type nonrec t = private {
126+
...: base ;
127+
y: int }[@@res.doc {js|doc|js}][@@attr ]
128+
type nonrec t = private ((< .. > )[@res.doc {js|doc|js}])
129+
type nonrec t = private ((< a: int ;.. > )[@res.doc {js|doc|js}])
130+
type nonrec t = private ((< base ;y: int > )[@attr ])
131+
type nonrec t = private ((< base > )[@attr ])
132+
type nonrec t =
133+
| ... of base [@res.doc {js|doc|js}]
134+
| C
135+
type nonrec t =
136+
| ... of base [@res.doc {js|doc|js}][@res.doc {js|doc2|js}]
137+
| C
138+
type nonrec t =
139+
| ... of base [@res.doc {js|doc|js}][@attr ]
140+
| C
141+
type nonrec t = private
142+
| ... of base [@res.doc {js|doc|js}]
143+
| C
144+
type nonrec t = private
145+
| ... of base [@res.doc {js|doc|js}][@attr ]
146+
| C
147+
type nonrec t = (('a)[@res.doc {js|doc|js}])
148+
type nonrec t = ((int)[@res.doc {js|doc|js}]) -> int (a:1)
149+
type nonrec t = ((M.t)[@res.doc {js|doc|js}]) = {
150+
x: int }
151+
type nonrec t = private (('a Dict.t)[@res.doc {js|doc|js}])

‎tests/syntax_tests/data/parsing/grammar/typedefinition/typeRepresentation.res‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,45 @@ type t = private /**doc*/ {x: int}
4545
type t = /**doc*/ A | B
4646
type t = /**doc*/ | A | /**doc2*/ B
4747
type t = /**doc*/ @attr A | B
48+
49+
// attributes and doc comments after `private`, and doc comments before a manifest
50+
type t = private @attr {x: int}
51+
type t = private @attr ..
52+
type t = private @attr A | B
53+
type t = private @attr int
54+
type t = private @attr {"a": int}
55+
type t = private /**doc*/ int
56+
type t = private /**doc*/ M.t
57+
type t = private /**doc*/ A | B
58+
type t = private /**doc*/ @attr {x: int}
59+
type t = /**doc*/ int
60+
type t = /**doc*/ {"a": int}
61+
type t = /**doc*/ (int, int)
62+
type t = /**doc*/ [#A]
63+
type t = /**doc*/ M.t
64+
type t = /**doc*/ M.t = A | B
65+
type t = /**doc*/ @attr int
66+
type t = /**doc*/ {...base, y: int}
67+
type t = /**doc*/ {...base}
68+
type t = /**doc*/ @attr {...base, y: int}
69+
type t = /**doc*/ {..}
70+
type t = /**doc*/ {.}
71+
type t = /**doc*/ {.. "a": int}
72+
type t = @attr {...base, "y": int}
73+
type t = @attr {...base}
74+
type t = private /**doc*/ {...base, y: int}
75+
type t = private /**doc*/ {...base}
76+
type t = private /**doc*/ @attr {...base, y: int}
77+
type t = private /**doc*/ {..}
78+
type t = private /**doc*/ {.. "a": int}
79+
type t = private @attr {...base, "y": int}
80+
type t = private @attr {...base}
81+
type t = /**doc*/ ...base | C
82+
type t = /**doc*/ /**doc2*/ ...base | C
83+
type t = /**doc*/ @attr ...base | C
84+
type t = private /**doc*/ ...base | C
85+
type t = private /**doc*/ @attr ...base | C
86+
type t = /**doc*/ 'a
87+
type t = /**doc*/ int => int
88+
type t = /**doc*/ M.t = {x: int}
89+
type t = private /**doc*/ Dict.t<'a>

‎tests/syntax_tests/data/printer/typeDef/attributes.res‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,20 @@ type t = /** d1 */ @attr /** d2 */ {x: int}
2727
type t = /* c1 */ /** doc */ /* c2 */ {x: int} // trail
2828
type t = /** doc */ A | B
2929
type t = | /** doc */ A | /** doc2 */ B
30+
type t = private @attr {x: int}
31+
type t = private /** doc */ @attr ..
32+
type t = private @attr int
33+
type t = private /** doc */ int
34+
type t = private /** doc */ A | B
35+
type t = /** doc */ int
36+
type t = /** doc */ {"a": int}
37+
type t = /** doc */ (int, int)
38+
type t = /** doc */ [#A]
39+
type t = /** doc */ M.t = A | B
40+
type t = /** doc */ {...base, y: int}
41+
type t = /** doc */ {.. "a": int}
42+
type t = private /** doc */ {...base, y: int}
43+
type t = private /** doc */ {.}
44+
type t = /** doc */ ...base | C
45+
type t = private /** doc */ ...base | C
46+
type t = /** doc */ 'a

‎tests/syntax_tests/data/printer/typeDef/expected/attributes.res.txt‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,34 @@ type t =
4444
| A
4545
/** doc2 */
4646
| B
47+
@attr type t = private {x: int}
48+
/** doc */
49+
@attr type t = private ..
50+
type t = private @attr int
51+
type t = private /** doc */ int
52+
type t =
53+
private
54+
/** doc */
55+
| A
56+
| B
57+
type t = /** doc */ int
58+
type t = /** doc */ {"a": int}
59+
type t = /** doc */ (int, int)
60+
type t = /** doc */ [#A]
61+
type t = /** doc */ M.t = A | B
62+
/** doc */
63+
type t = {...base, y: int}
64+
type t = /** doc */ {.."a": int}
65+
/** doc */
66+
type t = private {...base, y: int}
67+
type t = private /** doc */ {.}
68+
type t =
69+
/** doc */
70+
| ...base
71+
| C
72+
type t =
73+
private
74+
/** doc */
75+
| ...base
76+
| C
77+
type t = /** doc */ 'a

0 commit comments

Comments
 (0)