Skip to content

support let unwrap syntax (let?) #7586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions compiler/syntax/src/res_core.ml
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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`"
Expand Down Expand Up @@ -2518,21 +2524,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)
Expand Down Expand Up @@ -3275,8 +3295,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
Expand Down Expand Up @@ -3447,7 +3469,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
Expand Down Expand Up @@ -5787,8 +5809,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)
Expand Down Expand Up @@ -6417,7 +6441,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;
Expand Down Expand Up @@ -6617,7 +6645,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;
Expand Down
8 changes: 4 additions & 4 deletions compiler/syntax/src/res_grammar.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
15 changes: 12 additions & 3 deletions compiler/syntax/src/res_printer.ml
Original file line number Diff line number Diff line change
Expand Up @@ -2078,11 +2078,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
| {
Expand Down
4 changes: 4 additions & 0 deletions compiler/syntax/src/res_scanner.ml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,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 =
Expand Down
10 changes: 6 additions & 4 deletions compiler/syntax/src/res_token.ml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type t =
| DotDotDot
| Bang
| Semicolon
| Let
| Let of {unwrap: bool}
| And
| Rec
| Underscore
Expand Down Expand Up @@ -134,7 +134,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 -> "_"
Expand Down Expand Up @@ -233,7 +234,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
Expand All @@ -253,7 +255,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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let? rec Some(baz) = someOption
and Some(bar) = baz
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let? foo: string
Original file line number Diff line number Diff line change
@@ -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 ]
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions tests/syntax_tests/data/printer/expr/letUnwrap.res
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion tests/syntax_tests/res_test.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 () =
Expand Down
12 changes: 6 additions & 6 deletions tests/tests/src/LetUnwrap.res
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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)
}

Expand All @@ -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)
}