Skip to content

Commit 0a51c27

Browse files
jfrolichclaudecknitt
authored
Restore legacy (. ...) uncurried syntax with deprecation warning (#8383)
* Restore legacy `(. ...)` uncurried syntax with deprecation warning Accept the pre-v11 dotted uncurried syntax in function parameters, arguments, and type parameters so projects depending on libraries that still use it can parse again. Emit a `Warnings.Deprecated` on every occurrence, pointing at the leading dot, similar to the deprecation warnings rewatch emits for `bs-*` fields in rescript.json. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rewatch: surface uncurried-dot deprecation for external deps Rewatch suppresses warnings from external dependencies (users can't act on them), but the legacy `(. ...)` uncurried-syntax deprecation is a signal consumers need to see so they can report breakage upstream before the syntax is removed. Add a small allow-list in the stderr capture path for both the AST-parse phase and the compile phase that keeps warning blocks mentioning that specific deprecation while still dropping everything else from external packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix ocamlformat of res_core.ml and add PR link to changelog Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Rewatch: normalize CRLF before splitting warning blocks The "\n\n\n" block separator used by retain_critical_external_warnings assumes LF line endings. On Windows bsc emits CRLF, so the splitter would find no boundary and return the entire stderr — effectively disabling external-dep warning suppression for any package that emits the uncurried-dot deprecation alongside other warnings. Normalize CRLF → LF before splitting. Add a CRLF test that exercises the Windows-shaped input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Christoph Knittel <ck@cca.io>
1 parent 4f65236 commit 0a51c27

12 files changed

Lines changed: 387 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
#### :nail_care: Polish
4242

4343
- Allow builds while watchers are running. https://github.com/rescript-lang/rescript/pull/8349
44+
- Restore parsing of the legacy `(. ...)` uncurried syntax for backwards compatibility with libraries still on older ReScript versions; emit a deprecation warning when it is used. Rewatch also surfaces this specific deprecation when it originates from an external dependency so users can report breakage upstream. https://github.com/rescript-lang/rescript/pull/8383
4445
- Rewatch: replace wave-based compile scheduler with a work-stealing DAG dispatcher ordered by critical-path priority, avoiding the per-wave stall on the slowest file. https://github.com/rescript-lang/rescript/pull/8374
4546

4647
#### :house: Internal

compiler/syntax/src/res_core.ml

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,19 @@ let tagged_template_literal_attr =
285285
let spread_attr = (Location.mknoloc "res.spread", Parsetree.PStr [])
286286
let dict_spread_attr = (Location.mknoloc "res.dictSpread", Parsetree.PStr [])
287287

288+
(* Emit a deprecation warning when the legacy [(. ...)] uncurried syntax is
289+
encountered. Uncurried is the default since ReScript v11, so the leading
290+
dot is no longer meaningful; we still accept it so dependencies on older
291+
libraries keep parsing. *)
292+
let warn_uncurried_dot_syntax ~loc =
293+
Location.prerr_warning loc
294+
(Warnings.Deprecated
295+
( "The `(. ...)` uncurried syntax is deprecated. Uncurried is now the \
296+
default in ReScript — remove the leading dot.",
297+
loc,
298+
loc,
299+
false ))
300+
288301
type argument = {label: Asttypes.arg_label; expr: Parsetree.expression}
289302

290303
type type_parameter = {
@@ -1857,6 +1870,9 @@ and parse_es6_arrow_expression ?(arrow_attrs = []) ?(arrow_start_pos = None)
18571870
{arrow_expr with pexp_loc = {arrow_expr.pexp_loc with loc_start = start_pos}}
18581871

18591872
(*
1873+
* dotted_parameter ::=
1874+
* | . parameter (* deprecated uncurried syntax *)
1875+
*
18601876
* parameter ::=
18611877
* | pattern
18621878
* | pattern : type
@@ -1874,10 +1890,14 @@ and parse_es6_arrow_expression ?(arrow_attrs = []) ?(arrow_start_pos = None)
18741890
*)
18751891
and parse_parameter p =
18761892
if
1877-
p.Parser.token = Token.Typ || p.token = Tilde
1893+
p.Parser.token = Token.Typ || p.token = Tilde || p.token = Dot
18781894
|| Grammar.is_pattern_start p.token
1879-
then
1895+
then (
18801896
let start_pos = p.Parser.start_pos in
1897+
if p.Parser.token = Token.Dot then (
1898+
let dot_loc = mk_loc start_pos p.end_pos in
1899+
Parser.next p;
1900+
warn_uncurried_dot_syntax ~loc:dot_loc);
18811901
let attrs = parse_attributes p in
18821902
if p.Parser.token = Typ then (
18831903
Parser.next p;
@@ -1962,7 +1982,7 @@ and parse_parameter p =
19621982
| _ ->
19631983
Some
19641984
(TermParameter
1965-
{attrs; p_label = lbl; expr = None; pat; p_pos = start_pos})
1985+
{attrs; p_label = lbl; expr = None; pat; p_pos = start_pos}))
19661986
else None
19671987

19681988
and parse_parameter_list p =
@@ -1977,6 +1997,7 @@ and parse_parameter_list p =
19771997
* | _
19781998
* | lident
19791999
* | ()
2000+
* | (.) (* deprecated uncurried syntax *)
19802001
* | ( parameter {, parameter} [,] )
19812002
*)
19822003
and parse_parameters p : fundef_type_param option * fundef_term_param list =
@@ -2025,6 +2046,10 @@ and parse_parameters p : fundef_type_param option * fundef_term_param list =
20252046
] )
20262047
| Lparen ->
20272048
Parser.next p;
2049+
if p.Parser.token = Token.Dot then (
2050+
let dot_loc = mk_loc p.start_pos p.end_pos in
2051+
Parser.next p;
2052+
warn_uncurried_dot_syntax ~loc:dot_loc);
20282053
let type_params, term_params = parse_parameter_list p in
20292054
let term_params =
20302055
if term_params <> [] then term_params else [unit_term_parameter ()]
@@ -4033,13 +4058,31 @@ and parse_switch_expression p =
40334058
* | ~ label-name = ? _ (* syntax sugar *)
40344059
* | ~ label-name = ? expr : type
40354060
*
4061+
* dotted_argument ::=
4062+
* | . argument (* deprecated uncurried syntax *)
40364063
*)
40374064
and parse_argument p : argument option =
40384065
if
40394066
p.Parser.token = Token.Tilde
4040-
|| p.token = Underscore
4067+
|| p.token = Dot || p.token = Underscore
40414068
|| Grammar.is_expr_start p.token
4042-
then parse_argument2 p
4069+
then
4070+
match p.Parser.token with
4071+
| Dot -> (
4072+
let dot_loc = mk_loc p.start_pos p.end_pos in
4073+
Parser.next p;
4074+
warn_uncurried_dot_syntax ~loc:dot_loc;
4075+
match p.token with
4076+
(* apply(.) — legacy uncurried unit call *)
4077+
| Rparen ->
4078+
let unit_expr =
4079+
Ast_helper.Exp.construct
4080+
(Location.mknoloc (Longident.Lident "()"))
4081+
None
4082+
in
4083+
Some {label = Asttypes.Nolabel; expr = unit_expr}
4084+
| _ -> parse_argument2 p)
4085+
| _ -> parse_argument2 p
40434086
else None
40444087

