Skip to content

Commit

Permalink
feat: expression lifting (#3213)
Browse files Browse the repository at this point in the history
Refactor the lifting system to work on expressions instead of references and merge lifting and capturing into a unified model.

*Lifting* is the concept of referencing a preflight expression from an inflight scope, while *capturing* is the concept of referencing an object defined in a parent scope (albeit not another phase).

The mechanism works like this: we now have another "lifting" phase which runs immediately after type checking. This phase does the following:

1. **Lifting**: for inflight methods, it scans all the expressions. If it finds an expression marked as a preflight expression, it uses the jsifier to render it's preflight code and obtains a *token* from a lifts registry that's maintained at the class *type* level.

2. **Capturing**: for each expression that is a reference (both type refs and object refs), it checks looks up in which symbol environment it is defined. It then checks if that environment is a parent of the current method environment. If it is, is adds it to the lifts registry as well.

For each class, the lifting phase returns a new class object with the lift registry updated. So `class.lifts` has all the information about lifting and capturing for this class, later to be used downstream.

Now we move to the jsification phase. For each expression, the jsifier checks the `lifts` registry and if there's a token for this expression (by id), it emits the token instead (only in inflight of course).

The token registry is also consulted when the class machinery is jsified in order to emit the code that lifts expressions from preflight to inflight (both at the object level and at the type level) and binds them for access management.

When jsifying inflight base classes, we need to resolve the base class type and extract the lifting tokens from it.

## Misc

* The simulator failed to start certain test apps due to unresolvable attributes. This is because there was no mechanism to handle implicit dependencies on startup. A popular solution to this problem is to use an *init queue*. We basically pop a resource from the queue and try to start it. If we can't resolve a reference to other resources, we retry by returning it to the queue. An attempt count is decremented to avoid an infinite loop in case of a dependency cycle or unresolvable attribute (cc @Chriscbr).
* Additionally, the simulator did not call `cleanup()` during the shutdown sequence. This resulted in leaking ports in the `cloud.Api` resource that prevented the program from exiting (not exactly sure how this have worked). @tsuf239, any idea?


## Fixes

* #1878 is fixed because now we are able to lift complete expressions. So `bucket.bucketArn` is a simple string and there is no need to lift the bucket.
* #2729 is fixed with the following error: `Unable to reassign a captured variable`
* #3253

This is the 3rd attempt at this refactor, and hopefully the last for now. It supersedes #3125 and #2909.

## Follow ups

* #3244
* #3249
* #3189

## Checklist

- [x] Title matches [Winglang's style guide](https://docs.winglang.io/contributing/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)
- [x] 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 [Monada Contribution License](https://docs.winglang.io/terms-and-policies/contribution-license.html)*.
  • Loading branch information
eladb authored Jul 6, 2023
1 parent 8042879 commit 184aa8a
Show file tree
Hide file tree
Showing 326 changed files with 22,161 additions and 7,224 deletions.
29 changes: 20 additions & 9 deletions examples/tests/invalid/capture_redefinition.w
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
let y = "hello";
let y = "h";

inflight () => {
() => {
log("hello");
log(y);
// ^ Cannot capture symbol "y" because it is shadowed by another symbol with the same name
let y = "world";
let y = "y";
};

inflight () => {
let y = "hi";

inflight () => {
log(y);
// ^ Cannot capture symbol "y" because it is shadowed by another symbol with the same name
let y = "world";
};
};

// TODO: https://github.com/winglang/wing/issues/2753
// let x = "hi";
// if true {
// log(x);
// let x = "world";
// }
let x = "hi";
if true {
log(x);
// ^ Cannot capture symbol "x" because it is shadowed by another symbol with the same name
let x = "world";
}
4 changes: 2 additions & 2 deletions examples/tests/invalid/inflight_ref_explicit_ops.w
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class Another {

inflight inflightReturnsResource(): cloud.Queue {
return this.myQueue;
// ^^^^^^^^ Cannot qualify which operations are performed on resource
// ^^^^^^^^ Cannot qualify access to a lifted object
}
}

Expand All @@ -32,7 +32,7 @@ class Test {

inflight test1() {
let x = this.b;
// ^ Cannot qualify which operations are performed on resource
// ^ Cannot qualify access to a lifted object
x.put("hello", "world");
assert(this.justStr == "hello");
this.justBucket.put("hello", "world");
Expand Down
4 changes: 2 additions & 2 deletions examples/tests/invalid/inflight_ref_resource_sub_method.w
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ class Another {

inflight inflightReturnsResource(): cloud.Queue {
return this.myQueue;
// ^^^^^^^^ Cannot qualify which operations are performed on class "this.myQueue"
// ^^^^^^^^ Cannot qualify access to a lifted object
}

inflight inflightReturnsResource2(): cloud.Queue {
return globalQueue;
// ^^^^^^^^^^^^ Cannot qualify which operations are performed on class "globalQueue"
// ^^^^^^^^^^^^ Cannot qualify access to a lifted object
}
}

Expand Down
10 changes: 0 additions & 10 deletions examples/tests/invalid/resource_captures.w
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,14 @@ bring cloud;

class Foo {
bucket: cloud.Bucket;
mutArray: MutArray<str>;
var reassignable: num;
collectionOfResources: Array<cloud.Bucket>;

init() {
this.bucket = new cloud.Bucket();
this.mutArray = MutArray<str>[];
this.mutArray.push("hello");
this.reassignable = 42;
this.collectionOfResources = Array<cloud.Bucket>[];
}

inflight test() {
log("${this.reassignable}");
// ^^^^^^^^^^^^^^^^^ Cannot capture reassignable field 'reassignable'
log(this.mutArray.at(0));
// ^^^^^^^^^ Unable to reference "this.mutArray" from inflight method "test" because type MutArray<str> is not capturable

let b = this.bucket;
// ^^^^^^^^^^^ Unable to qualify which operations are performed on 'this.bucket' of type 'Bucket'. This is not supported yet.
b.put("hello", "world");
Expand Down
3 changes: 2 additions & 1 deletion examples/tests/invalid/use_before_defined.w
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ log("${y}"); // Access y before it's defined
let y = "ho";

if true {
log("${x}"); // Access x before it's defined (even though it's defined in an outer scope)
log("${x}");
// ^ Symbol "x" used before being defined
}
let x = "hi";
33 changes: 33 additions & 0 deletions examples/tests/valid/call_static_of_myself.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class Foo {
static inflight foo(): num { return 1; }
static inflight bar(): num { return Foo.foo(); }

inflight callThis(): num {
return Foo.bar();
}

}

inflight class Bar {
static bar(): num { return 2; }

callThis(): num {
return Bar.bar();
}
}

let foo = new Foo();

test "test" {
class Zoo {
static zoo(): num { return 3; }
}

let bar = new Bar();

assert(Foo.foo() == 1);
assert(Bar.bar() == 2);
assert(Zoo.zoo() == 3);
assert(foo.callThis() == 1);
assert(bar.callThis() == 2);
}
2 changes: 0 additions & 2 deletions examples/tests/valid/calling_inflight_variants.w
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
bring cloud;

class Foo {
inflight1: inflight (): num;
init() {
Expand Down
16 changes: 16 additions & 0 deletions examples/tests/valid/closure_class.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class MyClosure {
inflight another(): str {
return "hello";
}

inflight handle(): num {
return 42;
}
}

let fn = new MyClosure();

test "test" {
assert(fn() == 42);
assert(fn.another() == "hello");
}
32 changes: 32 additions & 0 deletions examples/tests/valid/double_reference.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
bring cloud;

let initCount = new cloud.Counter();

class Foo {
inflight init() {
initCount.inc();
}

inflight method() { }
}

class Bar {
foo: Foo;
init() {
this.foo = new Foo();
}

inflight callFoo() {
this.foo.method();
}
}

let bar = new Bar();

test "hello" {
bar.callFoo();
bar.foo.method();

// TODO: https://github.com/winglang/wing/issues/3244
assert(initCount.peek() == /*1*/ 2);
}
13 changes: 6 additions & 7 deletions examples/tests/valid/inflight_class_inner_capture_mutable.w
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,16 @@ test "inner inflight class capture immutable" {
class Inner {
dang(): num {
y.push(2);

// TODO: this should be a compiler error (it doesn't)
// see https://github.com/winglang/wing/issues/2729
// since the inner class is defined within the same scope, it is actually possible to reassign
// `i` and all will be good with the world.
i = i + 1;

return y.at(0) + 10;
}
}

assert(new Inner().dang() == 11);
assert(y.at(1) == 2);

assert(i == 10); // we cannot reassign from within the inflight class above, so this should hold
}
assert(y.at(1) == 2);
assert(i == 11);
}
9 changes: 9 additions & 0 deletions examples/tests/valid/lift_expr_with_this.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class Foo {
value: str;
init() { this.value = "hello"; }
}

let foo_this = new Foo();
test "test" {
assert(foo_this.value == "hello");
}
7 changes: 7 additions & 0 deletions examples/tests/valid/lift_redefinition.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let y = "hello";

test "test" {
assert(y == "hello");
let y = "z";
assert(y == "z");
}
18 changes: 18 additions & 0 deletions examples/tests/valid/lift_this.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class Foo {
inflight x: num;
inflight init() {
this.x = 42;
}
inflight bar(): num {
return this.x;
}
inflight foo(): num {
return this.bar() / 2;
}
}

let f = new Foo();

test "test" {
assert(f.foo() == 21);
}
43 changes: 43 additions & 0 deletions examples/tests/valid/lift_via_closure.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
bring cloud;

let bucket2 = new cloud.Bucket();

let fn = inflight () => {
bucket2.put("hello", "world");
};

class MyClosure {
bucket: cloud.Bucket;

init() {
this.bucket = new cloud.Bucket();
}

inflight handle() {
log("handle called");
this.putFile();
}

inflight putFile() {
log("putFile called");
this.bucket.put("hello", "world");
}

inflight listFiles(): Array<str> {
bucket2.put("b2", "world");
return this.bucket.list();
}
}

let fn2 = new MyClosure();

test "call synthetic closure class as a function" {
fn();
}

test "call non-synthetic closure as a function" {
fn2();
assert(fn2.bucket.get("hello") == "world");
assert(fn2.listFiles().length == 1);
assert(bucket2.get("b2") == "world");
}
18 changes: 18 additions & 0 deletions examples/tests/valid/lift_via_closure_explicit.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
bring cloud;

class MyClosure {
q: cloud.Queue;
init() {
this.q = new cloud.Queue();
}

inflight handle() {
this.q.push("hello");
}
}

let fn = new MyClosure();

test "test" {
fn();
}
8 changes: 6 additions & 2 deletions examples/tests/valid/resource.w
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,11 @@ let res = new Bar("Arr", bucket, MyEnum.B);

test "test" {
let s = res.myMethod();
assert(s == "counter is: 101");
log(s);

// TODO: https://github.com/winglang/wing/issues/3244
assert(s == "counter is: 201"); // Supposed to be: assert(s == "counter is: 101");

assert(bucket.list().length == 1);
assert(res.foo.inflightField == 123);
res.testTypeAccess();
Expand Down Expand Up @@ -136,7 +140,7 @@ let bigOlPublisher = new BigPublisher();
test "dependency cycles" {
bigOlPublisher.publish("foo");
let count = bigOlPublisher.getObjectCount();
// assert(count == 2); TODO: This fails due to issue: https://github.com/winglang/wing/issues/2082
// assert(count == 2); // TODO: This fails due to issue: https://github.com/winglang/wing/issues/2082
}

// Scope and ID tests
Expand Down
2 changes: 1 addition & 1 deletion examples/tests/valid/shadowing.w
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let fn = inflight (): Array<str> => {
result.push(bar);
}

// since we are not attempting to capture "foo" before it ise defined in this scope, this should
// since we are not attempting to capture "foo" before it is defined in this scope, this should
// work.
let foo = "bang";
result.push(foo);
Expand Down
Loading

0 comments on commit 184aa8a

Please sign in to comment.