From 64b4c6ba47470d6b5abcbb7ed8ae323ec2b63e84 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sun, 29 Jun 2025 21:44:11 +0200 Subject: [PATCH 01/21] PoC of let.unwrap --- compiler/frontend/ast_attributes.ml | 6 + compiler/frontend/ast_attributes.mli | 2 + compiler/frontend/bs_builtin_ppx.ml | 69 ++++++++++++ tests/tests/src/LetUnwrap.mjs | 161 +++++++++++++++++++++++++++ tests/tests/src/LetUnwrap.res | 69 ++++++++++++ 5 files changed, 307 insertions(+) create mode 100644 tests/tests/src/LetUnwrap.mjs create mode 100644 tests/tests/src/LetUnwrap.res diff --git a/compiler/frontend/ast_attributes.ml b/compiler/frontend/ast_attributes.ml index add0c41225..b02733aa4c 100644 --- a/compiler/frontend/ast_attributes.ml +++ b/compiler/frontend/ast_attributes.ml @@ -199,6 +199,12 @@ let has_bs_optional (attrs : t) : bool = true | _ -> false) +let has_unwrap_attr (attrs : t) : bool = + Ext_list.exists attrs (fun ({txt}, _) -> + match txt with + | "let.unwrap" -> true + | _ -> false) + let iter_process_bs_int_as (attrs : t) = let st = ref None in Ext_list.iter attrs (fun (({txt; loc}, payload) as attr) -> diff --git a/compiler/frontend/ast_attributes.mli b/compiler/frontend/ast_attributes.mli index c2adfdd19e..1acb788701 100644 --- a/compiler/frontend/ast_attributes.mli +++ b/compiler/frontend/ast_attributes.mli @@ -46,6 +46,8 @@ val iter_process_bs_string_as : t -> string option val has_bs_optional : t -> bool +val has_unwrap_attr : t -> bool + val iter_process_bs_int_as : t -> int option type as_const_payload = Int of int | Str of string * External_arg_spec.delim diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index ce521163a6..8a4f5042de 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -143,6 +143,75 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) ] ) -> default_expr_mapper self {e with pexp_desc = Pexp_ifthenelse (b, t_exp, Some f_exp)} + (* Transform: + - `@let.unwrap let Ok(inner_pat) = expr` + - `@let.unwrap let Some(inner_pat) = expr` + ...into switches *) + | Pexp_let + ( Nonrecursive, + [ + { + pvb_pat = + { + ppat_desc = + Ppat_construct + ( {txt = Lident (("Ok" | "Some") as variant_name)}, + Some _inner_pat ); + } as pvb_pat; + pvb_expr; + pvb_attributes; + }; + ], + body ) + when Ast_attributes.has_unwrap_attr pvb_attributes -> ( + let variant = + match variant_name with + | "Ok" -> `Result + | _ -> `Option + in + match pvb_expr.pexp_desc with + | Pexp_pack _ -> default_expr_mapper self e + | _ -> + let ok_case = + { + Parsetree.pc_bar = None; + pc_lhs = pvb_pat; + pc_guard = None; + pc_rhs = body; + } + in + let loc = Location.none in + let error_case = + match variant with + | `Result -> + { + Parsetree.pc_bar = None; + pc_lhs = + Ast_helper.Pat.construct ~loc + {txt = Lident "Error"; loc} + (Some (Ast_helper.Pat.var ~loc {txt = "e"; loc})); + pc_guard = None; + pc_rhs = + Ast_helper.Exp.construct ~loc + {txt = Lident "Error"; loc} + (Some (Ast_helper.Exp.ident ~loc {txt = Lident "e"; loc})); + } + | `Option -> + { + Parsetree.pc_bar = None; + pc_lhs = + Ast_helper.Pat.construct ~loc {txt = Lident "None"; loc} None; + pc_guard = None; + pc_rhs = + Ast_helper.Exp.construct ~loc {txt = Lident "None"; loc} None; + } + in + default_expr_mapper self + { + e with + pexp_desc = Pexp_match (pvb_expr, [ok_case; error_case]); + pexp_attributes = e.pexp_attributes @ pvb_attributes; + }) | Pexp_let ( Nonrecursive, [ diff --git a/tests/tests/src/LetUnwrap.mjs b/tests/tests/src/LetUnwrap.mjs new file mode 100644 index 0000000000..b9cc30264d --- /dev/null +++ b/tests/tests/src/LetUnwrap.mjs @@ -0,0 +1,161 @@ +// Generated by ReScript, PLEASE EDIT WITH CARE + + +function doStuffWithResult(s) { + if (s === "s") { + return { + TAG: "Ok", + _0: "hello" + }; + } else { + return { + TAG: "Error", + _0: "InvalidString" + }; + } +} + +function doNextStuffWithResult(s) { + if (s === "s") { + return { + TAG: "Ok", + _0: "hello" + }; + } else { + return { + TAG: "Error", + _0: "InvalidNext" + }; + } +} + +function getXWithResult(s) { + let y = doStuffWithResult(s); + if (y.TAG !== "Ok") { + return { + TAG: "Error", + _0: y._0 + }; + } + let y$1 = y._0; + let x = doNextStuffWithResult(y$1); + if (x.TAG === "Ok") { + return { + TAG: "Ok", + _0: x._0 + y$1 + }; + } else { + return { + TAG: "Error", + _0: x._0 + }; + } +} + +let x = getXWithResult("s"); + +let someResult; + +someResult = x.TAG === "Ok" ? x._0 : ( + x._0 === "InvalidNext" ? "nope!" : "nope" + ); + +function doStuffWithOption(s) { + if (s === "s") { + return "hello"; + } + +} + +function doNextStuffWithOption(s) { + if (s === "s") { + return "hello"; + } + +} + +function getXWithOption(s) { + let y = doStuffWithOption(s); + if (y === undefined) { + return; + } + let x = doNextStuffWithOption(y); + if (x !== undefined) { + return x + y; + } + +} + +let x$1 = getXWithOption("s"); + +let someOption = x$1 !== undefined ? x$1 : "nope"; + +async function doStuffResultAsync(s) { + if (s === "s") { + return { + TAG: "Ok", + _0: { + s: "hello" + } + }; + } else { + return { + TAG: "Error", + _0: "FetchError" + }; + } +} + +async function decodeResAsync(res) { + let match = res.s; + if (match === "s") { + return { + TAG: "Ok", + _0: res.s + }; + } else { + return { + TAG: "Error", + _0: "DecodeError" + }; + } +} + +async function getXWithResultAsync(s) { + let res = await doStuffResultAsync(s); + if (res.TAG !== "Ok") { + return { + TAG: "Error", + _0: res._0 + }; + } + let res$1 = res._0; + console.log(res$1.s); + let x = await decodeResAsync(res$1); + if (x.TAG === "Ok") { + return { + TAG: "Ok", + _0: x._0 + }; + } else { + return { + TAG: "Error", + _0: x._0 + }; + } +} + +export { + doStuffWithResult, + doNextStuffWithResult, + getXWithResult, + someResult, + doStuffWithOption, + doNextStuffWithOption, + getXWithOption, + someOption, + doStuffResultAsync, + decodeResAsync, + getXWithResultAsync, +} +/* x Not a pure module */ diff --git a/tests/tests/src/LetUnwrap.res b/tests/tests/src/LetUnwrap.res new file mode 100644 index 0000000000..2a1cee99cf --- /dev/null +++ b/tests/tests/src/LetUnwrap.res @@ -0,0 +1,69 @@ +let doStuffWithResult = s => + switch s { + | "s" => Ok("hello") + | _ => Error(#InvalidString) + } + +let doNextStuffWithResult = s => + switch s { + | "s" => Ok("hello") + | _ => Error(#InvalidNext) + } + +let getXWithResult = s => { + @let.unwrap let Ok(y) = doStuffWithResult(s) + @let.unwrap let Ok(x) = doNextStuffWithResult(y) + Ok(x ++ y) +} + +let someResult = switch getXWithResult("s") { +| Ok(x) => x +| Error(#InvalidString) => "nope" +| Error(#InvalidNext) => "nope!" +} + +let doStuffWithOption = s => + switch s { + | "s" => Some("hello") + | _ => None + } + +let doNextStuffWithOption = s => + switch s { + | "s" => Some("hello") + | _ => None + } + +let getXWithOption = s => { + @let.unwrap let Some(y) = doStuffWithOption(s) + @let.unwrap let Some(x) = doNextStuffWithOption(y) + Some(x ++ y) +} + +let someOption = switch getXWithOption("s") { +| Some(x) => x +| None => "nope" +} + +type res = {s: string} + +let doStuffResultAsync = async s => { + switch s { + | "s" => Ok({s: "hello"}) + | _ => Error(#FetchError) + } +} + +let decodeResAsync = async res => { + switch res.s { + | "s" => Ok(res.s) + | _ => Error(#DecodeError) + } +} + +let getXWithResultAsync = async s => { + @let.unwrap let Ok({s} as res) = await doStuffResultAsync(s) + Console.log(s) + @let.unwrap let Ok(x) = await decodeResAsync(res) + Ok(x) +} From cf870da4d5aab1bc82820a9c3efcbcce9e08b524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20Tsnobiladz=C3=A9?= Date: Tue, 1 Jul 2025 18:55:42 +0200 Subject: [PATCH 02/21] support let unwrap syntax (`let?`) (#7586) * support let unwrap syntax (`let?`) * fix printing of let? --- compiler/syntax/src/res_core.ml | 48 +++++++++++++++---- compiler/syntax/src/res_grammar.ml | 8 ++-- compiler/syntax/src/res_printer.ml | 15 ++++-- compiler/syntax/src/res_scanner.ml | 4 ++ compiler/syntax/src/res_token.ml | 10 ++-- .../expressions/expected/letUnwrapRec.res.txt | 11 +++++ .../errors/expressions/letUnwrapRec.res | 2 + .../signature/expected/letUnwrap.resi.txt | 9 ++++ .../parsing/errors/signature/letUnwrap.resi | 1 + .../expressions/expected/letUnwrap.res.txt | 4 ++ .../parsing/grammar/expressions/letUnwrap.res | 9 ++++ .../printer/expr/expected/letUnwrap.res.txt | 9 ++++ .../data/printer/expr/letUnwrap.res | 9 ++++ tests/syntax_tests/res_test.ml | 2 +- tests/tests/src/LetUnwrap.res | 12 ++--- 15 files changed, 125 insertions(+), 28 deletions(-) create mode 100644 tests/syntax_tests/data/parsing/errors/expressions/expected/letUnwrapRec.res.txt create mode 100644 tests/syntax_tests/data/parsing/errors/expressions/letUnwrapRec.res create mode 100644 tests/syntax_tests/data/parsing/errors/signature/expected/letUnwrap.resi.txt create mode 100644 tests/syntax_tests/data/parsing/errors/signature/letUnwrap.resi create mode 100644 tests/syntax_tests/data/parsing/grammar/expressions/expected/letUnwrap.res.txt create mode 100644 tests/syntax_tests/data/parsing/grammar/expressions/letUnwrap.res create mode 100644 tests/syntax_tests/data/printer/expr/expected/letUnwrap.res.txt create mode 100644 tests/syntax_tests/data/printer/expr/letUnwrap.res diff --git a/compiler/syntax/src/res_core.ml b/compiler/syntax/src/res_core.ml index ec0e7b38ad..4c3a683042 100644 --- a/compiler/syntax/src/res_core.ml +++ b/compiler/syntax/src/res_core.ml @@ -117,6 +117,12 @@ module ErrorMessages = struct ] |> Doc.to_string ~width:80 + let experimental_let_unwrap_rec = + "let? is not allowed to be recursive. Use a regular `let` or remove `rec`." + + let experimental_let_unwrap_sig = + "let? is not allowed in signatures. Use a regular `let` instead." + let type_param = "A type param consists of a singlequote followed by a name like `'a` or \ `'A`" @@ -2696,21 +2702,35 @@ and parse_attributes_and_binding (p : Parser.t) = | _ -> [] (* definition ::= let [rec] let-binding { and let-binding } *) -and parse_let_bindings ~attrs ~start_pos p = - Parser.optional p Let |> ignore; +and parse_let_bindings ~unwrap ~attrs ~start_pos p = + Parser.optional p (Let {unwrap}) |> ignore; let rec_flag = if Parser.optional p Token.Rec then Asttypes.Recursive else Asttypes.Nonrecursive in + let end_pos = p.Parser.start_pos in + if rec_flag = Asttypes.Recursive && unwrap then + Parser.err ~start_pos ~end_pos p + (Diagnostics.message ErrorMessages.experimental_let_unwrap_rec); + let add_unwrap_attr ~unwrap ~start_pos ~end_pos attrs = + if unwrap then + ( {Asttypes.txt = "let.unwrap"; loc = mk_loc start_pos end_pos}, + Ast_payload.empty ) + :: attrs + else attrs + in + let attrs = add_unwrap_attr ~unwrap ~start_pos ~end_pos attrs in let first = parse_let_binding_body ~start_pos ~attrs p in let rec loop p bindings = let start_pos = p.Parser.start_pos in + let end_pos = p.Parser.end_pos in let attrs = parse_attributes_and_binding p in + let attrs = add_unwrap_attr ~unwrap ~start_pos ~end_pos attrs in match p.Parser.token with | And -> Parser.next p; - ignore (Parser.optional p Let); + ignore (Parser.optional p (Let {unwrap = false})); (* overparse for fault tolerance *) let let_binding = parse_let_binding_body ~start_pos ~attrs p in loop p (let_binding :: bindings) @@ -3444,8 +3464,10 @@ and parse_expr_block_item p = let block_expr = parse_expr_block p in let loc = mk_loc start_pos p.prev_end_pos in Ast_helper.Exp.open_ ~loc od.popen_override od.popen_lid block_expr - | Let -> - let rec_flag, let_bindings = parse_let_bindings ~attrs ~start_pos p in + | Let {unwrap} -> + let rec_flag, let_bindings = + parse_let_bindings ~unwrap ~attrs ~start_pos p + in parse_newline_or_semicolon_expr_block p; let next = if Grammar.is_block_expr_start p.Parser.token then parse_expr_block p @@ -3616,7 +3638,7 @@ and parse_if_or_if_let_expression p = Parser.expect If p; let expr = match p.Parser.token with - | Let -> + | Let _ -> Parser.next p; let if_let_expr = parse_if_let_expr start_pos p in Parser.err ~start_pos:if_let_expr.pexp_loc.loc_start @@ -6064,8 +6086,10 @@ and parse_structure_item_region p = parse_newline_or_semicolon_structure p; let loc = mk_loc start_pos p.prev_end_pos in Some (Ast_helper.Str.open_ ~loc open_description) - | Let -> - let rec_flag, let_bindings = parse_let_bindings ~attrs ~start_pos p in + | Let {unwrap} -> + let rec_flag, let_bindings = + parse_let_bindings ~unwrap ~attrs ~start_pos p + in parse_newline_or_semicolon_structure p; let loc = mk_loc start_pos p.prev_end_pos in Some (Ast_helper.Str.value ~loc rec_flag let_bindings) @@ -6694,7 +6718,11 @@ and parse_signature_item_region p = let start_pos = p.Parser.start_pos in let attrs = parse_attributes p in match p.Parser.token with - | Let -> + | Let {unwrap} -> + if unwrap then ( + Parser.err ~start_pos ~end_pos:p.Parser.end_pos p + (Diagnostics.message ErrorMessages.experimental_let_unwrap_sig); + Parser.next p); Parser.begin_region p; let value_desc = parse_sign_let_desc ~attrs p in parse_newline_or_semicolon_signature p; @@ -6894,7 +6922,7 @@ and parse_module_type_declaration ~attrs ~start_pos p = and parse_sign_let_desc ~attrs p = let start_pos = p.Parser.start_pos in - Parser.optional p Let |> ignore; + Parser.optional p (Let {unwrap = false}) |> ignore; let name, loc = parse_lident p in let name = Location.mkloc name loc in Parser.expect Colon p; diff --git a/compiler/syntax/src/res_grammar.ml b/compiler/syntax/src/res_grammar.ml index 456767c5bc..2c5b1e1ac0 100644 --- a/compiler/syntax/src/res_grammar.ml +++ b/compiler/syntax/src/res_grammar.ml @@ -124,8 +124,8 @@ let to_string = function | DictRows -> "rows of a dict" let is_signature_item_start = function - | Token.At | Let | Typ | External | Exception | Open | Include | Module | AtAt - | PercentPercent -> + | Token.At | Let _ | Typ | External | Exception | Open | Include | Module + | AtAt | PercentPercent -> true | _ -> false @@ -162,7 +162,7 @@ let is_jsx_attribute_start = function | _ -> false let is_structure_item_start = function - | Token.Open | Let | Typ | External | Exception | Include | Module | AtAt + | Token.Open | Let _ | Typ | External | Exception | Include | Module | AtAt | PercentPercent | At -> true | t when is_expr_start t -> true @@ -265,7 +265,7 @@ let is_jsx_child_start = is_atomic_expr_start let is_block_expr_start = function | Token.Assert | At | Await | Backtick | Bang | Codepoint _ | Exception | False | Float _ | For | Forwardslash | ForwardslashDot | Hash | If | Int _ - | Lbrace | Lbracket | LessThan | Let | Lident _ | List | Lparen | Minus + | Lbrace | Lbracket | LessThan | Let _ | Lident _ | List | Lparen | Minus | MinusDot | Module | Open | Percent | Plus | PlusDot | String _ | Switch | True | Try | Uident _ | Underscore | While | Dict -> true diff --git a/compiler/syntax/src/res_printer.ml b/compiler/syntax/src/res_printer.ml index a7c95cf5ee..cf790f8201 100644 --- a/compiler/syntax/src/res_printer.ml +++ b/compiler/syntax/src/res_printer.ml @@ -2093,11 +2093,20 @@ and print_type_parameter ~state {attrs; lbl; typ} cmt_tbl = and print_value_binding ~state ~rec_flag (vb : Parsetree.value_binding) cmt_tbl i = + let has_unwrap = ref false in let attrs = - print_attributes ~state ~loc:vb.pvb_pat.ppat_loc vb.pvb_attributes cmt_tbl - in + vb.pvb_attributes + |> List.filter_map (function + | {Asttypes.txt = "let.unwrap"}, _ -> + has_unwrap := true; + None + | attr -> Some attr) + in + let attrs = print_attributes ~state ~loc:vb.pvb_pat.ppat_loc attrs cmt_tbl in let header = - if i == 0 then Doc.concat [Doc.text "let "; rec_flag] else Doc.text "and " + if i == 0 then + Doc.concat [Doc.text (if !has_unwrap then "let? " else "let "); rec_flag] + else Doc.text "and " in match vb with | { diff --git a/compiler/syntax/src/res_scanner.ml b/compiler/syntax/src/res_scanner.ml index c404d36cc2..ee731f0ec5 100644 --- a/compiler/syntax/src/res_scanner.ml +++ b/compiler/syntax/src/res_scanner.ml @@ -209,6 +209,10 @@ let scan_identifier scanner = next scanner; (* TODO: this isn't great *) Token.lookup_keyword "dict{" + | {ch = '?'}, "let" -> + next scanner; + (* TODO: this isn't great *) + Token.lookup_keyword "let?" | _ -> Token.lookup_keyword str let scan_digits scanner ~base = diff --git a/compiler/syntax/src/res_token.ml b/compiler/syntax/src/res_token.ml index 5fc89658c0..d48a97d24e 100644 --- a/compiler/syntax/src/res_token.ml +++ b/compiler/syntax/src/res_token.ml @@ -17,7 +17,7 @@ type t = | DotDotDot | Bang | Semicolon - | Let + | Let of {unwrap: bool} | And | Rec | Underscore @@ -133,7 +133,8 @@ let to_string = function | Float {f} -> "Float: " ^ f | Bang -> "!" | Semicolon -> ";" - | Let -> "let" + | Let {unwrap = true} -> "let?" + | Let {unwrap = false} -> "let" | And -> "and" | Rec -> "rec" | Underscore -> "_" @@ -231,7 +232,8 @@ let keyword_table = function | "if" -> If | "in" -> In | "include" -> Include - | "let" -> Let + | "let?" -> Let {unwrap = true} + | "let" -> Let {unwrap = false} | "list{" -> List | "dict{" -> Dict | "module" -> Module @@ -251,7 +253,7 @@ let keyword_table = function let is_keyword = function | Await | And | As | Assert | Constraint | Else | Exception | External | False - | For | If | In | Include | Land | Let | List | Lor | Module | Mutable | Of + | For | If | In | Include | Land | Let _ | List | Lor | Module | Mutable | Of | Open | Private | Rec | Switch | True | Try | Typ | When | While | Dict -> true | _ -> false diff --git a/tests/syntax_tests/data/parsing/errors/expressions/expected/letUnwrapRec.res.txt b/tests/syntax_tests/data/parsing/errors/expressions/expected/letUnwrapRec.res.txt new file mode 100644 index 0000000000..ee6fbcfcf4 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/expressions/expected/letUnwrapRec.res.txt @@ -0,0 +1,11 @@ + + Syntax error! + syntax_tests/data/parsing/errors/expressions/letUnwrapRec.res:1:1-9 + + 1 │ let? rec Some(baz) = someOption + 2 │ and Some(bar) = baz + + let? is not allowed to be recursive. Use a regular `let` or remove `rec`. + +let rec Some baz = someOption[@@let.unwrap ] +and Some bar = baz[@@let.unwrap ] \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/expressions/letUnwrapRec.res b/tests/syntax_tests/data/parsing/errors/expressions/letUnwrapRec.res new file mode 100644 index 0000000000..ce29385c36 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/expressions/letUnwrapRec.res @@ -0,0 +1,2 @@ +let? rec Some(baz) = someOption +and Some(bar) = baz \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/signature/expected/letUnwrap.resi.txt b/tests/syntax_tests/data/parsing/errors/signature/expected/letUnwrap.resi.txt new file mode 100644 index 0000000000..74ace608ce --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/signature/expected/letUnwrap.resi.txt @@ -0,0 +1,9 @@ + + Syntax error! + syntax_tests/data/parsing/errors/signature/letUnwrap.resi:1:1-4 + + 1 │ let? foo: string + + let? is not allowed in signatures. Use a regular `let` instead. + +val foo : string \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/errors/signature/letUnwrap.resi b/tests/syntax_tests/data/parsing/errors/signature/letUnwrap.resi new file mode 100644 index 0000000000..4b31b705e8 --- /dev/null +++ b/tests/syntax_tests/data/parsing/errors/signature/letUnwrap.resi @@ -0,0 +1 @@ +let? foo: string \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/expressions/expected/letUnwrap.res.txt b/tests/syntax_tests/data/parsing/grammar/expressions/expected/letUnwrap.res.txt new file mode 100644 index 0000000000..46810e3112 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/expressions/expected/letUnwrap.res.txt @@ -0,0 +1,4 @@ +let Ok foo = someResult[@@let.unwrap ] +let Some bar = someOption[@@let.unwrap ] +let Some baz = someOption[@@let.unwrap ] +and Some bar = someOtherOption[@@let.unwrap ] \ No newline at end of file diff --git a/tests/syntax_tests/data/parsing/grammar/expressions/letUnwrap.res b/tests/syntax_tests/data/parsing/grammar/expressions/letUnwrap.res new file mode 100644 index 0000000000..bf141d4d09 --- /dev/null +++ b/tests/syntax_tests/data/parsing/grammar/expressions/letUnwrap.res @@ -0,0 +1,9 @@ +// with Ok +let? Ok(foo) = someResult + +// with Some +let? Some(bar) = someOption + +// with and +let? Some(baz) = someOption +and Some(bar) = someOtherOption \ No newline at end of file diff --git a/tests/syntax_tests/data/printer/expr/expected/letUnwrap.res.txt b/tests/syntax_tests/data/printer/expr/expected/letUnwrap.res.txt new file mode 100644 index 0000000000..4f64a35929 --- /dev/null +++ b/tests/syntax_tests/data/printer/expr/expected/letUnwrap.res.txt @@ -0,0 +1,9 @@ +// with Ok +let? Ok(foo) = someResult + +// with Some +let? Some(bar) = someOption + +// with and +let? Some(baz) = someOption +and Some(bar) = someOtherOption diff --git a/tests/syntax_tests/data/printer/expr/letUnwrap.res b/tests/syntax_tests/data/printer/expr/letUnwrap.res new file mode 100644 index 0000000000..bf141d4d09 --- /dev/null +++ b/tests/syntax_tests/data/printer/expr/letUnwrap.res @@ -0,0 +1,9 @@ +// with Ok +let? Ok(foo) = someResult + +// with Some +let? Some(bar) = someOption + +// with and +let? Some(baz) = someOption +and Some(bar) = someOtherOption \ No newline at end of file diff --git a/tests/syntax_tests/res_test.ml b/tests/syntax_tests/res_test.ml index a7a0d57b4f..505637dd04 100644 --- a/tests/syntax_tests/res_test.ml +++ b/tests/syntax_tests/res_test.ml @@ -94,7 +94,7 @@ module ParserApiTest = struct assert (parser.scanner.lnum == 1); assert (parser.scanner.line_offset == 0); assert (parser.scanner.offset == 6); - assert (parser.token = Res_token.Let); + assert (parser.token = Res_token.Let {unwrap = false}); print_endline "✅ Parser make: initializes parser and checking offsets" let unix_lf () = diff --git a/tests/tests/src/LetUnwrap.res b/tests/tests/src/LetUnwrap.res index 2a1cee99cf..6a52c29ea1 100644 --- a/tests/tests/src/LetUnwrap.res +++ b/tests/tests/src/LetUnwrap.res @@ -11,8 +11,8 @@ let doNextStuffWithResult = s => } let getXWithResult = s => { - @let.unwrap let Ok(y) = doStuffWithResult(s) - @let.unwrap let Ok(x) = doNextStuffWithResult(y) + let? Ok(y) = doStuffWithResult(s) + let? Ok(x) = doNextStuffWithResult(y) Ok(x ++ y) } @@ -35,8 +35,8 @@ let doNextStuffWithOption = s => } let getXWithOption = s => { - @let.unwrap let Some(y) = doStuffWithOption(s) - @let.unwrap let Some(x) = doNextStuffWithOption(y) + let? Some(y) = doStuffWithOption(s) + let? Some(x) = doNextStuffWithOption(y) Some(x ++ y) } @@ -62,8 +62,8 @@ let decodeResAsync = async res => { } let getXWithResultAsync = async s => { - @let.unwrap let Ok({s} as res) = await doStuffResultAsync(s) + let? Ok({s} as res) = await doStuffResultAsync(s) Console.log(s) - @let.unwrap let Ok(x) = await decodeResAsync(res) + let? Ok(x) = await decodeResAsync(res) Ok(x) } From 5122f1886f93f2f685014d3b7bc1ac6350c7c0fd Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Tue, 1 Jul 2025 21:25:16 +0200 Subject: [PATCH 03/21] fix loc and put error case first for better errors --- compiler/frontend/bs_builtin_ppx.ml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index 8a4f5042de..c632f24700 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -180,7 +180,7 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) pc_rhs = body; } in - let loc = Location.none in + let loc = {pvb_pat.ppat_loc with loc_ghost = true} in let error_case = match variant with | `Result -> @@ -209,7 +209,7 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) default_expr_mapper self { e with - pexp_desc = Pexp_match (pvb_expr, [ok_case; error_case]); + pexp_desc = Pexp_match (pvb_expr, [error_case; ok_case]); pexp_attributes = e.pexp_attributes @ pvb_attributes; }) | Pexp_let From a284c86f15f06d1de008b27353a81ce8c797e401 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 2 Jul 2025 08:29:14 +0200 Subject: [PATCH 04/21] changed test output --- tests/tests/src/LetUnwrap.mjs | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/tests/src/LetUnwrap.mjs b/tests/tests/src/LetUnwrap.mjs index b9cc30264d..89d2ef8010 100644 --- a/tests/tests/src/LetUnwrap.mjs +++ b/tests/tests/src/LetUnwrap.mjs @@ -30,24 +30,24 @@ function doNextStuffWithResult(s) { } function getXWithResult(s) { - let y = doStuffWithResult(s); - if (y.TAG !== "Ok") { + let e = doStuffWithResult(s); + if (e.TAG !== "Ok") { return { TAG: "Error", - _0: y._0 + _0: e._0 }; } - let y$1 = y._0; - let x = doNextStuffWithResult(y$1); - if (x.TAG === "Ok") { + let y = e._0; + let e$1 = doNextStuffWithResult(y); + if (e$1.TAG === "Ok") { return { TAG: "Ok", - _0: x._0 + y$1 + _0: e$1._0 + y }; } else { return { TAG: "Error", - _0: x._0 + _0: e$1._0 }; } } @@ -122,25 +122,25 @@ async function decodeResAsync(res) { } async function getXWithResultAsync(s) { - let res = await doStuffResultAsync(s); - if (res.TAG !== "Ok") { + let e = await doStuffResultAsync(s); + if (e.TAG !== "Ok") { return { TAG: "Error", - _0: res._0 + _0: e._0 }; } - let res$1 = res._0; - console.log(res$1.s); - let x = await decodeResAsync(res$1); - if (x.TAG === "Ok") { + let res = e._0; + console.log(res.s); + let e$1 = await decodeResAsync(res); + if (e$1.TAG === "Ok") { return { TAG: "Ok", - _0: x._0 + _0: e$1._0 }; } else { return { TAG: "Error", - _0: x._0 + _0: e$1._0 }; } } From f286bcef10b8b89c03eed3d354d881feb3cfd1d4 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 20 Aug 2025 13:49:54 +0200 Subject: [PATCH 05/21] fix --- compiler/syntax/src/res_token_debugger.ml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/syntax/src/res_token_debugger.ml b/compiler/syntax/src/res_token_debugger.ml index 6f3631ec20..852bc110a3 100644 --- a/compiler/syntax/src/res_token_debugger.ml +++ b/compiler/syntax/src/res_token_debugger.ml @@ -33,7 +33,7 @@ let dump_tokens filename = | Res_token.DotDotDot -> "DotDotDot" | Res_token.Bang -> "Bang" | Res_token.Semicolon -> "Semicolon" - | Res_token.Let -> "Let" + | Res_token.Let {unwrap} -> "Let" ^ if unwrap then "?" else "" | Res_token.And -> "And" | Res_token.Rec -> "Rec" | Res_token.Underscore -> "Underscore" From f6083fa6f459b0e1da4b79e46d90f9830ca59af6 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 20 Aug 2025 14:17:58 +0200 Subject: [PATCH 06/21] support let? on Error and None as well --- compiler/frontend/bs_builtin_ppx.ml | 81 +++++++++++++++++++++-------- tests/tests/src/LetUnwrap.mjs | 72 ++++++++++++++++++------- tests/tests/src/LetUnwrap.res | 16 ++++++ 3 files changed, 126 insertions(+), 43 deletions(-) diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index c632f24700..beee9ece16 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -144,8 +144,10 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) default_expr_mapper self {e with pexp_desc = Pexp_ifthenelse (b, t_exp, Some f_exp)} (* Transform: - - `@let.unwrap let Ok(inner_pat) = expr` - - `@let.unwrap let Some(inner_pat) = expr` + - `@let.unwrap let Ok(inner_pat) = expr` + - `@let.unwrap let Error(inner_pat) = expr` + - `@let.unwrap let Some(inner_pat) = expr` + - `@let.unwrap let None = expr` ...into switches *) | Pexp_let ( Nonrecursive, @@ -154,9 +156,14 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) pvb_pat = { ppat_desc = - Ppat_construct - ( {txt = Lident (("Ok" | "Some") as variant_name)}, - Some _inner_pat ); + ( Ppat_construct + ({txt = Lident ("Ok" as variant_name)}, Some _) + | Ppat_construct + ({txt = Lident ("Error" as variant_name)}, Some _) + | Ppat_construct + ({txt = Lident ("Some" as variant_name)}, Some _) + | Ppat_construct + ({txt = Lident ("None" as variant_name)}, None) ); } as pvb_pat; pvb_expr; pvb_attributes; @@ -164,15 +171,17 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) ], body ) when Ast_attributes.has_unwrap_attr pvb_attributes -> ( - let variant = + let variant : [`Result_Ok | `Result_Error | `Option_Some | `Option_None] = match variant_name with - | "Ok" -> `Result - | _ -> `Option + | "Ok" -> `Result_Ok + | "Error" -> `Result_Error + | "Some" -> `Option_Some + | _ -> `Option_None in match pvb_expr.pexp_desc with | Pexp_pack _ -> default_expr_mapper self e | _ -> - let ok_case = + let cont_case = { Parsetree.pc_bar = None; pc_lhs = pvb_pat; @@ -181,35 +190,61 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) } in let loc = {pvb_pat.ppat_loc with loc_ghost = true} in - let error_case = + let early_case = match variant with - | `Result -> + (* Result: continue on Ok(_), early-return on Error(e) *) + | `Result_Ok -> { Parsetree.pc_bar = None; pc_lhs = - Ast_helper.Pat.construct ~loc - {txt = Lident "Error"; loc} - (Some (Ast_helper.Pat.var ~loc {txt = "e"; loc})); + Ast_helper.Pat.alias + (Ast_helper.Pat.construct ~loc + {txt = Lident "Error"; loc} + (Some (Ast_helper.Pat.any ~loc ()))) + {txt = "e"; loc}; pc_guard = None; - pc_rhs = - Ast_helper.Exp.construct ~loc - {txt = Lident "Error"; loc} - (Some (Ast_helper.Exp.ident ~loc {txt = Lident "e"; loc})); + pc_rhs = Ast_helper.Exp.ident ~loc {txt = Lident "e"; loc}; } - | `Option -> + (* Result: continue on Error(_), early-return on Ok(x) *) + | `Result_Error -> { Parsetree.pc_bar = None; pc_lhs = - Ast_helper.Pat.construct ~loc {txt = Lident "None"; loc} None; + Ast_helper.Pat.alias + (Ast_helper.Pat.construct ~loc {txt = Lident "Ok"; loc} + (Some (Ast_helper.Pat.any ~loc ()))) + {txt = "x"; loc}; pc_guard = None; - pc_rhs = - Ast_helper.Exp.construct ~loc {txt = Lident "None"; loc} None; + pc_rhs = Ast_helper.Exp.ident ~loc {txt = Lident "x"; loc}; + } + (* Option: continue on Some(_), early-return on None *) + | `Option_Some -> + { + Parsetree.pc_bar = None; + pc_lhs = + Ast_helper.Pat.alias + (Ast_helper.Pat.construct ~loc {txt = Lident "None"; loc} None) + {txt = "x"; loc}; + pc_guard = None; + pc_rhs = Ast_helper.Exp.ident ~loc {txt = Lident "x"; loc}; + } + (* Option: continue on None, early-return on Some(x) *) + | `Option_None -> + { + Parsetree.pc_bar = None; + pc_lhs = + Ast_helper.Pat.alias + (Ast_helper.Pat.construct ~loc {txt = Lident "Some"; loc} + (Some (Ast_helper.Pat.any ~loc ()))) + {txt = "x"; loc}; + pc_guard = None; + pc_rhs = Ast_helper.Exp.ident ~loc {txt = Lident "x"; loc}; } in default_expr_mapper self { e with - pexp_desc = Pexp_match (pvb_expr, [error_case; ok_case]); + pexp_desc = Pexp_match (pvb_expr, [early_case; cont_case]); pexp_attributes = e.pexp_attributes @ pvb_attributes; }) | Pexp_let diff --git a/tests/tests/src/LetUnwrap.mjs b/tests/tests/src/LetUnwrap.mjs index 89d2ef8010..9bf42700b7 100644 --- a/tests/tests/src/LetUnwrap.mjs +++ b/tests/tests/src/LetUnwrap.mjs @@ -32,10 +32,7 @@ function doNextStuffWithResult(s) { function getXWithResult(s) { let e = doStuffWithResult(s); if (e.TAG !== "Ok") { - return { - TAG: "Error", - _0: e._0 - }; + return e; } let y = e._0; let e$1 = doNextStuffWithResult(y); @@ -45,10 +42,7 @@ function getXWithResult(s) { _0: e$1._0 + y }; } else { - return { - TAG: "Error", - _0: e$1._0 - }; + return e$1; } } @@ -75,15 +69,16 @@ function doNextStuffWithOption(s) { } function getXWithOption(s) { - let y = doStuffWithOption(s); - if (y === undefined) { - return; + let x = doStuffWithOption(s); + if (x === undefined) { + return x; } - let x = doNextStuffWithOption(y); - if (x !== undefined) { - return x + y; + let x$1 = doNextStuffWithOption(x); + if (x$1 !== undefined) { + return x$1 + x; + } else { + return x$1; } - } let x$1 = getXWithOption("s"); @@ -124,10 +119,7 @@ async function decodeResAsync(res) { async function getXWithResultAsync(s) { let e = await doStuffResultAsync(s); if (e.TAG !== "Ok") { - return { - TAG: "Error", - _0: e._0 - }; + return e; } let res = e._0; console.log(res.s); @@ -137,10 +129,47 @@ async function getXWithResultAsync(s) { TAG: "Ok", _0: e$1._0 }; + } else { + return e$1; + } +} + +function returnsAliasOnFirstError(s) { + let e = doStuffWithResult(s); + if (e.TAG === "Ok") { + return { + TAG: "Ok", + _0: "ok" + }; + } else { + return e; + } +} + +function returnsAliasOnSecondError(s) { + let e = doStuffWithResult(s); + if (e.TAG !== "Ok") { + return e; + } + let e$1 = doNextStuffWithResult(e._0); + if (e$1.TAG === "Ok") { + return { + TAG: "Ok", + _0: "ok" + }; + } else { + return e$1; + } +} + +function returnsAliasOnOk(s) { + let x = doStuffWithResult(s); + if (x.TAG === "Ok") { + return x; } else { return { TAG: "Error", - _0: e$1._0 + _0: "GotError" }; } } @@ -157,5 +186,8 @@ export { doStuffResultAsync, decodeResAsync, getXWithResultAsync, + returnsAliasOnFirstError, + returnsAliasOnSecondError, + returnsAliasOnOk, } /* x Not a pure module */ diff --git a/tests/tests/src/LetUnwrap.res b/tests/tests/src/LetUnwrap.res index 6a52c29ea1..df3ada1533 100644 --- a/tests/tests/src/LetUnwrap.res +++ b/tests/tests/src/LetUnwrap.res @@ -67,3 +67,19 @@ let getXWithResultAsync = async s => { let? Ok(x) = await decodeResAsync(res) Ok(x) } + +let returnsAliasOnFirstError = s => { + let? Ok(_y) = doStuffWithResult(s) + Ok("ok") +} + +let returnsAliasOnSecondError = s => { + let? Ok(y) = doStuffWithResult(s) + let? Ok(_x) = doNextStuffWithResult(y) + Ok("ok") +} + +let returnsAliasOnOk = s => { + let? Error(_e) = doStuffWithResult(s) + Error(#GotError) +} From 480fd04d96f056f0916b8d9d8c47e5a7c8de1970 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 21 Aug 2025 08:55:10 +0200 Subject: [PATCH 07/21] add feature for shipping experimental features --- compiler/bsc/rescript_compiler_main.ml | 4 ++ compiler/common/experimental_features.ml | 18 +++++++ compiler/common/experimental_features.mli | 5 ++ compiler/frontend/bs_builtin_ppx.ml | 4 ++ compiler/frontend/bs_syntaxerr.ml | 9 +++- compiler/frontend/bs_syntaxerr.mli | 1 + docs/docson/build-schema.json | 11 +++++ rewatch/CompilerConfigurationSpec.md | 12 +++++ rewatch/src/build/compile.rs | 2 + rewatch/src/config.rs | 58 ++++++++++++++++++++++- rewatch/tests/experimental-invalid.sh | 36 ++++++++++++++ rewatch/tests/experimental.sh | 38 +++++++++++++++ rewatch/tests/suite-ci.sh | 2 +- tests/tests/src/LetUnwrap.res | 2 + 14 files changed, 199 insertions(+), 3 deletions(-) create mode 100644 compiler/common/experimental_features.ml create mode 100644 compiler/common/experimental_features.mli create mode 100755 rewatch/tests/experimental-invalid.sh create mode 100755 rewatch/tests/experimental.sh diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index 0042b4f5b9..8aaf91a2ee 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -393,6 +393,10 @@ let command_line_flags : (string * Bsc_args.spec * string) array = ( "-absname", set absname, "*internal* Show absolute filenames in error messages" ); + ( "-enable-experimental", + string_call Experimental_features.enable_from_string, + "Enable experimental features: repeatable, e.g. -enable-experimental \ + LetUnwrap" ); (* Not used, the build system did the expansion *) ( "-bs-no-bin-annot", clear Clflags.binary_annotations, diff --git a/compiler/common/experimental_features.ml b/compiler/common/experimental_features.ml new file mode 100644 index 0000000000..aeb1ce54fe --- /dev/null +++ b/compiler/common/experimental_features.ml @@ -0,0 +1,18 @@ +type feature = LetUnwrap + +let to_string (f : feature) : string = + match f with + | LetUnwrap -> "LetUnwrap" + +let from_string (s : string) : feature option = + match s with + | "LetUnwrap" -> Some LetUnwrap + | _ -> None + +let enabled_features : feature list ref = ref [] +let enable_from_string (s : string) = + match from_string s with + | Some f -> enabled_features := f :: !enabled_features + | None -> () + +let is_enabled (f : feature) = List.mem f !enabled_features diff --git a/compiler/common/experimental_features.mli b/compiler/common/experimental_features.mli new file mode 100644 index 0000000000..bc58c931c2 --- /dev/null +++ b/compiler/common/experimental_features.mli @@ -0,0 +1,5 @@ +type feature = LetUnwrap + +val enable_from_string : string -> unit +val is_enabled : feature -> bool +val to_string : feature -> string diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index beee9ece16..4f306cda3c 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -171,6 +171,10 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) ], body ) when Ast_attributes.has_unwrap_attr pvb_attributes -> ( + if not (Experimental_features.is_enabled Experimental_features.LetUnwrap) + then + Bs_syntaxerr.err pvb_pat.ppat_loc + (Experimental_feature_not_enabled LetUnwrap); let variant : [`Result_Ok | `Result_Error | `Option_Some | `Option_None] = match variant_name with | "Ok" -> `Result_Ok diff --git a/compiler/frontend/bs_syntaxerr.ml b/compiler/frontend/bs_syntaxerr.ml index 88ea8ac270..0b0bace60f 100644 --- a/compiler/frontend/bs_syntaxerr.ml +++ b/compiler/frontend/bs_syntaxerr.ml @@ -47,6 +47,7 @@ type error = | Misplaced_label_syntax | Optional_in_uncurried_bs_attribute | Bs_this_simple_pattern + | Experimental_feature_not_enabled of Experimental_features.feature let pp_error fmt err = Format.pp_print_string fmt @@ -82,7 +83,13 @@ let pp_error fmt err = each constructor must have an argument." | Conflict_ffi_attribute str -> "Conflicting attributes: " ^ str | Bs_this_simple_pattern -> - "%@this expect its pattern variable to be simple form") + "%@this expect its pattern variable to be simple form" + | Experimental_feature_not_enabled feature -> + Printf.sprintf + "Experimental feature not enabled: %s. Enable it by setting \"%s\" to \ + true under \"experimentalFeatures\" in rescript.json" + (Experimental_features.to_string feature) + (Experimental_features.to_string feature)) type exn += Error of Location.t * error diff --git a/compiler/frontend/bs_syntaxerr.mli b/compiler/frontend/bs_syntaxerr.mli index d5b3c9b6f9..6602a36c18 100644 --- a/compiler/frontend/bs_syntaxerr.mli +++ b/compiler/frontend/bs_syntaxerr.mli @@ -47,6 +47,7 @@ type error = | Misplaced_label_syntax | Optional_in_uncurried_bs_attribute | Bs_this_simple_pattern + | Experimental_feature_not_enabled of Experimental_features.feature val err : Location.t -> error -> 'a diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index fa46f507e6..f22af7fc7a 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -484,6 +484,17 @@ "editor": { "$ref": "#/definitions/editor", "description": "Configure editor functionality, like modules that should be included in autocompletions for given (built-in) types." + }, + "experimentalFeatures": { + "type": "object", + "description": "Enable experimental compiler features.", + "properties": { + "LetUnwrap": { + "type": "boolean", + "description": "Enable let? syntax." + } + }, + "additionalProperties": false } }, "additionalProperties": false, diff --git a/rewatch/CompilerConfigurationSpec.md b/rewatch/CompilerConfigurationSpec.md index b47b4da6d5..6568945818 100644 --- a/rewatch/CompilerConfigurationSpec.md +++ b/rewatch/CompilerConfigurationSpec.md @@ -32,6 +32,7 @@ This document contains a list of all bsconfig parameters with remarks, and wheth | bs-external-includes | array of string | | [_] | | suffix | Suffix | | [x] | | reanalyze | Reanalyze | | [_] | +| experimentalFeatures | ExperimentalFeatures | | [x] | ### Source @@ -111,6 +112,17 @@ enum: "classic" | "automatic" enum: | "dce" | "exception" | "termination" +### ExperimentalFeatures + +An object of feature flags to enable experimental compiler behavior. Only supported by Rewatch. + +- Keys: feature identifiers (PascalCase) +- Values: boolean (true to enable) + +Currently supported features: + +- LetUnwrap: Enable the `let?` transformation. + ### Warnings | Parameter | JSON type | Remark | Implemented? | diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index f62a5123d8..f5b908ea11 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -381,6 +381,7 @@ pub fn compiler_args( let jsx_mode_args = root_config.get_jsx_mode_args(); let jsx_preserve_args = root_config.get_jsx_preserve_args(); let gentype_arg = config.get_gentype_arg(); + let experimental_args = root_config.get_experimental_features_args(); let warning_args = config.get_warning_args(is_local_dep); let read_cmi_args = match has_interface { @@ -445,6 +446,7 @@ pub fn compiler_args( bsc_flags.to_owned(), warning_args, gentype_arg, + experimental_args, // vec!["-warn-error".to_string(), "A".to_string()], // ^^ this one fails for bisect-ppx // this is the default diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 43391a9f22..c9e6653922 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -4,7 +4,9 @@ use crate::helpers::deserialize::*; use crate::project_context::ProjectContext; use anyhow::{Result, bail}; use convert_case::{Case, Casing}; -use serde::Deserialize; +use serde::de::{Error as DeError, Visitor}; +use serde::{Deserialize, Deserializer}; +use std::collections::HashMap; use std::fs; use std::path::{MAIN_SEPARATOR, Path, PathBuf}; @@ -224,6 +226,39 @@ pub enum DeprecationWarning { BscFlags, } +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum ExperimentalFeature { + LetUnwrap, +} + +impl<'de> serde::Deserialize<'de> for ExperimentalFeature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct EFVisitor; + impl<'de> Visitor<'de> for EFVisitor { + type Value = ExperimentalFeature; + fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "a valid experimental feature id (e.g. LetUnwrap)") + } + fn visit_str(self, v: &str) -> Result + where + E: DeError, + { + match v { + "LetUnwrap" => Ok(ExperimentalFeature::LetUnwrap), + other => Err(DeError::custom(format!( + "Unknown experimental feature '{}'. Available features: LetUnwrap", + other + ))), + } + } + } + deserializer.deserialize_any(EFVisitor) + } +} + /// # rescript.json representation /// This is tricky, there is a lot of ambiguity. This is probably incomplete. #[derive(Deserialize, Debug, Clone, Default)] @@ -256,6 +291,8 @@ pub struct Config { pub namespace: Option, pub jsx: Option, + #[serde(rename = "experimentalFeatures")] + pub experimental_features: Option>, #[serde(rename = "gentypeconfig")] pub gentype_config: Option, // this is a new feature of rewatch, and it's not part of the rescript.json spec @@ -498,6 +535,25 @@ impl Config { } } + pub fn get_experimental_features_args(&self) -> Vec { + match &self.experimental_features { + None => vec![], + Some(map) => map + .iter() + .filter_map(|(k, v)| if *v { Some(k) } else { None }) + .flat_map(|feature| { + vec![ + "-enable-experimental".to_string(), + match feature { + ExperimentalFeature::LetUnwrap => "LetUnwrap", + } + .to_string(), + ] + }) + .collect(), + } + } + pub fn get_gentype_arg(&self) -> Vec { match &self.gentype_config { Some(_) => vec!["-bs-gentype".to_string()], diff --git a/rewatch/tests/experimental-invalid.sh b/rewatch/tests/experimental-invalid.sh new file mode 100755 index 0000000000..bb93545aff --- /dev/null +++ b/rewatch/tests/experimental-invalid.sh @@ -0,0 +1,36 @@ +#!/bin/bash +cd $(dirname $0) +source "./utils.sh" +cd ../testrepo + +bold "Test: invalid experimentalFeatures keys produce helpful error" + +cp rescript.json rescript.json.bak + +node -e ' +const fs=require("fs"); +const j=JSON.parse(fs.readFileSync("rescript.json","utf8")); +j.experimentalFeatures={FooBar:true}; +fs.writeFileSync("rescript.json", JSON.stringify(j,null,2)); +' + +out=$(rewatch compiler-args packages/file-casing/src/Consume.res 2>&1) +status=$? + +mv rescript.json.bak rescript.json + +if [ $status -eq 0 ]; then + error "Expected compiler-args to fail for unknown experimental feature" + echo "$out" + exit 1 +fi + +echo "$out" | grep -q "Unknown experimental feature 'FooBar'. Available features: LetUnwrap" +if [ $? -ne 0 ]; then + error "Missing helpful message for unknown experimental feature" + echo "$out" + exit 1 +fi + +success "invalid experimentalFeatures produces helpful error" + diff --git a/rewatch/tests/experimental.sh b/rewatch/tests/experimental.sh new file mode 100755 index 0000000000..f38ae1617c --- /dev/null +++ b/rewatch/tests/experimental.sh @@ -0,0 +1,38 @@ +#!/bin/bash +cd $(dirname $0) +source "./utils.sh" +cd ../testrepo + +bold "Test: experimentalFeatures in rescript.json emits -enable-experimental as string list" + +# Backup rescript.json +cp rescript.json rescript.json.bak + +# Inject experimentalFeatures enabling LetUnwrap using node for portability +node -e ' +const fs=require("fs"); +const j=JSON.parse(fs.readFileSync("rescript.json","utf8")); +j.experimentalFeatures={LetUnwrap:true}; +fs.writeFileSync("rescript.json", JSON.stringify(j,null,2)); +' + +stdout=$(rewatch compiler-args packages/file-casing/src/Consume.res 2>/dev/null) +if [ $? -ne 0 ]; then + mv rescript.json.bak rescript.json + error "Error grabbing compiler args with experimentalFeatures enabled" + exit 1 +fi + +# Expect repeated string-list style: presence of -enable-experimental and LetUnwrap entries +echo "$stdout" | grep -q '"-enable-experimental"' && echo "$stdout" | grep -q '"LetUnwrap"' +if [ $? -ne 0 ]; then + mv rescript.json.bak rescript.json + error "-enable-experimental / LetUnwrap not found in compiler-args output" + echo "$stdout" + exit 1 +fi + +# Restore original rescript.json +mv rescript.json.bak rescript.json + +success "experimentalFeatures emits -enable-experimental as string list" diff --git a/rewatch/tests/suite-ci.sh b/rewatch/tests/suite-ci.sh index fc1d615643..d7da7de3ce 100755 --- a/rewatch/tests/suite-ci.sh +++ b/rewatch/tests/suite-ci.sh @@ -43,4 +43,4 @@ else exit 1 fi -./compile.sh && ./watch.sh && ./lock.sh && ./suffix.sh && ./legacy.sh && ./format.sh && ./clean.sh && ./compiler-args.sh \ No newline at end of file +./compile.sh && ./watch.sh && ./lock.sh && ./suffix.sh && ./legacy.sh && ./format.sh && ./clean.sh && ./experimental.sh && ./experimental-invalid.sh && ./compiler-args.sh diff --git a/tests/tests/src/LetUnwrap.res b/tests/tests/src/LetUnwrap.res index df3ada1533..9611cc9adb 100644 --- a/tests/tests/src/LetUnwrap.res +++ b/tests/tests/src/LetUnwrap.res @@ -1,3 +1,5 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + let doStuffWithResult = s => switch s { | "s" => Ok("hello") From 4be85789b9d0df85b5e471853961a115715ca97f Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 21 Aug 2025 08:58:02 +0200 Subject: [PATCH 08/21] error test for not enabled feature --- .../feature_letunwrap_not_enabled.res.expected | 11 +++++++++++ .../fixtures/feature_letunwrap_not_enabled.res | 6 ++++++ 2 files changed, 17 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/feature_letunwrap_not_enabled.res diff --git a/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected b/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected new file mode 100644 index 0000000000..7facd8a5c8 --- /dev/null +++ b/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/feature_letunwrap_not_enabled.res:4:8-13 + + 2 │ + 3 │ let x = { + 4 │ let? Ok(_x) = ok + 5 │ Ok() + 6 │ } + + Experimental feature not enabled: LetUnwrap. Enable it by setting "LetUnwrap" to true under "experimentalFeatures" in rescript.json \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/feature_letunwrap_not_enabled.res b/tests/build_tests/super_errors/fixtures/feature_letunwrap_not_enabled.res new file mode 100644 index 0000000000..1c8ca1c6a8 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/feature_letunwrap_not_enabled.res @@ -0,0 +1,6 @@ +let ok = Ok(1) + +let x = { + let? Ok(_x) = ok + Ok() +} From 41604520bff9a49e54589809071ac1a97f647989 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Thu, 21 Aug 2025 09:00:42 +0200 Subject: [PATCH 09/21] fix --- rewatch/src/config.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index c9e6653922..c690f407b1 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -719,6 +719,7 @@ pub mod tests { gentype_config: None, namespace_entry: None, deprecation_warnings: vec![], + experimental_features: None, allowed_dependents: args.allowed_dependents, path: args.path, } From 712249b3734d833ebbb5eec0ed40fac6e040c0b5 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 08:29:32 +0200 Subject: [PATCH 10/21] error handling --- compiler/frontend/bs_builtin_ppx.ml | 22 +++++++++++++++++++ compiler/frontend/bs_syntaxerr.ml | 9 +++++++- compiler/frontend/bs_syntaxerr.mli | 1 + .../{common => ml}/experimental_features.ml | 11 +++++++--- .../{common => ml}/experimental_features.mli | 0 ...wrap_on_not_supported_variant.res.expected | 11 ++++++++++ .../let_unwrap_on_top_level.res.expected | 10 +++++++++ .../let_unwrap_on_not_supported_variant.res | 15 +++++++++++++ .../fixtures/let_unwrap_on_top_level.res | 3 +++ 9 files changed, 78 insertions(+), 4 deletions(-) rename compiler/{common => ml}/experimental_features.ml (51%) rename compiler/{common => ml}/experimental_features.mli (100%) create mode 100644 tests/build_tests/super_errors/expected/let_unwrap_on_not_supported_variant.res.expected create mode 100644 tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/let_unwrap_on_not_supported_variant.res create mode 100644 tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index 4f306cda3c..b2e1595a28 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -149,6 +149,15 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) - `@let.unwrap let Some(inner_pat) = expr` - `@let.unwrap let None = expr` ...into switches *) + | Pexp_let (_, [{pvb_pat; pvb_attributes}], _) + when Ast_attributes.has_unwrap_attr pvb_attributes -> + if not (Experimental_features.is_enabled Experimental_features.LetUnwrap) + then + Bs_syntaxerr.err pvb_pat.ppat_loc + (Experimental_feature_not_enabled LetUnwrap) + else + Bs_syntaxerr.err pvb_pat.ppat_loc + (LetUnwrap_not_supported_in_position `Unsupported_type) | Pexp_let ( Nonrecursive, [ @@ -441,6 +450,19 @@ let signature_item_mapper (self : mapper) (sigi : Parsetree.signature_item) : let structure_item_mapper (self : mapper) (str : Parsetree.structure_item) : Parsetree.structure_item = match str.pstr_desc with + | Pstr_value (_, vbs) + when List.exists + (fun (vb : Parsetree.value_binding) -> + Ast_attributes.has_unwrap_attr vb.pvb_attributes) + vbs -> + let vb = + List.find + (fun (vb : Parsetree.value_binding) -> + Ast_attributes.has_unwrap_attr vb.pvb_attributes) + vbs + in + Bs_syntaxerr.err vb.pvb_pat.ppat_loc + (LetUnwrap_not_supported_in_position `Toplevel) | Pstr_type (rf, tdcls) (* [ {ptype_attributes} as tdcl ] *) -> Ast_tdcls.handle_tdcls_in_stru self str rf tdcls | Pstr_primitive prim diff --git a/compiler/frontend/bs_syntaxerr.ml b/compiler/frontend/bs_syntaxerr.ml index 0b0bace60f..30334c7076 100644 --- a/compiler/frontend/bs_syntaxerr.ml +++ b/compiler/frontend/bs_syntaxerr.ml @@ -48,6 +48,7 @@ type error = | Optional_in_uncurried_bs_attribute | Bs_this_simple_pattern | Experimental_feature_not_enabled of Experimental_features.feature + | LetUnwrap_not_supported_in_position of [`Toplevel | `Unsupported_type] let pp_error fmt err = Format.pp_print_string fmt @@ -89,7 +90,13 @@ let pp_error fmt err = "Experimental feature not enabled: %s. Enable it by setting \"%s\" to \ true under \"experimentalFeatures\" in rescript.json" (Experimental_features.to_string feature) - (Experimental_features.to_string feature)) + (Experimental_features.to_string feature) + | LetUnwrap_not_supported_in_position hint -> ( + match hint with + | `Toplevel -> "`let?` is not allowed for top-level bindings." + | `Unsupported_type -> + "`let?` is only supported in let bindings targeting the `result` or \ + `option` type.")) type exn += Error of Location.t * error diff --git a/compiler/frontend/bs_syntaxerr.mli b/compiler/frontend/bs_syntaxerr.mli index 6602a36c18..ecdaaaa0e6 100644 --- a/compiler/frontend/bs_syntaxerr.mli +++ b/compiler/frontend/bs_syntaxerr.mli @@ -48,6 +48,7 @@ type error = | Optional_in_uncurried_bs_attribute | Bs_this_simple_pattern | Experimental_feature_not_enabled of Experimental_features.feature + | LetUnwrap_not_supported_in_position of [`Toplevel | `Unsupported_type] val err : Location.t -> error -> 'a diff --git a/compiler/common/experimental_features.ml b/compiler/ml/experimental_features.ml similarity index 51% rename from compiler/common/experimental_features.ml rename to compiler/ml/experimental_features.ml index aeb1ce54fe..179f43735b 100644 --- a/compiler/common/experimental_features.ml +++ b/compiler/ml/experimental_features.ml @@ -9,10 +9,15 @@ let from_string (s : string) : feature option = | "LetUnwrap" -> Some LetUnwrap | _ -> None -let enabled_features : feature list ref = ref [] +module FeatureSet = Set.Make (struct + type t = feature + let compare = compare +end) + +let enabled_features : FeatureSet.t ref = ref FeatureSet.empty let enable_from_string (s : string) = match from_string s with - | Some f -> enabled_features := f :: !enabled_features + | Some f -> enabled_features := FeatureSet.add f !enabled_features | None -> () -let is_enabled (f : feature) = List.mem f !enabled_features +let is_enabled (f : feature) = FeatureSet.mem f !enabled_features diff --git a/compiler/common/experimental_features.mli b/compiler/ml/experimental_features.mli similarity index 100% rename from compiler/common/experimental_features.mli rename to compiler/ml/experimental_features.mli diff --git a/tests/build_tests/super_errors/expected/let_unwrap_on_not_supported_variant.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_on_not_supported_variant.res.expected new file mode 100644 index 0000000000..4ebc684b8e --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_unwrap_on_not_supported_variant.res.expected @@ -0,0 +1,11 @@ + + We've found a bug for you! + /.../fixtures/let_unwrap_on_not_supported_variant.res:13:8-16 + + 11 │ + 12 │ let ff = { + 13 │ let? Failed(x) = xx + 14 │ Ok(x) + 15 │ } + + `let?` is only supported in let bindings targeting the `result` or `option` type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected new file mode 100644 index 0000000000..74aca037c5 --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/let_unwrap_on_top_level.res:3:6-10 + + 1 │ let x = Ok(1) + 2 │ + 3 │ let? Ok(_) = x + 4 │ + + `let?` is not allowed for top-level bindings. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_on_not_supported_variant.res b/tests/build_tests/super_errors/fixtures/let_unwrap_on_not_supported_variant.res new file mode 100644 index 0000000000..40900293a2 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_on_not_supported_variant.res @@ -0,0 +1,15 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + +let x = switch 1 { +| 1 => Ok(1) +| _ => Error(#Invalid) +} + +type ff = Failed(int) | GoOn + +let xx = Failed(1) + +let ff = { + let? Failed(x) = xx + Ok(x) +} diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res b/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res new file mode 100644 index 0000000000..e5be8ded87 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res @@ -0,0 +1,3 @@ +let x = Ok(1) + +let? Ok(_) = x From 3255d10f8ee1a4cd927c8c6d4f23a20d5dc8ed89 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 08:36:24 +0200 Subject: [PATCH 11/21] move to more correct place --- compiler/frontend/bs_builtin_ppx.ml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index b2e1595a28..616df9ebb1 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -143,21 +143,12 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) ] ) -> default_expr_mapper self {e with pexp_desc = Pexp_ifthenelse (b, t_exp, Some f_exp)} - (* Transform: + (* Transform: - `@let.unwrap let Ok(inner_pat) = expr` - `@let.unwrap let Error(inner_pat) = expr` - `@let.unwrap let Some(inner_pat) = expr` - `@let.unwrap let None = expr` ...into switches *) - | Pexp_let (_, [{pvb_pat; pvb_attributes}], _) - when Ast_attributes.has_unwrap_attr pvb_attributes -> - if not (Experimental_features.is_enabled Experimental_features.LetUnwrap) - then - Bs_syntaxerr.err pvb_pat.ppat_loc - (Experimental_feature_not_enabled LetUnwrap) - else - Bs_syntaxerr.err pvb_pat.ppat_loc - (LetUnwrap_not_supported_in_position `Unsupported_type) | Pexp_let ( Nonrecursive, [ @@ -260,6 +251,15 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) pexp_desc = Pexp_match (pvb_expr, [early_case; cont_case]); pexp_attributes = e.pexp_attributes @ pvb_attributes; }) + | Pexp_let (_, [{pvb_pat; pvb_attributes}], _) + when Ast_attributes.has_unwrap_attr pvb_attributes -> + if not (Experimental_features.is_enabled Experimental_features.LetUnwrap) + then + Bs_syntaxerr.err pvb_pat.ppat_loc + (Experimental_feature_not_enabled LetUnwrap) + else + Bs_syntaxerr.err pvb_pat.ppat_loc + (LetUnwrap_not_supported_in_position `Unsupported_type) | Pexp_let ( Nonrecursive, [ From 17b18dbfdb31c1b6607f75036467c5ea18509f2d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 08:37:17 +0200 Subject: [PATCH 12/21] comment --- compiler/frontend/bs_builtin_ppx.ml | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index 616df9ebb1..adc61c15d3 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -253,6 +253,7 @@ let expr_mapper ~async_context ~in_function_def (self : mapper) }) | Pexp_let (_, [{pvb_pat; pvb_attributes}], _) when Ast_attributes.has_unwrap_attr pvb_attributes -> + (* Catch all unsupported cases for `let?` *) if not (Experimental_features.is_enabled Experimental_features.LetUnwrap) then Bs_syntaxerr.err pvb_pat.ppat_loc From 6b1d1962242642aa0f55ac378c96c725f2caffad Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 08:44:18 +0200 Subject: [PATCH 13/21] tweaks --- compiler/frontend/bs_builtin_ppx.ml | 9 +++++++-- compiler/frontend/bs_syntaxerr.ml | 2 +- docs/docson/build-schema.json | 2 +- rewatch/CompilerConfigurationSpec.md | 2 +- .../feature_letunwrap_not_enabled.res.expected | 2 +- .../expected/let_unwrap_on_top_level.res.expected | 8 ++++---- .../let_unwrap_on_top_level_not_enabled.res.expected | 10 ++++++++++ .../super_errors/fixtures/let_unwrap_on_top_level.res | 2 ++ .../fixtures/let_unwrap_on_top_level_not_enabled.res | 3 +++ 9 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/let_unwrap_on_top_level_not_enabled.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level_not_enabled.res diff --git a/compiler/frontend/bs_builtin_ppx.ml b/compiler/frontend/bs_builtin_ppx.ml index adc61c15d3..08ec91fc62 100644 --- a/compiler/frontend/bs_builtin_ppx.ml +++ b/compiler/frontend/bs_builtin_ppx.ml @@ -462,8 +462,13 @@ let structure_item_mapper (self : mapper) (str : Parsetree.structure_item) : Ast_attributes.has_unwrap_attr vb.pvb_attributes) vbs in - Bs_syntaxerr.err vb.pvb_pat.ppat_loc - (LetUnwrap_not_supported_in_position `Toplevel) + if not (Experimental_features.is_enabled Experimental_features.LetUnwrap) + then + Bs_syntaxerr.err vb.pvb_pat.ppat_loc + (Experimental_feature_not_enabled LetUnwrap) + else + Bs_syntaxerr.err vb.pvb_pat.ppat_loc + (LetUnwrap_not_supported_in_position `Toplevel) | Pstr_type (rf, tdcls) (* [ {ptype_attributes} as tdcl ] *) -> Ast_tdcls.handle_tdcls_in_stru self str rf tdcls | Pstr_primitive prim diff --git a/compiler/frontend/bs_syntaxerr.ml b/compiler/frontend/bs_syntaxerr.ml index 30334c7076..e665a630c9 100644 --- a/compiler/frontend/bs_syntaxerr.ml +++ b/compiler/frontend/bs_syntaxerr.ml @@ -88,7 +88,7 @@ let pp_error fmt err = | Experimental_feature_not_enabled feature -> Printf.sprintf "Experimental feature not enabled: %s. Enable it by setting \"%s\" to \ - true under \"experimentalFeatures\" in rescript.json" + true under \"experimentalFeatures\" in rescript.json." (Experimental_features.to_string feature) (Experimental_features.to_string feature) | LetUnwrap_not_supported_in_position hint -> ( diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index f22af7fc7a..a770247c6c 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -491,7 +491,7 @@ "properties": { "LetUnwrap": { "type": "boolean", - "description": "Enable let? syntax." + "description": "Enable `let?` syntax." } }, "additionalProperties": false diff --git a/rewatch/CompilerConfigurationSpec.md b/rewatch/CompilerConfigurationSpec.md index 6568945818..ac93033f6a 100644 --- a/rewatch/CompilerConfigurationSpec.md +++ b/rewatch/CompilerConfigurationSpec.md @@ -121,7 +121,7 @@ An object of feature flags to enable experimental compiler behavior. Only suppor Currently supported features: -- LetUnwrap: Enable the `let?` transformation. +- LetUnwrap: Enable `let?` syntax. ### Warnings diff --git a/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected b/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected index 7facd8a5c8..d38986742e 100644 --- a/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected +++ b/tests/build_tests/super_errors/expected/feature_letunwrap_not_enabled.res.expected @@ -8,4 +8,4 @@ 5 │ Ok() 6 │ } - Experimental feature not enabled: LetUnwrap. Enable it by setting "LetUnwrap" to true under "experimentalFeatures" in rescript.json \ No newline at end of file + Experimental feature not enabled: LetUnwrap. Enable it by setting "LetUnwrap" to true under "experimentalFeatures" in rescript.json. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected index 74aca037c5..f8cb2107e1 100644 --- a/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected +++ b/tests/build_tests/super_errors/expected/let_unwrap_on_top_level.res.expected @@ -1,10 +1,10 @@ We've found a bug for you! - /.../fixtures/let_unwrap_on_top_level.res:3:6-10 + /.../fixtures/let_unwrap_on_top_level.res:5:6-10 - 1 │ let x = Ok(1) - 2 │ - 3 │ let? Ok(_) = x + 3 │ let x = Ok(1) 4 │ + 5 │ let? Ok(_) = x + 6 │ `let?` is not allowed for top-level bindings. \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/let_unwrap_on_top_level_not_enabled.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_on_top_level_not_enabled.res.expected new file mode 100644 index 0000000000..107435eb9c --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_unwrap_on_top_level_not_enabled.res.expected @@ -0,0 +1,10 @@ + + We've found a bug for you! + /.../fixtures/let_unwrap_on_top_level_not_enabled.res:3:6-10 + + 1 │ let x = Ok(1) + 2 │ + 3 │ let? Ok(_) = x + 4 │ + + Experimental feature not enabled: LetUnwrap. Enable it by setting "LetUnwrap" to true under "experimentalFeatures" in rescript.json. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res b/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res index e5be8ded87..f9996b8b45 100644 --- a/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level.res @@ -1,3 +1,5 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + let x = Ok(1) let? Ok(_) = x diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level_not_enabled.res b/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level_not_enabled.res new file mode 100644 index 0000000000..e5be8ded87 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_on_top_level_not_enabled.res @@ -0,0 +1,3 @@ +let x = Ok(1) + +let? Ok(_) = x From 9d93edeefe91999c6d417d34bb5814571ceda6db Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 08:54:24 +0200 Subject: [PATCH 14/21] Update rewatch/src/config.rs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- rewatch/src/config.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index c690f407b1..cb9df97d67 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -248,10 +248,13 @@ impl<'de> serde::Deserialize<'de> for ExperimentalFeature { { match v { "LetUnwrap" => Ok(ExperimentalFeature::LetUnwrap), - other => Err(DeError::custom(format!( - "Unknown experimental feature '{}'. Available features: LetUnwrap", - other - ))), + other => { + let available = ExperimentalFeature::all_names().join(", "); + Err(DeError::custom(format!( + "Unknown experimental feature '{}'. Available features: {}", + other, available + ))) + } } } } From 2fb657c03fbb23ea832fa8764413a5f37df341cb Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 12:05:57 +0200 Subject: [PATCH 15/21] hint about when appropriate --- compiler/ml/parmatch.ml | 15 ++++++++---- compiler/ml/parmatch.mli | 1 + compiler/ml/typecore.ml | 24 ++++++++++++++++--- compiler/ml/typecore.mli | 1 + ...et_without_unwrap_but_elgible.res.expected | 14 +++++++++++ .../let_without_unwrap_but_elgible.res | 8 +++++++ 6 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/let_without_unwrap_but_elgible.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/let_without_unwrap_but_elgible.res diff --git a/compiler/ml/parmatch.ml b/compiler/ml/parmatch.ml index 0dae8985bf..8d1fe66c70 100644 --- a/compiler/ml/parmatch.ml +++ b/compiler/ml/parmatch.ml @@ -2013,7 +2013,7 @@ let ppat_of_type env ty = (Conv.mkpat Parsetree.Ppat_any, Hashtbl.create 0, Hashtbl.create 0) | pats -> Conv.conv (orify_many pats) -let do_check_partial ?pred exhaust loc casel pss = +let do_check_partial ?partial_match_warning_hint ?pred exhaust loc casel pss = match pss with | [] -> (* @@ -2071,6 +2071,11 @@ let do_check_partial ?pred exhaust loc casel pss = Matching over values of extensible variant types (the \ *extension* above)\n\ must include a wild card pattern in order to be exhaustive."; + (match partial_match_warning_hint with + | None -> () + | Some h when String.length h > 0 -> + Buffer.add_string buf ("\n\n " ^ h) + | Some _ -> ()); Buffer.contents buf with _ -> "" in @@ -2083,8 +2088,8 @@ let do_check_partial_normal loc casel pss = do_check_partial exhaust loc casel pss *) -let do_check_partial_gadt pred loc casel pss = - do_check_partial ~pred exhaust_gadt loc casel pss +let do_check_partial_gadt ?partial_match_warning_hint pred loc casel pss = + do_check_partial ?partial_match_warning_hint ~pred exhaust_gadt loc casel pss (*****************) (* Fragile check *) @@ -2265,9 +2270,9 @@ let check_partial_param do_check_partial do_check_fragile loc casel = do_check_partial_normal do_check_fragile_normal*) -let check_partial_gadt pred loc casel = +let check_partial_gadt ?partial_match_warning_hint pred loc casel = check_partial_param - (do_check_partial_gadt pred) + (do_check_partial_gadt ?partial_match_warning_hint pred) do_check_fragile_gadt loc casel (*************************************) diff --git a/compiler/ml/parmatch.mli b/compiler/ml/parmatch.mli index 201abd05ff..517206a600 100644 --- a/compiler/ml/parmatch.mli +++ b/compiler/ml/parmatch.mli @@ -69,6 +69,7 @@ val ppat_of_type : val pressure_variants : Env.t -> pattern list -> unit val check_partial_gadt : + ?partial_match_warning_hint:string -> ((string, constructor_description) Hashtbl.t -> (string, label_description) Hashtbl.t -> Parsetree.pattern -> diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 3ed7e59178..6015d49175 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -1722,13 +1722,14 @@ let partial_pred ~lev ?mode ?explode env expected_ty constrs labels p = set_state state env; None -let check_partial ?(lev = get_current_level ()) env expected_ty loc cases = +let check_partial ?(lev = get_current_level ()) ?partial_match_warning_hint env + expected_ty loc cases = let explode = match cases with | [_] -> 5 | _ -> 0 in - Parmatch.check_partial_gadt + Parmatch.check_partial_gadt ?partial_match_warning_hint (partial_pred ~lev ~explode env expected_ty) loc cases @@ -4202,7 +4203,24 @@ and type_let ~context ?(check = fun s -> Warnings.Unused_var s) List.iter2 (fun pat (attrs, exp) -> Builtin_attributes.warning_scope ~ppwarning:false attrs (fun () -> - ignore (check_partial env pat.pat_type pat.pat_loc [case pat exp]))) + let partial_match_warning_hint = + if Experimental_features.is_enabled Experimental_features.LetUnwrap + then + let ty = repr (Ctype.expand_head env pat.pat_type) in + match ty.desc with + | Tconstr (path, _, _) + when Path.same path Predef.path_option + || Path.same path Predef.path_result -> + Some + "Hint: You can use `let?` to automatically unwrap this \ + expression." + | _ -> None + else None + in + ignore + (check_partial ?partial_match_warning_hint env pat.pat_type + pat.pat_loc + [case pat exp]))) pat_list (List.map2 (fun (attrs, _) e -> (attrs, e)) spatl exp_list); end_def (); diff --git a/compiler/ml/typecore.mli b/compiler/ml/typecore.mli index 2a35693ec2..3e3a7c0601 100644 --- a/compiler/ml/typecore.mli +++ b/compiler/ml/typecore.mli @@ -35,6 +35,7 @@ val type_expression : Typedtree.expression val check_partial : ?lev:int -> + ?partial_match_warning_hint:string -> Env.t -> type_expr -> Location.t -> diff --git a/tests/build_tests/super_errors/expected/let_without_unwrap_but_elgible.res.expected b/tests/build_tests/super_errors/expected/let_without_unwrap_but_elgible.res.expected new file mode 100644 index 0000000000..c12f629da5 --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_without_unwrap_but_elgible.res.expected @@ -0,0 +1,14 @@ + + Warning number 8 + /.../fixtures/let_without_unwrap_but_elgible.res:6:7-11 + + 4 │ + 5 │ let f = { + 6 │ let Ok(_) = x + 7 │ Ok(1) + 8 │ } + + You forgot to handle a possible case here, for example: + | Error(_) + + Hint: You can use `let?` to automatically unwrap this expression. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/let_without_unwrap_but_elgible.res b/tests/build_tests/super_errors/fixtures/let_without_unwrap_but_elgible.res new file mode 100644 index 0000000000..bc41822c7b --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_without_unwrap_but_elgible.res @@ -0,0 +1,8 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + +let x = Ok(1) + +let f = { + let Ok(_) = x + Ok(1) +} From 89ee32a9fc75c5aa0c00335f5efb3ab3376477e3 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 12:12:20 +0200 Subject: [PATCH 16/21] fix --- rewatch/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index cb9df97d67..04b7a250dc 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -249,7 +249,7 @@ impl<'de> serde::Deserialize<'de> for ExperimentalFeature { match v { "LetUnwrap" => Ok(ExperimentalFeature::LetUnwrap), other => { - let available = ExperimentalFeature::all_names().join(", "); + let available = vec!["LetUnwrap"].join(", "); Err(DeError::custom(format!( "Unknown experimental feature '{}'. Available features: {}", other, available From af9c0a4bf74171a6344da61590735af34be64df3 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 12:15:29 +0200 Subject: [PATCH 17/21] add another error message test --- .../let_unwrap_return_type_mismatch.res.expected | 15 +++++++++++++++ .../fixtures/let_unwrap_return_type_mismatch.res | 6 ++++++ 2 files changed, 21 insertions(+) create mode 100644 tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res diff --git a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected new file mode 100644 index 0000000000..695e169950 --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected @@ -0,0 +1,15 @@ + + We've found a bug for you! + /.../fixtures/let_unwrap_return_type_mismatch.res:4:8-14 + + 2 │ + 3 │ let fn = (): int => { + 4 │ let? Some(x) = None + 5 │ 42 + 6 │ } + + This has type: option<'a> + But this switch is expected to return: int + + All branches in a switch must return the same type. + To fix this, change your branch to return the expected type. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res new file mode 100644 index 0000000000..61b154bd2d --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res @@ -0,0 +1,6 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + +let fn = (): int => { + let? Some(x) = None + 42 +} From 1080871a91e5c96df90de5e93e446ee241de124d Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 12:16:52 +0200 Subject: [PATCH 18/21] fix lint --- rewatch/src/config.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 04b7a250dc..21fd542c17 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -249,7 +249,7 @@ impl<'de> serde::Deserialize<'de> for ExperimentalFeature { match v { "LetUnwrap" => Ok(ExperimentalFeature::LetUnwrap), other => { - let available = vec!["LetUnwrap"].join(", "); + let available = ["LetUnwrap"].join(", "); Err(DeError::custom(format!( "Unknown experimental feature '{}'. Available features: {}", other, available From 6ddf3209a6bcfe1ea155a19bdfd0980313c97991 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 12:33:36 +0200 Subject: [PATCH 19/21] try to improve error message --- compiler/ml/error_message_utils.ml | 10 ++++++++++ compiler/ml/typecore.ml | 19 +++++++++++++++---- ...t_unwrap_return_type_mismatch.res.expected | 17 ++++++++--------- .../let_unwrap_return_type_mismatch.res | 2 ++ 4 files changed, 35 insertions(+), 13 deletions(-) diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml index c725329570..bccba4793f 100644 --- a/compiler/ml/error_message_utils.ml +++ b/compiler/ml/error_message_utils.ml @@ -96,6 +96,7 @@ type type_clash_context = | IfReturn | TernaryReturn | SwitchReturn + | LetUnwrapReturn | TryReturn | StringConcat | ComparisonOperator @@ -131,6 +132,7 @@ let context_to_string = function | Some TernaryReturn -> "TernaryReturn" | Some Await -> "Await" | Some BracedIdent -> "BracedIdent" + | Some LetUnwrapReturn -> "LetUnwrapReturn" | None -> "None" let fprintf = Format.fprintf @@ -163,6 +165,9 @@ let error_expected_type_text ppf type_clash_context = | Some ComparisonOperator -> fprintf ppf "But it's being compared to something of type:" | Some SwitchReturn -> fprintf ppf "But this switch is expected to return:" + | Some LetUnwrapReturn -> + fprintf ppf + "But this @{let?@} is used in a context expecting the type:" | Some TryReturn -> fprintf ppf "But this try/catch is expected to return:" | Some WhileCondition -> fprintf ppf "But a @{while@} loop condition must always be of type:" @@ -314,6 +319,11 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf "\n\n\ \ All branches in a @{switch@} must return the same type.@,\ To fix this, change your branch to return the expected type." + | Some LetUnwrapReturn, _ -> + fprintf ppf + "\n\n\ + \ @{let?@} can only be used in a context that expects \ + @{option@} or @{result@}." | Some TryReturn, _ -> fprintf ppf "\n\n\ diff --git a/compiler/ml/typecore.ml b/compiler/ml/typecore.ml index 6015d49175..ebcd7bb210 100644 --- a/compiler/ml/typecore.ml +++ b/compiler/ml/typecore.ml @@ -2486,12 +2486,21 @@ and type_expect_ ~context ?in_function ?(recarg = Rejected) env sexp ty_expected (* Note: val_caselist = [] and exn_caselist = [], i.e. a fully empty pattern matching can be generated by Camlp4 with its revised syntax. Let's accept it for backward compatibility. *) + let call_context = + if + Ext_list.exists sexp.pexp_attributes (fun ({txt}, _) -> + match txt with + | "let.unwrap" -> true + | _ -> false) + then `LetUnwrap + else `Switch + in let val_cases, partial = - type_cases ~call_context:`Switch env arg.exp_type ty_expected true loc + type_cases ~call_context env arg.exp_type ty_expected true loc val_caselist in let exn_cases, _ = - type_cases ~call_context:`Switch env Predef.type_exn ty_expected false loc + type_cases ~call_context env Predef.type_exn ty_expected false loc exn_caselist in re @@ -3887,8 +3896,9 @@ and type_statement ~context env sexp = (* Typing of match cases *) -and type_cases ~(call_context : [`Switch | `Function | `Try]) ?in_function env - ty_arg ty_res partial_flag loc caselist : _ * Typedtree.partial = +and type_cases ~(call_context : [`LetUnwrap | `Switch | `Function | `Try]) + ?in_function env ty_arg ty_res partial_flag loc caselist : + _ * Typedtree.partial = (* ty_arg is _fully_ generalized *) let patterns = List.map (fun {pc_lhs = p} -> p) caselist in let contains_polyvars = List.exists contains_polymorphic_variant patterns in @@ -4006,6 +4016,7 @@ and type_cases ~(call_context : [`Switch | `Function | `Try]) ?in_function env (match call_context with | `Switch -> Some SwitchReturn | `Try -> Some TryReturn + | `LetUnwrap -> Some LetUnwrapReturn | `Function -> None) ?in_function ext_env sexp ty_res' in diff --git a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected index 695e169950..084a272924 100644 --- a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected +++ b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected @@ -1,15 +1,14 @@ We've found a bug for you! - /.../fixtures/let_unwrap_return_type_mismatch.res:4:8-14 + /.../fixtures/let_unwrap_return_type_mismatch.res:6:8-14 - 2 │ - 3 │ let fn = (): int => { - 4 │ let? Some(x) = None - 5 │ 42 - 6 │ } + 4 │ + 5 │ let fn = (): int => { + 6 │ let? Some(x) = None + 7 │ 42 + 8 │ } This has type: option<'a> - But this switch is expected to return: int + But this let? is used in a context expecting the type: int - All branches in a switch must return the same type. - To fix this, change your branch to return the expected type. \ No newline at end of file + let? can only be used in a context that expects option or result. \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res index 61b154bd2d..b45cc90ebc 100644 --- a/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch.res @@ -1,5 +1,7 @@ @@config({flags: ["-enable-experimental", "LetUnwrap"]}) +let x = Some(1) + let fn = (): int => { let? Some(x) = None 42 From 15b185579cb881644956bd28c202349c0d41625b Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Sat, 23 Aug 2025 20:40:50 +0200 Subject: [PATCH 20/21] more work on error messages --- compiler/ml/error_message_utils.ml | 67 +++++++++++++++++-- ...t_unwrap_return_type_mismatch.res.expected | 9 ++- ...ap_return_type_mismatch_block.res.expected | 19 ++++++ ...p_return_type_mismatch_result.res.expected | 19 ++++++ .../let_unwrap_return_type_mismatch_block.res | 15 +++++ ...let_unwrap_return_type_mismatch_result.res | 8 +++ 6 files changed, 128 insertions(+), 9 deletions(-) create mode 100644 tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_block.res.expected create mode 100644 tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_result.res.expected create mode 100644 tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_block.res create mode 100644 tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_result.res diff --git a/compiler/ml/error_message_utils.ml b/compiler/ml/error_message_utils.ml index bccba4793f..1805844fd9 100644 --- a/compiler/ml/error_message_utils.ml +++ b/compiler/ml/error_message_utils.ml @@ -166,8 +166,7 @@ let error_expected_type_text ppf type_clash_context = fprintf ppf "But it's being compared to something of type:" | Some SwitchReturn -> fprintf ppf "But this switch is expected to return:" | Some LetUnwrapReturn -> - fprintf ppf - "But this @{let?@} is used in a context expecting the type:" + fprintf ppf "But this @{let?@} is used where this type is expected:" | Some TryReturn -> fprintf ppf "But this try/catch is expected to return:" | Some WhileCondition -> fprintf ppf "But a @{while@} loop condition must always be of type:" @@ -319,11 +318,65 @@ let print_extra_type_clash_help ~extract_concrete_typedecl ~env loc ppf "\n\n\ \ All branches in a @{switch@} must return the same type.@,\ To fix this, change your branch to return the expected type." - | Some LetUnwrapReturn, _ -> - fprintf ppf - "\n\n\ - \ @{let?@} can only be used in a context that expects \ - @{option@} or @{result@}." + | Some LetUnwrapReturn, bottom_aliases -> ( + let kind = + match bottom_aliases with + | Some ({Types.desc = Tconstr (p, _, _)}, _) + when Path.same p Predef.path_option -> + `Option + | Some (_, {Types.desc = Tconstr (p, _, _)}) + when Path.same p Predef.path_option -> + `Option + | Some ({Types.desc = Tconstr (p, _, _)}, _) + when Path.same p Predef.path_result -> + `Result + | Some (_, {Types.desc = Tconstr (p, _, _)}) + when Path.same p Predef.path_result -> + `Result + | _ -> `Unknown + in + match kind with + | `Option -> + fprintf ppf + "\n\n\ + \ This @{let?@} unwraps an @{option@}; use it where the \ + enclosing function or let binding returns an @{option@} so \ + @{None@} can propagate.\n\n\ + \ Possible solutions:\n\ + \ - Change the enclosing function or let binding to return \ + @{option<'t>@} and use @{Some@} for success; \ + @{let?@} will propagate @{None@}.\n\ + \ - Replace @{let?@} with a @{switch@} and handle the \ + @{None@} case explicitly.\n\ + \ - If you want a default value instead of early return, unwrap using \ + @{Option.getOr(default)@}." + | `Result -> + fprintf ppf + "\n\n\ + \ This @{let?@} unwraps a @{result@}; use it where the \ + enclosing function or let binding returns a @{result@} so \ + @{Error@} can propagate.\n\n\ + \ Possible solutions:\n\ + \ - Change the enclosing function or let binding to return \ + @{result<'ok, 'error>@}; use @{Ok@} for success, and \ + @{let?@} will propagate @{Error@}.\n\ + \ - Replace @{let?@} with a @{switch@} and handle the \ + @{Error@} case explicitly.\n\ + \ - If you want a default value instead of early return, unwrap using \ + @{Result.getOr(default)@}." + | `Unknown -> + fprintf ppf + "\n\n\ + \ @{let?@} can only be used in a context that expects \ + @{option@} or @{result@}.\n\n\ + \ Possible solutions:\n\ + \ - Change the enclosing function or let binding to return an \ + @{option<'t>@} or @{result<'ok, 'error>@} and propagate \ + with @{Some/Ok@}.\n\ + \ - Replace @{let?@} with a @{switch@} and handle the \ + @{None/Error@} case explicitly.\n\ + \ - If you want a default value instead of early return, unwrap using \ + @{Option.getOr(default)@} or @{Result.getOr(default)@}.") | Some TryReturn, _ -> fprintf ppf "\n\n\ diff --git a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected index 084a272924..e401d27fa9 100644 --- a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected +++ b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch.res.expected @@ -9,6 +9,11 @@ 8 │ } This has type: option<'a> - But this let? is used in a context expecting the type: int + But this let? is used where this type is expected: int - let? can only be used in a context that expects option or result. \ No newline at end of file + This let? unwraps an option; use it where the enclosing function or let binding returns an option so None can propagate. + + Possible solutions: + - Change the enclosing function or let binding to return option<'t> and use Some for success; let? will propagate None. + - Replace let? with a switch and handle the None case explicitly. + - If you want a default value instead of early return, unwrap using Option.getOr(default). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_block.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_block.res.expected new file mode 100644 index 0000000000..f5e925eda7 --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_block.res.expected @@ -0,0 +1,19 @@ + + We've found a bug for you! + /.../fixtures/let_unwrap_return_type_mismatch_block.res:10:12-18 + + 8 ┆ 1 + 9 ┆ } else { + 10 ┆ let? Some(x) = None + 11 ┆ Some(x) + 12 ┆ } + + This has type: option<'a> + But this let? is used where this type is expected: int + + This let? unwraps an option; use it where the enclosing function or let binding returns an option so None can propagate. + + Possible solutions: + - Change the enclosing function or let binding to return option<'t> and use Some for success; let? will propagate None. + - Replace let? with a switch and handle the None case explicitly. + - If you want a default value instead of early return, unwrap using Option.getOr(default). \ No newline at end of file diff --git a/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_result.res.expected b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_result.res.expected new file mode 100644 index 0000000000..5d43ade0bc --- /dev/null +++ b/tests/build_tests/super_errors/expected/let_unwrap_return_type_mismatch_result.res.expected @@ -0,0 +1,19 @@ + + We've found a bug for you! + /.../fixtures/let_unwrap_return_type_mismatch_result.res:6:8-12 + + 4 │ + 5 │ let fn = (): int => { + 6 │ let? Ok(v) = Error("fail") + 7 │ 42 + 8 │ } + + This has type: result<'a, string> + But this let? is used where this type is expected: int + + This let? unwraps a result; use it where the enclosing function or let binding returns a result so Error can propagate. + + Possible solutions: + - Change the enclosing function or let binding to return result<'ok, 'error>; use Ok for success, and let? will propagate Error. + - Replace let? with a switch and handle the Error case explicitly. + - If you want a default value instead of early return, unwrap using Result.getOr(default). \ No newline at end of file diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_block.res b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_block.res new file mode 100644 index 0000000000..a0aa1e69a1 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_block.res @@ -0,0 +1,15 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + +let x = Some(1) + +let fn = (): int => { + let x = { + if 1 > 2 { + 1 + } else { + let? Some(x) = None + Some(x) + } + } + 42 +} diff --git a/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_result.res b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_result.res new file mode 100644 index 0000000000..a91699e3c1 --- /dev/null +++ b/tests/build_tests/super_errors/fixtures/let_unwrap_return_type_mismatch_result.res @@ -0,0 +1,8 @@ +@@config({flags: ["-enable-experimental", "LetUnwrap"]}) + +let x = Ok(1) + +let fn = (): int => { + let? Ok(v) = Error("fail") + 42 +} From 538646685aa1f366074fb9a1788f322a115809b3 Mon Sep 17 00:00:00 2001 From: Gabriel Nordeborn Date: Wed, 27 Aug 2025 16:44:45 +0200 Subject: [PATCH 21/21] changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38785d6af9..af0a73e09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ #### :rocket: New Feature - Add support for ArrayBuffer and typed arrays to `@unboxed`. https://github.com/rescript-lang/rescript/pull/7788 +- Experimental: Add `let?` syntax for unwrapping and propagating errors/none as early returns for option/result types. https://github.com/rescript-lang/rescript/pull/7582 +- Add support for shipping features as experimental, including configuring what experimental features are enabled in `rescript.json`. https://github.com/rescript-lang/rescript/pull/7582 #### :bug: Bug fix