Skip to content
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

rfc: unphased functions #1711

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
137 changes: 135 additions & 2 deletions docs/api/05-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,7 @@ For example (continuing the `Bucket` example above):

```TS
let bucket = new Bucket();

// OK! We are calling a preflight method from a preflight context
bucket.allowPublicAccess();
// ERROR: Cannot call into inflight phase while preflight
Expand All @@ -680,15 +681,117 @@ let handler2 = inflight() => {
}
```

Bridge between preflight and inflight is crossed with the help of immutable data
structures, "structs" (user definable and `Struct`), and the capture mechanism.
The bridge between preflight and inflight is crossed with the help of immutable data
structures, user-defined structs, resources, and the capture mechanism.

Preflight class methods and constructors can receive an inflight function as an argument. This
enables preflight classes to define code that will be executed on a cloud compute platform such as
lambda functions, docker, virtual machines etc.

[`▲ top`][top]

#### 1.3.1 Phase-independent code

> **Note**: Phase-independent functions are not yet implemented. Subscribe to [issue #435](https://github.com/winglang/wing/issues/435) for updates.

Code that is not dependent on the phase of execution can be designated as phase-independent using the `unphased` modifier.

Using this modifier means that the function can be called from either preflight or inflight contexts.

```TS
let odd_numbers = unphased (nums: Array<num>): Array<num> => {
let result = MutArray<num>[];
for num in nums {
if num % 2 == 1 {
result.push(num);
}
}
return result.copy();
};

// OK! We are calling an unphased function from a preflight context
let odds = odd_numbers([1, 2, 3]);

let handler = inflight () => {
// OK! We are calling an unphased function from an inflight context
let big_odds = odd_numbers([7, 8, 9]);
}
```

Phase-independent functions are useful for code that is useful across both
execution phases, such as for data manipulation, utility functions, etc.

Since phase-independent functions can be used inflight, they inherit the same restrictions as inflight functions, like not being able to call preflight functions or instantiate preflight classes.

But phase-independent functions can also be used preflight, so they inherit the same restrictions as preflight functions, like not being able to call inflight functions or instantiate inflight classes.

Phase-independent methods can be defined on resources:

```TS
class AwsBucket {
name: str; // preflight field

new() {
this.name = "my-bucket";
}

unphased object_url(key: str): str {
// This method references a preflight field (this.name) -- that is
// allowed in both phases so it is OK!
return `s3://${this.name}/${key}`;
}
}
```

Phase-independent methods take on the additional restriction that they
cannot mutate fields of the resource. For example, the following is disallowed:

```TS
class Bucket {
name: str; // preflight field

init() {
// initialize `name`
}

unphased set_name(name: str): void {
// ERROR: cannot mutate a preflight field from a phase-independent context
this.name = name;
}
}
```

An unphased function can be passed to a function that expects a preflight function or an inflight function. In this way, we can say that an unphased functions are a superset of both preflight and inflight functions.

However, a preflight or inflight function cannot be passed to a function that expects an unphased function.
An exception to this rule is that if a function is unphased, then we can automatically assume any functions passed to it or returned by it have a matching phase.

You can imagine that when a function is unphased, then "preflight" and "inflight" versions of it are generated at compile-time, and unphased-function types in parameters or return types are automatically converted to the appropriate phase.

For example, `Array<T>.map` is modeled like the following pseudocode:

```js
unphased map<U>(f: unphased (T) => U): Array<U> {
// ...
}
```

At compile-time, since the function is unphased, preflight and inflight versions are generated:

```js
preflight map<U>(f: preflight (T) => U): Array<U> {
// ...
}
inflight map<U>(f: inflight (T) => U): Array<U> {
// ...
}
```

Notice how the type of "f" is automatically converted to the appropriate phase. This is possible because "map" is unphased.
This way, when you call `Array<T>.map` with in preflight, it's possible to pass a preflight function to it, and when you call it in inflight, it's possible to pass an inflight function to it. (If you're calling `Array<T>.map` within another unphased function, then the unphased version of `Array<T>.map` is used.)

[`▲ top`][top]

---

### 1.4 Storage Modifiers
Expand Down Expand Up @@ -1983,6 +2086,36 @@ matching name (without any case conversion).

Extern methods do not support access to class's members through `this`, so they must be declared `static`.

If an extern function is declared as `unphased`, a preflight and inflight implementation must be provided, by providing two separate functions in the JavaScript module.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could also still accept a function, if both implementations are the same. I'm just trying to avoid clutter.

So it could be:

exports.randomHex = async function (length) {
    const buffer = Buffer.alloc(length);
    await randomFillAsync(buffer);
    return buffer.toString("hex");
}

but also:

exports.randomHex = {
  preflight: function (length) {
    const buffer = Buffer.alloc(length);
    crypto.randomFillSync(buffer);
    return buffer.toString("hex");
  },
  inflight: async function (length) {
    const buffer = Buffer.alloc(length);
    await randomFillAsync(buffer);
    return buffer.toString("hex");
  }

A simple check could be used to detect both cases: exports.randomHex instanceof Function ? "single implementation" : "dual implementation".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course, this assumes externs can only be functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds ok to me as an extra enhancement (I probably wouldn't implement it for the MVP but maybe later).

Note: if you only provide a single function, it will need to be sync (not async), otherwise it can't be safely called in preflight.

Copy link
Contributor Author

@Chriscbr Chriscbr Jul 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also FWIW possible to reuse a single implementation twice in the JavaScript (so I'm not sure it's worth slowing down performance by making it Wing's responsibility to check if externs are instanceof Function each time an extern is called).

function randomHex(length) {
  const buffer = Buffer.alloc(length);
  await randomFillAsync(buffer);
  return buffer.toString("hex");
}
exports.randomHex = { preflight: randomHex, inflight: randomHex };


This can be useful when the implementation of the function is different in preflight and inflight phases (for example, inflight functions are allowed to be async).

```TS
// main.w
class Util {
extern "./helper.js" static unphased randomHex(length: num): str;
}

// helper.js
const { promisify } = require("node:util");
const crypto = require("node:crypto");

const randomFillAsync = promisify(crypto.randomFill);

exports.randomHex = {
preflight: function (length) {
const buffer = Buffer.alloc(length);
crypto.randomFillSync(buffer);
return buffer.toString("hex");
},
inflight: async function (length) {
const buffer = Buffer.alloc(length);
await randomFillAsync(buffer);
return buffer.toString("hex");
}
}
```

### 5.2.1 Type model

The table below shows the mapping between Wing types and JavaScript values, shown with TypeScript types.
Expand Down
Loading