40454088
and parse_argument2 p : argument option =
@@ -4796,6 +4839,9 @@ and parse_type_alias p typ =
47964839
* note:
47974840
* | attrs ~ident: type_expr -> attrs are on the arrow
47984841
* | attrs type_expr -> attrs are here part of the type_expr
4842+
*
4843+
* dotted_type_parameter ::=
4844+
* | . type_parameter (* deprecated uncurried syntax *)
47994845
*)
48004846
and parse_type_parameter ?current_type_name_path ?inline_types_context
48014847
?positional_type_name_path p =
@@ -4806,8 +4852,16 @@ and parse_type_parameter ?current_type_name_path ?inline_types_context
48064852
[doc_comment_to_attribute loc s]
48074853
| _ -> []
48084854
in
4809-
if p.Parser.token = Token.Tilde || Grammar.is_typ_expr_start p.token then
4855+
if
4856+
p.Parser.token = Token.Tilde
4857+
|| p.token = Dot
4858+
|| Grammar.is_typ_expr_start p.token
4859+
then (
48104860
let start_pos = p.Parser.start_pos in
4861+
if p.Parser.token = Token.Dot then (
4862+
let dot_loc = mk_loc start_pos p.end_pos in
4863+
Parser.next p;
4864+
warn_uncurried_dot_syntax ~loc:dot_loc);
48114865
let attrs = doc_attr @ parse_attributes p in
48124866
match p.Parser.token with
48134867
| Tilde -> (
@@ -4879,7 +4933,7 @@ and parse_type_parameter ?current_type_name_path ?inline_types_context
48794933
let typ_with_attributes =
48804934
{typ with ptyp_attributes = List.concat [attrs; typ.ptyp_attributes]}
48814935
in
4882-
Some {attrs = []; label = Nolabel; typ = typ_with_attributes; start_pos}
4936+
Some {attrs = []; label = Nolabel; typ = typ_with_attributes; start_pos})
48834937
else None
48844938

48854939
(* (int, ~x:string, float) *)

compiler/syntax/src/res_grammar.ml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ let is_pattern_start = function
178178
| _ -> false
179179

180180
let is_parameter_start = function
181-
| Token.Typ | Tilde -> true
181+
| Token.Typ | Tilde | Dot -> true
182182
| token when is_pattern_start token -> true
183183
| _ -> false
184184

@@ -206,7 +206,7 @@ let is_typ_expr_start = function
206206
| _ -> false
207207

208208
let is_type_parameter_start = function
209-
| Token.Tilde -> true
209+
| Token.Tilde | Dot -> true
210210
| token when is_typ_expr_start token -> true
211211
| _ -> false
212212

@@ -239,7 +239,7 @@ let is_record_row_string_key_start = function
239239
| _ -> false
240240

241241
let is_argument_start = function
242-
| Token.Tilde | Underscore -> true
242+
| Token.Tilde | Dot | Underscore -> true
243243
| t when is_expr_start t -> true
244244
| _ -> false
245245

rewatch/src/build/compile.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,10 +1094,13 @@ fn compile_file(
10941094

10951095
if helpers::contains_ascii_characters(&err) {
10961096
if package.is_local_dep {
1097-
// suppress warnings of external deps
10981097
Ok(Some(err))
10991098
} else {
1100-
Ok(None)
1099+
// Warnings from external deps are suppressed by default —
1100+
// users can't act on them. A small allow-list of critical
1101+
// deprecations still gets through so breakage signals are
1102+
// visible (and can be reported upstream).
1103+
Ok(retain_critical_external_warnings(&err))
11011104
}
11021105
} else {
11031106
Ok(None)
@@ -1106,6 +1109,35 @@ fn compile_file(
11061109
}
11071110
}
11081111

1112+
/// Filter a bsc stderr capture to the warning blocks the user needs to see
1113+
/// even when they originate in an external dependency.
1114+
///
1115+
/// Currently preserved:
1116+
/// - Warning 3 deprecations mentioning the legacy `(. ...)` uncurried syntax.
1117+
/// These indicate source that parses today but is scheduled for removal, so
1118+
/// consumers need to hear about them even when the code isn't theirs.
1119+
pub(super) fn retain_critical_external_warnings(stderr: &str) -> Option<String> {
1120+
const UNCURRIED_DOT_MARKER: &str = "`(. ...)` uncurried syntax";
1121+
if !stderr.contains(UNCURRIED_DOT_MARKER) {
1122+
return None;
1123+
}
1124+
// bsc prints each warning as its own block separated by a blank-line pair
1125+
// (three consecutive newlines). On Windows the same stream uses CRLF, so
1126+
// normalize before splitting to keep the block boundary recognizable —
1127+
// otherwise the whole stderr would be treated as a single block and
1128+
// unrelated warnings would leak through alongside the critical one.
1129+
let normalized = stderr.replace("\r\n", "\n");
1130+
let kept: Vec<&str> = normalized
1131+
.split("\n\n\n")
1132+
.filter(|block| block.contains(UNCURRIED_DOT_MARKER))
1133+
.collect();
1134+
if kept.is_empty() {
1135+
None
1136+
} else {
1137+
Some(kept.join("\n\n\n"))
1138+
}
1139+
}
1140+
11091141
pub fn mark_modules_with_deleted_deps_dirty(build_state: &mut BuildState) {
11101142
build_state.modules.iter_mut().for_each(|(_, module)| {
11111143
if !module.deps.is_disjoint(&build_state.deleted_modules) {
@@ -1202,3 +1234,42 @@ pub fn mark_modules_with_expired_deps_dirty(build_state: &mut BuildCommandState)
12021234
}
12031235
});
12041236
}
1237+
1238+
#[cfg(test)]
1239+
mod tests {
1240+
use super::*;
1241+
1242+
#[test]
1243+
fn retain_critical_external_warnings_returns_none_without_marker() {
1244+
let input = "\n Warning number 26\n foo.res:1:1\n\n unused variable x.\n";
1245+
assert_eq!(retain_critical_external_warnings(input), None);
1246+
}
1247+
1248+
#[test]
1249+
fn retain_critical_external_warnings_keeps_uncurried_dot_block() {
1250+
let input = concat!(
1251+
"\n Warning number 26\n foo.res:1:1\n\n unused variable x.\n",
1252+
"\n\n\n Warning number 3\n bar.res:5:10\n\n ",
1253+
"deprecated: The `(. ...)` uncurried syntax is deprecated.\n",
1254+
);
1255+
let kept = retain_critical_external_warnings(input).expect("uncurried-dot warning should survive");
1256+
assert!(kept.contains("`(. ...)` uncurried syntax"));
1257+
assert!(!kept.contains("unused variable"));
1258+
}
1259+
1260+
#[test]
1261+
fn retain_critical_external_warnings_handles_crlf_line_endings() {
1262+
// Windows stderr from bsc uses CRLF. Without normalization the "\n\n\n"
1263+
// splitter would find no boundary and return the entire stream, which
1264+
// would effectively disable suppression for external deps on Windows.
1265+
let input = concat!(
1266+
"\r\n Warning number 26\r\n foo.res:1:1\r\n\r\n unused variable x.\r\n",
1267+
"\r\n\r\n\r\n Warning number 3\r\n bar.res:5:10\r\n\r\n ",
1268+
"deprecated: The `(. ...)` uncurried syntax is deprecated.\r\n",
1269+
);
1270+
let kept = retain_critical_external_warnings(input)
1271+
.expect("uncurried-dot warning should survive on Windows too");
1272+
assert!(kept.contains("`(. ...)` uncurried syntax"));
1273+
assert!(!kept.contains("unused variable"));
1274+
}
1275+
}

rewatch/src/build/parse.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::build_types::*;
2+
use super::compile::retain_critical_external_warnings;
23
use super::logs;
34
use super::namespaces;
45
use crate::build::packages::Package;
@@ -139,7 +140,18 @@ pub fn generate_asts(
139140
logs::append(package, &stderr_warnings);
140141
stderr.push_str(&stderr_warnings);
141142
}
142-
Ok((_path, Some(_))) | Ok((_path, None)) => {
143+
Ok((_path, Some(stderr_warnings))) => {
144+
source_file.implementation.parse_state = ParseState::Success;
145+
source_file.implementation.parse_dirty = false;
146+
// External dep: surface only the critical warnings
147+
// (e.g. legacy `(. ...)` uncurried syntax) so
148+
// downstream users can report breakage upstream.
149+
if let Some(kept) = retain_critical_external_warnings(&stderr_warnings) {
150+
logs::append(package, &kept);
151+
stderr.push_str(&kept);
152+
}
153+
}
154+
Ok((_path, None)) => {
143155
source_file.implementation.parse_state = ParseState::Success;
144156
source_file.implementation.parse_dirty = false;
145157
}
@@ -167,7 +179,17 @@ pub fn generate_asts(
167179
logs::append(package, &stderr_warnings);
168180
stderr.push_str(&stderr_warnings);
169181
}
170-
Ok(Some((_, None))) | Ok(Some((_, Some(_)))) => {
182+
Ok(Some((_, Some(stderr_warnings)))) => {
183+
if let Some(interface) = source_file.interface.as_mut() {
184+
interface.parse_state = ParseState::Success;
185+
interface.parse_dirty = false;
186+
}
187+
if let Some(kept) = retain_critical_external_warnings(&stderr_warnings) {
188+
logs::append(package, &kept);
189+
stderr.push_str(&kept);
190+
}
191+
}
192+
Ok(Some((_, None))) => {
171193
if let Some(interface) = source_file.interface.as_mut() {
172194
interface.parse_state = ParseState::Success;
173195
interface.parse_dirty = false;

0 commit comments

Comments
 (0)