Skip to content

Commit

Permalink
feat(compiler): bang operator (#5556)
Browse files Browse the repository at this point in the history
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");})())
```
  • Loading branch information
meirdev authored Feb 7, 2024
1 parent f1f65b4 commit cbc950e
Show file tree
Hide file tree
Showing 12 changed files with 165 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/docs/03-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions examples/tests/invalid/optionals.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -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"

39 changes: 39 additions & 0 deletions examples/tests/valid/optionals.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -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<str>? => {
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! == "");
5 changes: 5 additions & 0 deletions libs/tree-sitter-wing/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const PREC = {
POWER: 140,
MEMBER: 150,
CALL: 160,
OPTIONAL_UNWRAP: 170,
};

module.exports = grammar({
Expand Down Expand Up @@ -339,6 +340,7 @@ module.exports = grammar({
$.struct_literal,
$.optional_test,
$.compiler_dbg_panic,
$.optional_unwrap,
),

// Primitives
Expand Down Expand Up @@ -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 = [
Expand Down
29 changes: 29 additions & 0 deletions libs/tree-sitter-wing/test/corpus/expressions.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
================================================================================
Expand Down
1 change: 1 addition & 0 deletions libs/wingc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,7 @@ pub enum UnaryOperator {
Minus,
Not,
OptionalTest,
OptionalUnwrap,
}

#[derive(Debug)]
Expand Down
3 changes: 3 additions & 0 deletions libs/wingc/src/jsify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 } => {
Expand Down
10 changes: 10 additions & 0 deletions libs/wingc/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!();
Expand Down
9 changes: 9 additions & 0 deletions libs/wingc/src/type_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 7 additions & 0 deletions libs/wingsdk/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,10 @@ export function nodeof(construct: Construct): Node {
export function normalPath(path: string): string {
return path.replace(/\\+/g, "/");
}

export function unwrap<T>(value: T): T | never {
if (value != null) {
return value;
}
throw new Error("Unexpected nil");
}
14 changes: 14 additions & 0 deletions tools/hangar/__snapshots__/invalid.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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});
Expand Down

0 comments on commit cbc950e

Please sign in to comment.