From cbc950e451317fe3f529de60d9b164f7c590201b Mon Sep 17 00:00:00 2001 From: Meir Date: Wed, 7 Feb 2024 22:01:29 +0200 Subject: [PATCH] feat(compiler): bang operator (#5556) I added the bang operator as described in #4923. `throw` is a statement, so `x!` cannot be compiled to: `x ?? throw 'nil'`, It is also not enough to use block: `x ?? { throw 'nil' }` (syntax error). The only way is to use a function: `x ?? (() => throw 'nil')()`, and that's how I implemented it. More precisely, `x!` becoming: ``` (x??(()=>{throw new Error("Unexpected nil");})()) ``` --- docs/docs/03-language-reference.md | 1 + examples/tests/invalid/optionals.test.w | 13 +++++++ examples/tests/valid/optionals.test.w | 39 +++++++++++++++++++ libs/tree-sitter-wing/grammar.js | 5 +++ .../test/corpus/expressions.txt | 29 ++++++++++++++ libs/wingc/src/ast.rs | 1 + libs/wingc/src/jsify.rs | 3 ++ libs/wingc/src/parser.rs | 10 +++++ libs/wingc/src/type_check.rs | 9 +++++ libs/wingsdk/src/helpers.ts | 7 ++++ tools/hangar/__snapshots__/invalid.ts.snap | 14 +++++++ .../valid/optionals.test.w_compile_tf-aws.md | 34 ++++++++++++++++ 12 files changed, 165 insertions(+) diff --git a/docs/docs/03-language-reference.md b/docs/docs/03-language-reference.md index 4a1f5c5f37b..b7c5a7475e5 100644 --- a/docs/docs/03-language-reference.md +++ b/docs/docs/03-language-reference.md @@ -808,6 +808,7 @@ Here's a quick summary of how optionality works in Wing: otherwise. * `if let y = x { } else { }` is a special control flow statement which binds `y` inside the first block only if `x` has a value. Otherwise, the `else` block will be executed. +* The `x!` notation will return the value in `x` if there is one, otherwise it will throw an error. * The `x?.y?.z` notation can be used to access fields only if they have a value. The type of this expression is `Z?` (an optional based on the type of the last component). * The `x ?? y` notation will return the value in `x` if there is one, `y` otherwise. diff --git a/examples/tests/invalid/optionals.test.w b/examples/tests/invalid/optionals.test.w index 762ce3de0a5..56bc1c3c521 100644 --- a/examples/tests/invalid/optionals.test.w +++ b/examples/tests/invalid/optionals.test.w @@ -103,3 +103,16 @@ let functionWithOptionalFuncParam1: ((num):void)? = (x: str):void => {}; // ^^^^^^^^^^^^^^^^^^^ Expected type to be "(preflight (num): void)?", but got "preflight (x: str): void" instead let functionWithOptionalFuncParam2: (():num)? = ():str => { return "s"; }; // ^^^^^^^^^^^^^^^^^^^^^^^^^ Expected type to be "(preflight (): num)?", but got "preflight (): str" instead + +// Unwrap non-optional type + +let nonOptional: num = 10; +let unwrapValue = nonOptional!; +// ^^^^^^^^^^^ '!' expects an optional type, found "num" + +let nonOptionalFn = (): num => { + return 10; +}; +let unwrapValueFn = nonOptionalFn()!; +// ^^^^^^^^^^^^^^^ '!' expects an optional type, found "num" + diff --git a/examples/tests/valid/optionals.test.w b/examples/tests/valid/optionals.test.w index 82ab5942e09..511be164e9c 100644 --- a/examples/tests/valid/optionals.test.w +++ b/examples/tests/valid/optionals.test.w @@ -219,3 +219,42 @@ if let f = fn() { assert(true); } +let maybeVar: num? = 123; +assert(maybeVar! == 123); + +let maybeVarNull: str? = nil; +try { + let err = maybeVarNull!; + assert(false); +} catch e { + assert(e == "Unexpected nil"); +} + +let maybeFn = (b: bool): Array? => { + if b { + return ["hi"]; + } +}; +try { + maybeFn(false)!; + assert(false); +} catch e { + assert(e == "Unexpected nil"); +} +assert(maybeFn(true)! == ["hi"]); + +let maybeVarBool: bool? = true; +assert(!maybeVarBool! == false); + +struct Person { + name: str; + age: num; +} +let person = Person.tryParseJson(Json.stringify({"name": "john", "age": 30}))!; +assert(person.name == "john" && person.age == 30); + +let maybeX: num? = 0; +assert(maybeX! == 0); + +let maybeY: str? = ""; +assert(maybeY! == ""); diff --git a/libs/tree-sitter-wing/grammar.js b/libs/tree-sitter-wing/grammar.js index c938fc53959..2522bf16bde 100644 --- a/libs/tree-sitter-wing/grammar.js +++ b/libs/tree-sitter-wing/grammar.js @@ -15,6 +15,7 @@ const PREC = { POWER: 140, MEMBER: 150, CALL: 160, + OPTIONAL_UNWRAP: 170, }; module.exports = grammar({ @@ -339,6 +340,7 @@ module.exports = grammar({ $.struct_literal, $.optional_test, $.compiler_dbg_panic, + $.optional_unwrap, ), // Primitives @@ -544,6 +546,9 @@ module.exports = grammar({ _container_value_type: ($) => seq("<", field("type_parameter", $._type), ">"), + optional_unwrap: ($) => + prec.right(PREC.OPTIONAL_UNWRAP, seq($.expression, "!")), + unary_expression: ($) => { /** @type {Array<[RuleOrLiteral, number]>} */ const table = [ diff --git a/libs/tree-sitter-wing/test/corpus/expressions.txt b/libs/tree-sitter-wing/test/corpus/expressions.txt index ad03de7f7c1..5409688fe0a 100644 --- a/libs/tree-sitter-wing/test/corpus/expressions.txt +++ b/libs/tree-sitter-wing/test/corpus/expressions.txt @@ -322,6 +322,35 @@ maybeVal ?? 2 == 2; (number)) (number)))) +================================================================================ +Unwrap expression +================================================================================ + +maybeVal!; +tryGet()!; +!tryGetBool()!; + +-------------------------------------------------------------------------------- + +(source + (expression_statement + (optional_unwrap + (reference + (reference_identifier)))) + (expression_statement + (optional_unwrap + (call + (reference + (reference_identifier)) + (argument_list)))) + (expression_statement + (unary_expression + (optional_unwrap + (call + (reference + (reference_identifier)) + (argument_list)))))) + ================================================================================ Set Literal ================================================================================ diff --git a/libs/wingc/src/ast.rs b/libs/wingc/src/ast.rs index c39aa67c644..124b6da5870 100644 --- a/libs/wingc/src/ast.rs +++ b/libs/wingc/src/ast.rs @@ -733,6 +733,7 @@ pub enum UnaryOperator { Minus, Not, OptionalTest, + OptionalUnwrap, } #[derive(Debug)] diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs index 60ef90f36c3..5a0296beb75 100644 --- a/libs/wingc/src/jsify.rs +++ b/libs/wingc/src/jsify.rs @@ -710,6 +710,9 @@ impl<'a> JSifier<'a> { // We use the abstract inequality operator here because we want to check for null or undefined new_code!(expr_span, "((", js_exp, ") != null)") } + UnaryOperator::OptionalUnwrap => { + new_code!(expr_span, "$helpers.unwrap(", js_exp, ")") + } } } ExprKind::Binary { op, left, right } => { diff --git a/libs/wingc/src/parser.rs b/libs/wingc/src/parser.rs index 4c29190fa19..444b851e513 100644 --- a/libs/wingc/src/parser.rs +++ b/libs/wingc/src/parser.rs @@ -2310,6 +2310,16 @@ impl<'s> Parser<'s> { expression_span, )) } + "optional_unwrap" => { + let expression = self.build_expression(&expression_node.named_child(0).unwrap(), phase); + Ok(Expr::new( + ExprKind::Unary { + op: UnaryOperator::OptionalUnwrap, + exp: Box::new(expression?), + }, + expression_span, + )) + } "compiler_dbg_panic" => { // Handle the debug panic expression (during parsing) dbg_panic!(); diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index fcd6fc5e013..153dc76b98c 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -2136,6 +2136,15 @@ impl<'a> TypeChecker<'a> { } (self.types.bool(), phase) } + UnaryOperator::OptionalUnwrap => { + if !type_.is_option() { + self.spanned_error(unary_exp, format!("'!' expects an optional type, found \"{}\"", type_)); + (type_, phase) + } else { + let inner_type = *type_.maybe_unwrap_option(); + (inner_type, phase) + } + } } } ExprKind::Range { diff --git a/libs/wingsdk/src/helpers.ts b/libs/wingsdk/src/helpers.ts index 664653ec490..a565c8d369e 100644 --- a/libs/wingsdk/src/helpers.ts +++ b/libs/wingsdk/src/helpers.ts @@ -45,3 +45,10 @@ export function nodeof(construct: Construct): Node { export function normalPath(path: string): string { return path.replace(/\\+/g, "/"); } + +export function unwrap(value: T): T | never { + if (value != null) { + return value; + } + throw new Error("Unexpected nil"); +} diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index 051b427c692..9e17e944ea9 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -2760,6 +2760,20 @@ error: Expected type to be \\"(preflight (): num)?\\", but got \\"preflight (): | ^^^^^^^^^^^^^^^^^^^^^^^^^ Expected type to be \\"(preflight (): num)?\\", but got \\"preflight (): str\\" instead +error: '!' expects an optional type, found \\"num\\" + --> ../../../examples/tests/invalid/optionals.test.w:110:19 + | +110 | let unwrapValue = nonOptional!; + | ^^^^^^^^^^^ '!' expects an optional type, found \\"num\\" + + +error: '!' expects an optional type, found \\"num\\" + --> ../../../examples/tests/invalid/optionals.test.w:116:21 + | +116 | let unwrapValueFn = nonOptionalFn()!; + | ^^^^^^^^^^^^^^^ '!' expects an optional type, found \\"num\\" + + error: Variable is not reassignable --> ../../../examples/tests/invalid/optionals.test.w:53:3 | diff --git a/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md index d8b69544777..5768f2d7c6e 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md @@ -127,6 +127,7 @@ const cloud = $stdlib.cloud; class $Root extends $stdlib.std.Resource { constructor($scope, $id) { super($scope, $id); + const Person = $stdlib.std.Struct._createJsonSchema({id:"/Person",type:"object",properties:{age:{type:"number"},name:{type:"string"},},required:["age","name",]}); class Super extends $stdlib.std.Resource { constructor($scope, $id, ) { super($scope, $id); @@ -495,6 +496,39 @@ class $Root extends $stdlib.std.Resource { $helpers.assert(true, "true"); } } + const maybeVar = 123; + $helpers.assert($helpers.eq($helpers.unwrap(maybeVar), 123), "maybeVar! == 123"); + const maybeVarNull = undefined; + try { + const err = $helpers.unwrap(maybeVarNull); + $helpers.assert(false, "false"); + } + catch ($error_e) { + const e = $error_e.message; + $helpers.assert($helpers.eq(e, "Unexpected nil"), "e == \"Unexpected nil\""); + } + const maybeFn = ((b) => { + if (b) { + return ["hi"]; + } + }); + try { + $helpers.unwrap((maybeFn(false))); + $helpers.assert(false, "false"); + } + catch ($error_e) { + const e = $error_e.message; + $helpers.assert($helpers.eq(e, "Unexpected nil"), "e == \"Unexpected nil\""); + } + $helpers.assert($helpers.eq($helpers.unwrap((maybeFn(true))), ["hi"]), "maybeFn(true)! == [\"hi\"]"); + const maybeVarBool = true; + $helpers.assert($helpers.eq((!$helpers.unwrap(maybeVarBool)), false), "!maybeVarBool! == false"); + const person = $helpers.unwrap(Person._tryParseJson(((json, opts) => { return JSON.stringify(json, null, opts?.indent) })(({"name": "john", "age": 30})))); + $helpers.assert(($helpers.eq(person.name, "john") && $helpers.eq(person.age, 30)), "person.name == \"john\" && person.age == 30"); + const maybeX = 0; + $helpers.assert($helpers.eq($helpers.unwrap(maybeX), 0), "maybeX! == 0"); + const maybeY = ""; + $helpers.assert($helpers.eq($helpers.unwrap(maybeY), ""), "maybeY! == \"\""); } } const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms});