From 00d09b5a279fc115c94fbea0d450251ab0432061 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Sep 2026 13:26:08 +0200 Subject: [PATCH 1/4] Print coercions with context-aware parentheses Signed-off-by: Christoph Knittel --- CHANGELOG.md | 1 + compiler/syntax/src/res_parens.ml | 45 ++++++----- compiler/syntax/src/res_parens.mli | 7 +- compiler/syntax/src/res_printer.ml | 54 ++++++++------ .../syntax_tests/data/printer/expr/coerce.res | 52 +++++++++++++ .../data/printer/expr/expected/coerce.res.txt | 74 +++++++++++++++++-- 6 files changed, 186 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2047caef9ad..03b688eca7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ #### :nail_care: Polish +- Omit unnecessary parentheses around coercions in call arguments, bindings, and collection elements while preserving expression grouping. https://github.com/rescript-lang/rescript/issues/6254 - Print external declarations in signatures and type errors with their processed attributes instead of the `"#rescript-external"` placeholder, and print inline constants using `@inline` syntax. https://github.com/rescript-lang/rescript/pull/8581 - Improve diagnostics for dynamic imports of local values and attempts to use `import` as a first-class value. https://github.com/rescript-lang/rescript/pull/8582 - Allow inferred labeled functions to be called with labels in any order by removing legacy curried-arrow commutation locks. https://github.com/rescript-lang/rescript/pull/8547 diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 0c25e46853b..09a7f6dcd42 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -1,7 +1,7 @@ module Parsetree_viewer = Res_parsetree_viewer type kind = Parenthesized | Braced of Location.t | Nothing -let expr expr = +let expr ?(allow_coercion = false) expr = let opt_braces, _ = Parsetree_viewer.process_braces_attr expr in match opt_braces with | Some ({Location.loc = braces_loc}, _) -> Braced braces_loc @@ -12,9 +12,17 @@ let expr expr = Pexp_constraint ({pexp_desc = Pexp_pack _}, {ptyp_desc = Ptyp_package _}); } -> Nothing - | {pexp_desc = Pexp_constraint _} -> Parenthesized + | {pexp_desc = Pexp_coerce _} when allow_coercion -> Nothing + | {pexp_desc = Pexp_constraint _ | Pexp_coerce _} -> Parenthesized | _ -> Nothing) +(* A source annotation may precede :> directly, but nested coercions need + grouping. Preserve explicit braces through the normal expression rule. *) +let coerce_expr_operand expression = + match (expr expression, expression.Parsetree.pexp_desc) with + | Parenthesized, Pexp_constraint _ -> Nothing + | kind, _ -> kind + let expr_record_row_rhs ~optional e = let kind = expr e in match kind with @@ -50,9 +58,9 @@ let call_expr expr = Nothing | { pexp_desc = - ( Pexp_assert _ | Pexp_fun _ | Pexp_constraint _ | Pexp_setfield _ - | Pexp_match _ | Pexp_try _ | Pexp_while _ | Pexp_for _ | Pexp_for_of _ - | Pexp_for_await_of _ | Pexp_ifthenelse _ ); + ( Pexp_assert _ | Pexp_fun _ | Pexp_constraint _ | Pexp_coerce _ + | Pexp_setfield _ | Pexp_match _ | Pexp_try _ | Pexp_while _ | Pexp_for _ + | Pexp_for_of _ | Pexp_for_await_of _ | Pexp_ifthenelse _ ); } -> Parenthesized | _ when Parsetree_viewer.expr_is_await expr -> Parenthesized @@ -72,7 +80,7 @@ let structure_expr expr = Pexp_constraint ({pexp_desc = Pexp_pack _}, {ptyp_desc = Ptyp_package _}); } -> Nothing - | {pexp_desc = Pexp_constraint _} -> Parenthesized + | {pexp_desc = Pexp_constraint _ | Pexp_coerce _} -> Parenthesized | _ -> Nothing) let unary_expr_operand expr = @@ -100,8 +108,8 @@ let unary_expr_operand expr = Nothing | { pexp_desc = - ( Pexp_assert _ | Pexp_fun _ | Pexp_constraint _ | Pexp_setfield _ - | Pexp_extension _ (* readability? maybe remove *) + ( Pexp_assert _ | Pexp_fun _ | Pexp_constraint _ | Pexp_coerce _ + | Pexp_setfield _ | Pexp_extension _ (* readability? maybe remove *) | Pexp_object_literal _ (* ({"a": 1})["a"] *) | Pexp_object_set _ (* (o["x"] = v)["y"] *) | Pexp_match _ | Pexp_try _ | Pexp_while _ | Pexp_for _ | Pexp_for_of _ | Pexp_for_await_of _ @@ -125,7 +133,8 @@ let binary_expr_operand ~is_lhs expr = | {pexp_desc = Pexp_fun _} when Parsetree_viewer.is_underscore_apply_sugar expr -> Nothing - | {pexp_desc = Pexp_constraint _ | Pexp_fun _} -> Parenthesized + | {pexp_desc = Pexp_constraint _ | Pexp_coerce _ | Pexp_fun _} -> + Parenthesized | expr when Parsetree_viewer.is_binary_expression expr -> Parenthesized | expr when Parsetree_viewer.is_ternary_expr expr -> Parenthesized | {pexp_desc = Pexp_assert _} when is_lhs -> Parenthesized @@ -182,7 +191,7 @@ let flatten_operand_rhs parent_operator rhs = false | Pexp_fun {params = {p_pat = {ppat_desc = Ppat_var {txt = "__x"}}} :: _} -> false - | Pexp_fun _ | Pexp_setfield _ | Pexp_constraint _ -> true + | Pexp_fun _ | Pexp_setfield _ | Pexp_constraint _ | Pexp_coerce _ -> true | _ when Parsetree_viewer.is_ternary_expr rhs -> true | _ -> false @@ -220,9 +229,9 @@ let assert_or_await_expr_rhs ?(in_await = false) expr = Nothing | { pexp_desc = - ( Pexp_assert _ | Pexp_fun _ | Pexp_constraint _ | Pexp_setfield _ - | Pexp_match _ | Pexp_try _ | Pexp_while _ | Pexp_for _ | Pexp_for_of _ - | Pexp_for_await_of _ | Pexp_ifthenelse _ ); + ( Pexp_assert _ | Pexp_fun _ | Pexp_constraint _ | Pexp_coerce _ + | Pexp_setfield _ | Pexp_match _ | Pexp_try _ | Pexp_while _ | Pexp_for _ + | Pexp_for_of _ | Pexp_for_await_of _ | Pexp_ifthenelse _ ); } -> Parenthesized | _ when (not in_await) && Parsetree_viewer.expr_is_await expr -> @@ -267,9 +276,9 @@ let field_expr expr = pexp_desc = ( Pexp_assert _ | Pexp_extension _ (* %extension.x vs (%extension).x *) | Pexp_object_literal _ (* ({"a": 1})["a"] *) | Pexp_fun _ - | Pexp_constraint _ | Pexp_setfield _ | Pexp_match _ | Pexp_try _ - | Pexp_while _ | Pexp_for _ | Pexp_for_of _ | Pexp_for_await_of _ - | Pexp_ifthenelse _ ); + | Pexp_constraint _ | Pexp_coerce _ | Pexp_setfield _ | Pexp_match _ + | Pexp_try _ | Pexp_while _ | Pexp_for _ | Pexp_for_of _ + | Pexp_for_await_of _ | Pexp_ifthenelse _ ); } -> Parenthesized | _ when Parsetree_viewer.expr_is_await expr -> Parenthesized @@ -286,11 +295,11 @@ let ternary_operand expr = Pexp_constraint ({pexp_desc = Pexp_pack _}, {ptyp_desc = Ptyp_package _}); } -> Nothing - | {pexp_desc = Pexp_constraint _} -> Parenthesized + | {pexp_desc = Pexp_constraint _ | Pexp_coerce _} -> Parenthesized | _ when Res_parsetree_viewer.is_fun_expr expr -> ( let _, _parameters, return_expr = Parsetree_viewer.fun_expr expr in match return_expr.pexp_desc with - | Pexp_constraint _ -> Parenthesized + | Pexp_constraint _ | Pexp_coerce _ -> Parenthesized | _ -> Nothing) | _ -> Nothing) diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 3ce0218c847..9aa8fc9b076 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -1,6 +1,9 @@ type kind = Parenthesized | Braced of Location.t | Nothing -val expr : Parsetree.expression -> kind +(* Set [allow_coercion] to [true] only in grammar positions accepting a trailing + coercion without parentheses, such as call arguments and binding right-hand + sides. Arrow bodies require parentheses. *) +val expr : ?allow_coercion:bool -> Parsetree.expression -> kind val structure_expr : Parsetree.expression -> kind val unary_expr_operand : Parsetree.expression -> kind @@ -37,3 +40,5 @@ val arrow_return_typ_expr : Parsetree.core_type -> bool val pattern_record_row_rhs : Parsetree.pattern -> bool val expr_record_row_rhs : optional:bool -> Parsetree.expression -> kind + +val coerce_expr_operand : Parsetree.expression -> kind diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 35b5087f2a2..1577004e3ba 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -1698,7 +1698,7 @@ and print_spread_dict_expr ~state parts (expr : Parsetree.expression) cmt_tbl = in let spread_doc = let doc = print_expression ~state spread_expr cmt_tbl in - match Parens.expr spread_expr with + match Parens.expr ~allow_coercion:true spread_expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc spread_expr braces | Nothing -> doc @@ -2473,7 +2473,7 @@ and print_value_binding ~state ~rec_flag (vb : Parsetree.value_binding) cmt_tbl let opt_braces, expr = Parsetree_viewer.process_braces_attr vb.pvb_expr in let printed_expr = let doc = print_expression_with_comments ~state vb.pvb_expr cmt_tbl in - match Parens.expr vb.pvb_expr with + match Parens.expr ~allow_coercion:true vb.pvb_expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc @@ -3244,7 +3244,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = Doc.line; Doc.dotdotdot; (let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc); @@ -3266,7 +3266,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -3291,7 +3291,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = [ Doc.lparen; (let doc = print_expression_with_comments ~state arg cmt_tbl in - match Parens.expr arg with + match Parens.expr ~allow_coercion:true arg with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc arg braces | Nothing -> doc); @@ -3312,7 +3312,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -3325,7 +3325,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = | Some arg -> let arg_doc = let doc = print_expression_with_comments ~state arg cmt_tbl in - match Parens.expr arg with + match Parens.expr ~allow_coercion:true arg with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc arg braces | Nothing -> doc @@ -3363,7 +3363,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -3392,7 +3392,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -3418,7 +3418,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = [ Doc.lparen; (let doc = print_expression_with_comments ~state arg cmt_tbl in - match Parens.expr arg with + match Parens.expr ~allow_coercion:true arg with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc arg braces | Nothing -> doc); @@ -3439,7 +3439,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -3452,7 +3452,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = | Some arg -> let arg_doc = let doc = print_expression_with_comments ~state arg cmt_tbl in - match Parens.expr arg with + match Parens.expr ~allow_coercion:true arg with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc arg braces | Nothing -> doc @@ -3491,7 +3491,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = Doc.concat [ Doc.dotdotdot; - (match Parens.expr expr with + (match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc); @@ -3843,7 +3843,17 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = | Pexp_coerce (expr, (), typ) -> let doc_expr = print_expression_with_comments ~state expr cmt_tbl in let doc_typ = print_typ_expr ~state typ cmt_tbl in - Doc.concat [Doc.lparen; doc_expr; Doc.text " :> "; doc_typ; Doc.rparen] + let doc_expr = + match Parens.coerce_expr_operand expr with + | Parens.Parenthesized -> add_parens doc_expr + | Braced braces -> print_braces doc_expr expr braces + | Nothing -> doc_expr + in + let doc = Doc.concat [doc_expr; Doc.text " :> "; doc_typ] in + (* Keep attributes on the coercion rather than its operand. *) + if Parsetree_viewer.has_printable_attributes e.pexp_attributes then + add_parens doc + else doc | Pexp_object_get (parent_expr, label) -> print_object_get_doc ~state parent_expr label cmt_tbl | Pexp_object_set (obj, member, rhs) -> @@ -4344,7 +4354,7 @@ and print_array_spread_apply ~state sub_lists cmt_tbl = (* Print expression without leading comments (they're already extracted) *) let expr_doc = let doc = print_expression ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc @@ -4376,7 +4386,7 @@ and print_array_spread_apply ~state sub_lists cmt_tbl = (List.map (fun expr -> let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -4411,7 +4421,7 @@ and print_list_spread_apply ~state sub_lists cmt_tbl = comma_before_spread; Doc.dotdotdot; (let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc); @@ -4432,7 +4442,7 @@ and print_list_spread_apply ~state sub_lists cmt_tbl = (List.map (fun expr -> let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc) @@ -4532,7 +4542,7 @@ and print_pexp_apply ~state expr cmt_tbl = let member = let member_doc = let doc = print_expression_with_comments ~state member_expr cmt_tbl in - match Parens.expr member_expr with + match Parens.expr ~allow_coercion:true member_expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc member_expr braces | Nothing -> doc @@ -4579,7 +4589,7 @@ and print_pexp_apply ~state expr cmt_tbl = let member = let member_doc = let doc = print_expression_with_comments ~state member_expr cmt_tbl in - match Parens.expr member_expr with + match Parens.expr ~allow_coercion:true member_expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc member_expr braces | Nothing -> doc @@ -5247,7 +5257,7 @@ and print_arguments ~state ~partial | [(Nolabel, arg)] when Parsetree_viewer.is_huggable_expression arg -> let arg_doc = let doc = print_expression_with_comments ~state arg cmt_tbl in - match Parens.expr arg with + match Parens.expr ~allow_coercion:true arg with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc arg braces | Nothing -> doc @@ -5350,7 +5360,7 @@ and print_argument ~state (arg_lbl, arg) cmt_tbl = in let printed_expr = let doc = print_expression_with_comments ~state expr cmt_tbl in - match Parens.expr expr with + match Parens.expr ~allow_coercion:true expr with | Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc diff --git a/tests/syntax_tests/data/printer/expr/coerce.res b/tests/syntax_tests/data/printer/expr/coerce.res index 1d5f309de6d..9d75b4df122 100644 --- a/tests/syntax_tests/data/printer/expr/coerce.res +++ b/tests/syntax_tests/data/printer/expr/coerce.res @@ -24,3 +24,55 @@ let foo = (~a=(3:int:>int), b) => 34 // let x : int1 :> int2 = 3 :> int3 let x = (/* c0 */ x /* c1 */ :> /* c2 */ int /* c3 */) + +// Delimited arguments and binding right-hand sides need no extra parentheses. +foo(v :> b) +foo((v :> b)) +foo(~arg=(v :> b), ~optional=?(v :> b)) +foo((v :> b), x => x) +foo(x => x, (v :> b)) +let tuple = ((v :> b), (w :> c)) +let array = [(v :> b), (w :> c)] +let spreadArray = [...(vs :> array), (w :> b)] +let list = list{(v :> b), ...(vs :> list)} +let constructor = Some((v :> b)) +let variant = #Value((v :> b)) + +// Preserve grouping when the surrounding expression needs it. +let result = x => (x :> b) +let callback = foo(x => (x :> b)) +let coercedFunction = (x => x) :> (a => b) +let call = (f :> (a => b))(v) +let field = (v :> b).name +let objectField = (v :> b)["name"] +let equalLeft = (v :> b) == w +let equalRight = v == (w :> b) +let unary = !(v :> b) +let awaited = await (v :> b) +let nested = (v :> b) :> c +let record = {field: (v :> b)} +let conditional = condition ? (v :> b) : (w :> b) +let block = {v :> b} +let default = (~arg=(v :> b)) => arg +let jsx = b)}> {(v :> b)} + +// Comments must stay attached when parentheses disappear. +foo(/* before */ (v /* operand */ :> /* type */ b) /* after */) +foo((v :> b) // trailing line comment +) +let attributed = (@foo v) :> b +let coercionAttribute = @foo (v :> b) +let long = functionWithAVeryLongName((valueWithAVeryLongName :> typeWithAVeryLongName)) + +let arrayIndex = values[(index :> int)] +values[(index :> int)] = (value :> b) +let recordSpread = {...(value :> b), field: value} +let bracedOperand = {value} :> b +let blockSequence = {foo(); value :> b} +let checked = assert(value :> bool) +(value :> b) +let ternaryCallback = condition ? (x => (x :> b)) : other +let piped = (value :> b)->foo +let blockTail = {foo(); (value :> b)} +let blockHead = {(value :> b); foo()} +let dictSpread = dict{...(value :> dict), "field": value} diff --git a/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt b/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt index 92164f2ac5a..564d30ba657 100644 --- a/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt +++ b/tests/syntax_tests/data/printer/expr/expected/coerce.res.txt @@ -1,6 +1,6 @@ -let z = (x :> int) +let z = x :> int -let z2 = (x :> int) +let z2 = x :> int let foo = (x: int) => (x :> int) @@ -10,17 +10,79 @@ let bar = x => (x: t :> int) let bar2 = x => (x: t :> int) -call(~x=(y :> int), ~z=(w: int :> int), ~a, ~b) +call(~x=y :> int, ~z=w: int :> int, ~a, ~b) let foo = (~a=3: int, b) => 34 -let foo = (~a=(3 :> int), b) => 34 +let foo = (~a=3 :> int, b) => 34 -let foo = (~a=(3: int :> int), b) => 34 +let foo = (~a=3: int :> int, b) => 34 // THESE SHOULD NOT PARSE: no magic in the syntax // let x: int :> string = y // let x :> string = y // let x : int1 :> int2 = 3 :> int3 -let x = /* c0 */ (x /* c1 */ :> /* c2 */ int) /* c3 */ +let x = /* c0 */ x /* c1 */ :> /* c2 */ int /* c3 */ + +// Delimited arguments and binding right-hand sides need no extra parentheses. +foo(v :> b) +foo(v :> b) +foo(~arg=v :> b, ~optional=?v :> b) +foo(v :> b, x => x) +foo(x => x, v :> b) +let tuple = (v :> b, w :> c) +let array = [v :> b, w :> c] +let spreadArray = [...vs :> array, w :> b] +let list = list{v :> b, ...vs :> list} +let constructor = Some(v :> b) +let variant = #Value(v :> b) + +// Preserve grouping when the surrounding expression needs it. +let result = x => (x :> b) +let callback = foo(x => (x :> b)) +let coercedFunction = x => x :> a => b +let call = (f :> a => b)(v) +let field = (v :> b).name +let objectField = (v :> b)["name"] +let equalLeft = (v :> b) == w +let equalRight = v == (w :> b) +let unary = !(v :> b) +let awaited = await (v :> b) +let nested = (v :> b) :> c +let record = {field: (v :> b)} +let conditional = condition ? (v :> b) : (w :> b) +let block = {v :> b} +let default = (~arg=v :> b) => arg +let jsx = b}> {v :> b} + +// Comments must stay attached when parentheses disappear. +foo(/* before */ v /* operand */ :> /* type */ b /* after */) +foo(v :> b) // trailing line comment +let attributed = @foo v :> b +let coercionAttribute = @foo (v :> b) +let long = functionWithAVeryLongName(valueWithAVeryLongName :> typeWithAVeryLongName) + +let arrayIndex = values[index :> int] +values[index :> int] = (value :> b) +let recordSpread = {...value :> b, field: value} +let bracedOperand = {value} :> b +let blockSequence = { + { + foo() + value + } :> b +} +let checked = assert(value :> bool) +(value :> b) +let ternaryCallback = condition ? (x => (x :> b)) : other +let piped = (value :> b)->foo +let blockTail = { + foo() + (value :> b) +} +let blockHead = { + (value :> b) + foo() +} +let dictSpread = dict{...value :> dict, "field": value} From 27fee8be076947b948704bbaa9f906b5aba2cef5 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Sep 2026 13:27:06 +0200 Subject: [PATCH 2/4] Link coercion formatter changelog to PR Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03b688eca7d..863b3a2bf19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,7 @@ #### :nail_care: Polish -- Omit unnecessary parentheses around coercions in call arguments, bindings, and collection elements while preserving expression grouping. https://github.com/rescript-lang/rescript/issues/6254 +- Omit unnecessary parentheses around coercions in call arguments, bindings, and collection elements while preserving expression grouping. https://github.com/rescript-lang/rescript/pull/8614 - Print external declarations in signatures and type errors with their processed attributes instead of the `"#rescript-external"` placeholder, and print inline constants using `@inline` syntax. https://github.com/rescript-lang/rescript/pull/8581 - Improve diagnostics for dynamic imports of local values and attempts to use `import` as a first-class value. https://github.com/rescript-lang/rescript/pull/8582 - Allow inferred labeled functions to be called with labels in any order by removing legacy curried-arrow commutation locks. https://github.com/rescript-lang/rescript/pull/8547 From df3784c572829988a2adfe2b7cde1f9b5df6a41c Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Fri, 4 Sep 2026 14:02:21 +0200 Subject: [PATCH 3/4] Preserve required coercion grouping and update formatted sources Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- compiler/syntax/src/res_parens.ml | 7 ---- compiler/syntax/src/res_parens.mli | 6 +-- compiler/syntax/src/res_printer.ml | 4 +- packages/dev-playground/src/CompilerApi.res | 2 +- .../syntax_tests/data/printer/expr/coerce.res | 14 ++++++- .../data/printer/expr/expected/coerce.res.txt | 38 ++++++++++++------- tests/tests/src/Coercion.res | 2 +- tests/tests/src/VariantCoercion.res | 2 +- tests/tests/src/poly_variant_test.res | 4 +- tests/tests/src/type_coercion_free_vars.res | 2 +- 11 files changed, 49 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 863b3a2bf19..c5715a24530 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,7 +62,7 @@ #### :nail_care: Polish -- Omit unnecessary parentheses around coercions in call arguments, bindings, and collection elements while preserving expression grouping. https://github.com/rescript-lang/rescript/pull/8614 +- Omit unnecessary parentheses around coercions in call arguments, collection elements, and array indices while preserving expression grouping. https://github.com/rescript-lang/rescript/pull/8614 - Print external declarations in signatures and type errors with their processed attributes instead of the `"#rescript-external"` placeholder, and print inline constants using `@inline` syntax. https://github.com/rescript-lang/rescript/pull/8581 - Improve diagnostics for dynamic imports of local values and attempts to use `import` as a first-class value. https://github.com/rescript-lang/rescript/pull/8582 - Allow inferred labeled functions to be called with labels in any order by removing legacy curried-arrow commutation locks. https://github.com/rescript-lang/rescript/pull/8547 diff --git a/compiler/syntax/src/res_parens.ml b/compiler/syntax/src/res_parens.ml index 09a7f6dcd42..ab67bb23b44 100644 --- a/compiler/syntax/src/res_parens.ml +++ b/compiler/syntax/src/res_parens.ml @@ -16,13 +16,6 @@ let expr ?(allow_coercion = false) expr = | {pexp_desc = Pexp_constraint _ | Pexp_coerce _} -> Parenthesized | _ -> Nothing) -(* A source annotation may precede :> directly, but nested coercions need - grouping. Preserve explicit braces through the normal expression rule. *) -let coerce_expr_operand expression = - match (expr expression, expression.Parsetree.pexp_desc) with - | Parenthesized, Pexp_constraint _ -> Nothing - | kind, _ -> kind - let expr_record_row_rhs ~optional e = let kind = expr e in match kind with diff --git a/compiler/syntax/src/res_parens.mli b/compiler/syntax/src/res_parens.mli index 9aa8fc9b076..74022a87fb4 100644 --- a/compiler/syntax/src/res_parens.mli +++ b/compiler/syntax/src/res_parens.mli @@ -1,8 +1,8 @@ type kind = Parenthesized | Braced of Location.t | Nothing (* Set [allow_coercion] to [true] only in grammar positions accepting a trailing - coercion without parentheses, such as call arguments and binding right-hand - sides. Arrow bodies require parentheses. *) + coercion without parentheses, such as call arguments and collection elements. + Bindings and arrow bodies require parentheses. *) val expr : ?allow_coercion:bool -> Parsetree.expression -> kind val structure_expr : Parsetree.expression -> kind @@ -40,5 +40,3 @@ val arrow_return_typ_expr : Parsetree.core_type -> bool val pattern_record_row_rhs : Parsetree.pattern -> bool val expr_record_row_rhs : optional:bool -> Parsetree.expression -> kind - -val coerce_expr_operand : Parsetree.expression -> kind diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index 1577004e3ba..02ee9943379 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -2473,7 +2473,7 @@ and print_value_binding ~state ~rec_flag (vb : Parsetree.value_binding) cmt_tbl let opt_braces, expr = Parsetree_viewer.process_braces_attr vb.pvb_expr in let printed_expr = let doc = print_expression_with_comments ~state vb.pvb_expr cmt_tbl in - match Parens.expr ~allow_coercion:true vb.pvb_expr with + match Parens.expr vb.pvb_expr with | Parens.Parenthesized -> add_parens doc | Braced braces -> print_braces doc expr braces | Nothing -> doc @@ -3844,7 +3844,7 @@ and print_expression ~state (e : Parsetree.expression) cmt_tbl = let doc_expr = print_expression_with_comments ~state expr cmt_tbl in let doc_typ = print_typ_expr ~state typ cmt_tbl in let doc_expr = - match Parens.coerce_expr_operand expr with + match Parens.expr expr with | Parens.Parenthesized -> add_parens doc_expr | Braced braces -> print_braces doc_expr expr braces | Nothing -> doc_expr diff --git a/packages/dev-playground/src/CompilerApi.res b/packages/dev-playground/src/CompilerApi.res index 901eb314738..4e8914b5906 100644 --- a/packages/dev-playground/src/CompilerApi.res +++ b/packages/dev-playground/src/CompilerApi.res @@ -218,7 +218,7 @@ let applyConfig = ( ~experimentalFeatures: array, ) => { if hasFunction(instance, "setModuleSystem") { - instance->Instance.setModuleSystem((moduleSystem :> string)) + instance->Instance.setModuleSystem(moduleSystem :> string) } if hasFunction(instance, "setWarnFlags") { instance->Instance.setWarnFlags(warnFlags === "" ? defaultConfig.warnFlags : warnFlags) diff --git a/tests/syntax_tests/data/printer/expr/coerce.res b/tests/syntax_tests/data/printer/expr/coerce.res index 9d75b4df122..ef272d9d55f 100644 --- a/tests/syntax_tests/data/printer/expr/coerce.res +++ b/tests/syntax_tests/data/printer/expr/coerce.res @@ -25,7 +25,7 @@ let foo = (~a=(3:int:>int), b) => 34 let x = (/* c0 */ x /* c1 */ :> /* c2 */ int /* c3 */) -// Delimited arguments and binding right-hand sides need no extra parentheses. +// Delimited arguments need no extra parentheses. foo(v :> b) foo((v :> b)) foo(~arg=(v :> b), ~optional=?(v :> b)) @@ -76,3 +76,15 @@ let piped = (value :> b)->foo let blockTail = {foo(); (value :> b)} let blockHead = {(value :> b); foo()} let dictSpread = dict{...(value :> dict), "field": value} + +// A constrained operand needs parentheses when the coercion is a binding RHS. +let constrainedOperand = (x: t) :> u +let constrainedChain = ((x: t) :> u) :> v +let constrainedBlock = {(x: t) :> u} +call((x: t) :> u) + +// A following JSX element must not be read as coercion type arguments. +let beforeJsx = () => { + let value = (x :> string) +