Skip to content

Commit

Permalink
feat(compiler)!: explicit lift qualification statement (#6400)
Browse files Browse the repository at this point in the history
fixes #6080

Initial implementation of the explicit lift statement as defined in the [rfc](https://github.com/winglang/wing/blob/main/docs/contributing/999-rfcs/2024-03-14-explicit-lift-qualification.md).

```wing
let bucket = new cloud.Bucket();
inflight () => {
  let b = bucket; // Assing preflight object to inflight variable
  lift {bucket: put} {
    b.put("k","v"); // Within this block we can use the inflight variable because of the explicit lifting above
  }
  b.put("k","v"); // This is an error, because it's outside the explicit lift block and we can't figure out who `b` is
  
  // We can also define multiple lifts in a block and multiple ops per lift
  lift {bucket1: [put, get], bucket2: delete} { ... }
  
  // We can nest lift blocks
  lift {bucket1: put} {
    ..
    lift {bucket2: get} {
      ..
    }
  }

  // Type checker validates passed methods are really part of the inflight interface of the object
  lift {bucket: leak} {}
              //^ err: leak isn't an inflight method on type Bucket
}
```

Breaking change: this is instead of the `lift()` builtin macro that was used for the same purpose. 

## Checklist

- [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted)
- [x] Description explains motivation and solution
- [x] Tests added (always)
- [x] Docs updated (only required for features)
- [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing

*By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
  • Loading branch information
yoav-steinberg authored May 19, 2024
1 parent ac61766 commit d9a9ddf
Show file tree
Hide file tree
Showing 28 changed files with 767 additions and 434 deletions.
16 changes: 12 additions & 4 deletions docs/docs/02-concepts/01-preflight-and-inflight.md
Original file line number Diff line number Diff line change
Expand Up @@ -308,24 +308,32 @@ new cloud.Function(inflight () => {
});
```

To explicitly qualify lifts in an inflight closure or inflight method and supress the above compiler error use the `lift()` utility function:
To explicitly qualify lifts in an inflight closure or inflight method and suppress the above compiler error, create a `lift` block:

```js playground
bring cloud;
let main_bucket = new cloud.Bucket() as "main";
let secondary_bucket = new cloud.Bucket() as "backup";
let use_main = true;
new cloud.Function(inflight () => {
lift(main_bucket, ["put"]); // Explicitly sate the "put" may be used on `main_bucket`
lift(secondary_bucket, ["put"]); // Explicitly sate the "put" may be used on `secondary_bucket`
let var b = main_bucket;
if !use_main {
b = secondary_bucket;
}
b.put("key", "value"); // Error is supressed and all possible values of `b` were explicitly qualified with "put"
// Explicitly state that methods named `put` may be used on `main_bucket` and `secondary_bucket`
lift {main_bucket: [put], secondary_bucket: [put]} {
// Error is supressed in this block and all possible values of `b` are explicitly qualified with `put`
b.put("key1", "value");
b.put("key2", "value");
}
});
```

Within the first clause of the `lift` block, a list of qualifications on preflight objects can be added.

Statements within a `lift` block are exempt from the compiler's analyzer that tries to determine preflight object usage automatically.
If an inflight method is directly or indirectly called within a `lift` block without sufficient resource qualifications, it may result in errors at runtime.

## Phase-independent code

The global functions `log`, `assert`, and `throw` can all be used in both preflight and inflight code.
Expand Down
44 changes: 19 additions & 25 deletions examples/tests/invalid/explicit_lift_qualification.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,42 @@ interface IPreflightInterface {
}
class PreflightClass impl IPreflightInterface {
pub inflight method(): void {}
pub static inflight static_method() {}

preflight_method() {
lift {bucket: [put]}{} // Lift statment in preflight method
}
}

let bucket = new cloud.Bucket();

let prelight_string = "hi";
let preflight_class = new PreflightClass();

// Lift statement in preflight global scope
lift {bucket: [put]}{}

class Foo {
pub inflight mehtod1() {
let b = bucket;
lift(b, ["put"]); // Explicit qualification with inflight object, lift call as non first statement
// ^ Expected a preflight object as first argument to `lift` builtin, found inflight expression instead
//^^^^^^^^^^^^^^^ lift() calls must be at the top of the method

lift(prelight_string, ["contains"]); // Explicit qualification on preflight non-class
// ^^^^^^^^^^^^^^^ Expected type to be "Resource", but got "str" instead
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lift() calls must be at the top of the method

let inflight_qualifier = "delete";
lift(bucket, [inflight_qualifier]); // Explicit qualification with inflight qualifiers, lift call as non first statement
// ^^^^^^^^^^^^^^^^^^^^ Qualification list must not contain any inflight elements
//^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lift() calls must be at the top of the method

let inner_closure = () => {
lift(bucket, ["get"]); // lift() call in inner closure
//^^^^^^^^^^^^^^^^^^^^ lift() calls are only allowed in inflight methods and closures defined in preflight
};
class Bar {
pub inflight method() {
lift(bucket, ["get"]); // lift() call in inner class
//^^^^^^^^^^^^^^^^^^^^ lift() calls are only allowed in inflight methods and closures defined in preflight
}
}
lift {b: [put]}{} // Explicit qualification with inflight object
lift {prelight_string: contains}{} // Explicit qualification on preflight non-class
lift {bucket: [shoot]}{} // Explicit qualification with unknown member
lift {bucket: shoot}{} // Explicit qualification with unknown, single method format
lift {not_bucket: put, not_bucket_again: get}{} // Explicit qualification with unknown objects
lift {bucket: addObject}{} // Explicit qualification with preflight method op
lift {preflight_class: static_method}{} // Explicit qualification with static method
}

pub inflight method2() {
let b = bucket;
b.put("k", "v"); // With no explicit qualification this should be an error
//^ Expression of type "Bucket" references an unknown preflight object

let b2 = bucket;
lift {bucket: put}{}
b2.put("k", "v"); // With explicit qualification, but outside of lift block, this should be an error

let i: IPreflightInterface = preflight_class;
i.method(); // With no explicit qualification this should be an error
//^ Expression of type "IPreflightInterface" references an unknown preflight object
}
}
72 changes: 47 additions & 25 deletions examples/tests/valid/explicit_lift_qualification.test.w
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
bring cloud;

let bucket = new cloud.Bucket();
bucket.addObject("k", "value");

let put_and_list = ["put", "list"];
let bucket1 = new cloud.Bucket() as "b1";
bucket1.addObject("k", "value");
let bucket2 = new cloud.Bucket() as "b2";
let bucket3 = new cloud.Bucket() as "b3";
let maybe_bucket: cloud.Bucket? = nil;

class Foo {
pub inflight mehtod() {
lift(bucket, put_and_list); // Qualify `bucket` with a preflight expression
lift(bucket, ["delete"]); // Qualify `bucket` with `delete` via literal
let b = bucket; // Assign `bucket` to an inflight variable

// `put` should work on `b` since we explicitly qualified `bucket` with `put`
// no error generated here because of use of `lift()` in this method
b.put("k2", "value2");

// validate `put` worked and that we can also `list`
assert(b.list() == ["k", "k2"]);

// Validate `delete` works
b.delete("k2");
assert(bucket.tryGet("k2") == nil);
// Qualify `bucket` with `delete`, `put` and `list` (multiple methods)
// Qualify another preflight object (bucket2) with `put` (test multile qualifications in single statement)
lift { bucket1: [delete, put, list], bucket2: [put] } {
let b1 = bucket1; // Assign `bucket` to an inflight variable

// `put` should work on `b1` since we explicitly qualified `bucket1` with `put`
// no error generated here because of use of `lift` in this method
b1.put("k2", "value2");

// validate `put` worked and that we can also `list`
assert(b1.list() == ["k", "k2"]);

// Validate `delete` works
b1.delete("k2");

// Try the other object
let b2 = bucket2;
b2.put("k2", "value2");

// Nest another `lift` block, this time with the single method format (no square brackets)
let b3 = bucket3;
lift { bucket3: put } {
b3.put("k3", "value3");
}
// use expression (unwrap or) to specify the object to lift
lift { maybe_bucket ?? bucket3: [list] } {
b3.list();
}
}

assert(bucket1.tryGet("k2") == nil);
assert(bucket2.get("k2") == "value2");
assert(bucket3.get("k3") == "value3");
}
}

Expand All @@ -32,10 +52,11 @@ test "explicit method lift qualification" {

// Similar to the above test, but using a closure
let inflight_closure = inflight () => {
lift(bucket, ["put"]);
let b = bucket;
b.put("k3", "value3"); // Use inflight expression to access explicitly qualified `bucket`
assert(bucket.get("k3") == "value3");
let b = bucket1;
lift {bucket1: [put]} {
b.put("k3", "value3"); // Use inflight expression to access explicitly qualified `bucket`
}
assert(bucket1.get("k3") == "value3");
};

test "explicit closure lift qualification" {
Expand All @@ -56,7 +77,8 @@ class PreflightClass impl PreflightInterface {
let bar = new PreflightClass();

test "explicit interface lift qualification" {
lift(bar, ["method"]);
let x: PreflightInterface = bar;
assert(x.method() == "ahoy there");
}
lift {bar: [method]} {
assert(x.method() == "ahoy there");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ let bar = inflight (): cloud.Bucket => {
};

test "test qualify closure returning a preflight object" {
lift(b, ["put"]);
// Call the inflight handler and then call a method on the result in a single satatement.
// Here we expect the `handle` method to qualify `bar`'s lift and no other qualfications.
bar().put("a", "value");
lift {b: put} {
// Call the inflight handler and then call a method on the result in a single satatement.
// Here we expect the `handle` method to qualify `bar`'s lift and no other qualfications.
bar().put("a", "value");
}
assert(b.get("a") == "value");
}
10 changes: 9 additions & 1 deletion libs/tree-sitter-wing/grammar.js
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,8 @@ module.exports = grammar({
$.try_catch_statement,
$.compiler_dbg_env,
$.super_constructor_statement,
$.throw_statement
$.throw_statement,
$.lift_statement,
),

import_statement: ($) =>
Expand Down Expand Up @@ -162,6 +163,13 @@ module.exports = grammar({
throw_statement: ($) =>
seq("throw", optional(field("expression", $.expression)), $._semicolon),

lift_statement: ($) =>
seq("lift", field("lift_qualifications", $.lift_qualifications), field("block", $.block)),

lift_qualifications: ($) => seq("{", field("qualification", commaSep1($.lift_qualification)), "}"),

lift_qualification: ($) => seq(field("obj", $.expression), ":", choice(field("ops", $.identifier), field("ops", seq("[", commaSep1($.identifier), "]")))),

assignment_operator: ($) => choice("=", "+=", "-="),

variable_assignment_statement: ($) =>
Expand Down
Loading

0 comments on commit d9a9ddf

Please sign in to comment.