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

feat: external inflight typescript files #6305

Merged
merged 26 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b0ee810
use new intrinsic
MarkMcCulloh May 17, 2024
4be12df
Merge branch 'main' of https://github.com/winglang/wing into mark/int…
MarkMcCulloh May 20, 2024
99dd11b
wip
MarkMcCulloh May 21, 2024
236ebc0
wip
MarkMcCulloh May 21, 2024
678b08f
Merge branch 'main' of https://github.com/winglang/wing into mark/int…
MarkMcCulloh May 22, 2024
21fa66a
update
MarkMcCulloh May 22, 2024
2b77d57
wip
MarkMcCulloh May 24, 2024
0b870a2
Merge branch 'main' of https://github.com/winglang/wing into mark/int…
MarkMcCulloh May 24, 2024
7e33007
wip
MarkMcCulloh May 24, 2024
3885b87
wip
MarkMcCulloh May 24, 2024
5ee01d4
few more docs
MarkMcCulloh May 24, 2024
3276190
wip
MarkMcCulloh May 24, 2024
2bad677
add a couple error cases
MarkMcCulloh May 24, 2024
7856eee
move test
MarkMcCulloh May 24, 2024
5c09956
Merge branch 'main' into mark/intrinsic-inflight
MarkMcCulloh May 24, 2024
b33c4d5
wip
MarkMcCulloh May 26, 2024
a0b461d
Merge branch 'main' of https://github.com/winglang/wing into mark/int…
MarkMcCulloh May 26, 2024
36c9ee2
avoid updates to files if not needed
MarkMcCulloh May 26, 2024
f97e964
tweak docs
MarkMcCulloh May 26, 2024
40f9db0
possible windows fix
MarkMcCulloh May 26, 2024
8f33fbd
different relative path
MarkMcCulloh May 26, 2024
c98eec0
Merge branch 'main' into mark/intrinsic-inflight
monadabot May 26, 2024
20d82b6
chore: self mutation (e2e-2of2.diff)
monadabot May 26, 2024
eb54806
fix order to detect more errors
MarkMcCulloh May 27, 2024
f580a1c
Merge branch 'mark/intrinsic-inflight' of https://github.com/winglang…
MarkMcCulloh May 27, 2024
28bd04c
remove extra files
MarkMcCulloh May 27, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 72 additions & 1 deletion docs/docs/07-examples/10-using-javascript.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,78 @@ id: using-javascript
keywords: [example, javascript, extern, typescript, js, ts]
---

Calling a Javascript function from Wing requires two steps.
## Creating inflight function from JavaScript/TypeScript file

Inflight closures are an extremely important Wing functionality. Consider the following simple wing program:

```wing
// main.w
bring cloud;
let bucket = new cloud.Bucket();

bucket.onCreate(inflight (file) => {
log(file);
});
```

Being able to write inflight wing alongside the preflight code is beautiful, but you may want to write the inflight function in a separate file and *language*. The `@inflight` intrinsic function can be used to create an inflight closure from a JavaScript/TypeScript:

```wing
// main.w
bring cloud;
let bucket = new cloud.Bucket();

bucket.onCreate(@inflight("./bucket_create.ts"));
// ^ onCreate expects an `inflight (str): void` function, so the file must export a function with a typescript signature that matches
// ^ Relative (to current file) path to javascript or typescript file
// Note: This must be a static string
```

`wing compile` will generate `.bucket_create.inflight.ts` which will contain all of the information needed for TypeScript type checking and IDE support.
With that, you can create the `bucket_create.ts` file:

```ts
// bucket_create.ts
import inflight from "./.bucket_create.inflight";

export default inflight(async ({}, file) => {
// ^ This is known to be a string, the first positional argument needed for `onCreate`
console.log(file);
});
```

Something missing here is the ability to reference preflight resources inside an inflight function.
Let's create a Queue and pass it to the inflight function while exploring other options:

```wing
// main.w
bring cloud;
let bucket = new cloud.Bucket();
let queue = new cloud.Queue();

bucket.onCreate(@inflight("./bucket_create.ts",
export: "default",
// ^ Optional named export from the file, "default" is the default export
lifts:[{ obj: queue, alias: "myQueue", ops: ["push"] }],
// ^ object to lift, can be any preflight expression
// ^ Optional alias, by default, this will be the variable name passed to obj
// ^ methods to lift, if not provided then all methods will be granted
));
```

```ts
// bucket_create.ts
import inflight from "./bucket_create.inflight";

export default inflight(async ({ myQueue }, file) => {
// ^ inflight interface to your preflight queue
await myQueue.push(file);
});
```

## Using `extern` to expose JavaScript/TypeScript functions in preflight and inflight

When you want to use a JavaScript/TypeScript file anywhere in Wing, you can use the `extern` keyword to expose functions from that file.

1. Create a .js/.ts file that exports some functions

Expand Down
6 changes: 6 additions & 0 deletions examples/tests/invalid/inflight_intrinsic.test.w
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
let funkPath = "./path.ts";
let func: inflight (): str = @inflight(funkPath, lifts: [
// ^^^^^^^^ Must be a string literal
{ obj: [1, 2, 3] }
//^^^^^^^^^^^^^^^^^^ alias is required because object is not an identifier
]);
2 changes: 1 addition & 1 deletion examples/tests/invalid/intrinsics.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ inflight () => {
};

let path = @dirname();
// ^^ Unexpected arguments
// ^^ Unexpected arguments
2 changes: 1 addition & 1 deletion examples/tests/valid/external_ts.extern.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ export class Node {
readonly setContext: (key: string, value?: any) => void;
/** Return a direct child by id, or undefined.
@returns the child if found, or undefined */
readonly tryFindChild: (id: string) => (IConstruct) | undefined;
readonly tryFindChild: (id: string) => IConstruct | void;
/** Retrieves a value from tree context.
Context is usually initialized at the root, but can be overridden at any point in the tree.
@returns The context value or `undefined` if there is no context value for this key. */
Expand Down
8 changes: 8 additions & 0 deletions examples/tests/valid/inflight_ts/.example1.inflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/// This file is generated by Wing.
export interface Lifts {
};
type HandlerFunction<T> = T extends { handle: (...args: any[]) => any } ? T['handle'] : T;
type ExpectedFunction = HandlerFunction<(arg0: string) => Promise<string>>;
export type Handler = ((ctx: Lifts, ...args: Parameters<ExpectedFunction>) => ReturnType<ExpectedFunction>) & {};
export function inflight(handler: Handler): Handler { return handler; }
export default inflight;
63 changes: 63 additions & 0 deletions examples/tests/valid/inflight_ts/.example2.inflight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/// This file is generated by Wing.
export interface Lifts {
example: Example$Inflight
exampleCopy: Pick<Example$Inflight, "getMessage">
numbers: (readonly (number | number | number)[])
};
type HandlerFunction<T> = T extends { handle: (...args: any[]) => any } ? T['handle'] : T;
type ExpectedFunction = HandlerFunction<IFunctionHandler$Inflight>;
/** Entrypoint function that will be called when the cloud function is invoked. */
export type Handler = ((ctx: Lifts, ...args: Parameters<ExpectedFunction>) => ReturnType<ExpectedFunction>) & {};
export function inflight(handler: Handler): Handler { return handler; }
export default inflight;
/** Trait marker for classes that can be depended upon.
The presence of this interface indicates that an object has
an `IDependable` implementation.

This interface can be used to take an (ordering) dependency on a set of
constructs. An ordering dependency implies that the resources represented by
those constructs are deployed before the resources depending ON them are
deployed. */
export interface IDependable$Inflight {
}
/** Represents a construct. */
export interface IConstruct$Inflight extends IDependable$Inflight {
}
/** Represents the building block of the construct graph.
All constructs besides the root construct must be created within the scope of
another construct. */
export class Construct$Inflight implements IConstruct$Inflight {
}
/** Data that can be lifted into inflight. */
export interface ILiftable$Inflight {
}
/** A liftable object that needs to be registered on the host as part of the lifting process.
This is generally used so the host can set up permissions
to access the lifted object inflight. */
export interface IHostedLiftable$Inflight extends ILiftable$Inflight {
}
/** Abstract interface for `Resource`. */
export interface IResource$Inflight extends IConstruct$Inflight, IHostedLiftable$Inflight {
}
/** Shared behavior between all Wing SDK resources. */
export class Resource$Inflight extends Construct$Inflight implements IResource$Inflight {
}
export class Example$Inflight extends Resource$Inflight {
readonly $inflight_init: () => Promise<Example$Inflight>;
readonly done: () => Promise<void>;
readonly getMessage: () => Promise<string>;
}
/** Code that runs at runtime and implements your application's behavior.
For example, handling API requests, processing queue messages, etc.
Inflight code can be executed on various compute platforms in the cloud,
such as function services (such as AWS Lambda or Azure Functions),
containers (such as ECS or Kubernetes), VMs or even physical servers.

This data represents the code together with the bindings to preflight data required to run. */
export interface IInflight$Inflight extends IHostedLiftable$Inflight {
}
/** A resource with an inflight "handle" method that can be used to create a `cloud.Function`. */
export interface IFunctionHandler$Inflight extends IInflight$Inflight {
/** Entrypoint function that will be called when the cloud function is invoked. */
readonly handle: (event?: (string) | undefined) => Promise<string | void>;
}
5 changes: 5 additions & 0 deletions examples/tests/valid/inflight_ts/example1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import inflight from "./.example1.inflight";

export default inflight(async ({}, arg) => {
return arg;
})
11 changes: 11 additions & 0 deletions examples/tests/valid/inflight_ts/example2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import inflight from "./.example2.inflight";

export const main = inflight(async ({ example, numbers }, message) => {
const exampleMessage = await example.getMessage();
console.log(`"${message}" should be "${exampleMessage}"`);
console.log(numbers);
if (message !== exampleMessage) {
throw new Error("Message is not the same");
}
await example.done();
});
41 changes: 39 additions & 2 deletions examples/tests/valid/intrinsics.test.w
Original file line number Diff line number Diff line change
@@ -1,13 +1,50 @@
bring fs;
bring expect;
bring cloud;
bring util;
bring "./subdir/bar.w" as bar;

// @dirname

let path = "SHOULD_IGNORE";
let filename = "intrinsics.test.w";

let currentFile = fs.join(@dirname, filename);
expect.equal(filename, fs.basename(currentFile));

expect.equal(@dirname, fs.dirname(currentFile));

expect.equal(bar.Bar.getSubdir(), fs.join(@dirname, "subdir"));

// @inflight

let counter = new cloud.Counter();
MarkMcCulloh marked this conversation as resolved.
Show resolved Hide resolved
pub class Example {
pub inflight getMessage(): str {
return "message";
}
pub inflight done() {
counter.inc();
}
}

let echo: inflight (str): str = @inflight("./inflight_ts/example1.ts");

let example = new Example();
let funcFunction = @inflight("./inflight_ts/example2.ts",
export: "main",
lifts: [
{ obj: example },
{ obj: example, alias: "exampleCopy", ops: ["getMessage"]},
{ obj: [1, 2, 3], alias: "numbers" },
]
);
let func = new cloud.Function(funcFunction);


test "invoke default function" {
assert(echo("message") == "message");
}

test "invoke inflight function" {
funcFunction("message");
func.invoke("message");
}
7 changes: 7 additions & 0 deletions examples/tests/valid/json.test.w
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ struct StructyJson {
foo: str;
stuff: Array<num>;
maybe: InnerStructyJson?;
buckets: Array<cloud.Bucket>?;
}

let arrayStruct: Array<StructyJson> = [ { foo: "", stuff: [] } ];
Expand All @@ -237,6 +238,12 @@ let notJsonMissingField: StructyJson = {
stuff: [],
};

let notJsonWithInnerArray: StructyJson = {
foo: "bar",
stuff: [],
buckets: [new cloud.Bucket() as "B1InList"]
};

let notJson: StructyJson = {
foo: "bar",
stuff: [1, 2, 3],
Expand Down
57 changes: 29 additions & 28 deletions libs/wingc/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use itertools::Itertools;

use crate::diagnostic::WingSpan;

use crate::docs::Documented;
use crate::type_check::CLOSURE_CLASS_HANDLE_METHOD;

static EXPR_COUNTER: AtomicUsize = AtomicUsize::new(0);
Expand Down Expand Up @@ -579,66 +578,54 @@ pub struct Intrinsic {
pub kind: IntrinsicKind,
}

#[derive(Clone, Debug)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum IntrinsicKind {
/// Error state
Unknown,
Dirname,
Inflight,
}

impl Display for IntrinsicKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IntrinsicKind::Unknown => write!(f, "@"),
IntrinsicKind::Dirname => write!(f, "@dirname"),
}
}
}

impl Documented for IntrinsicKind {
fn docs(&self) -> Option<&crate::docs::Docs> {
None
}

fn render_docs(&self) -> String {
match self {
IntrinsicKind::Dirname => r#"Get the normalized absolute path of the current source file's directory.

The resolved path represents a path during preflight only and is not guaranteed to be valid while inflight.
It should primarily be used in preflight or in inflights that are guaranteed to be executed in the same filesystem where preflight executed."#.to_string(),
IntrinsicKind::Unknown => "".to_string(),
IntrinsicKind::Inflight => write!(f, "@inflight"),
}
}
}

impl IntrinsicKind {
pub const VALUES: [IntrinsicKind; 2] = [IntrinsicKind::Unknown, IntrinsicKind::Dirname];

pub fn from_str(s: &str) -> Self {
match s {
"@dirname" => IntrinsicKind::Dirname,
"@inflight" => IntrinsicKind::Inflight,
_ => IntrinsicKind::Unknown,
}
}

pub fn get_type(&self, types: &crate::type_check::Types) -> Option<crate::type_check::TypeRef> {
match self {
&IntrinsicKind::Dirname => Some(types.string()),
_ => None,
}
}

pub fn is_valid_phase(&self, phase: &Phase) -> bool {
match self {
IntrinsicKind::Unknown => true,
IntrinsicKind::Dirname => match phase {
Phase::Preflight => true,
_ => false,
},
IntrinsicKind::Unknown => true,
IntrinsicKind::Inflight => match phase {
Phase::Preflight => true,
_ => false,
},
}
}
}

impl Into<Symbol> for IntrinsicKind {
fn into(self) -> Symbol {
Symbol::global(self.to_string())
}
}

#[derive(Debug)]
pub enum ExprKind {
New(New),
Expand Down Expand Up @@ -730,6 +717,20 @@ impl Expr {
let id = EXPR_COUNTER.fetch_add(1, Ordering::SeqCst);
Self { id, kind, span }
}

pub fn as_static_string(&self) -> Option<&str> {
match &self.kind {
ExprKind::Literal(Literal::String(s)) => {
// strip the quotes ("data")
Some(&s[1..s.len() - 1])
}
ExprKind::Literal(Literal::NonInterpolatedString(s)) => {
// strip the quotes (#"data")
Some(&s[2..s.len() - 1])
}
_ => None,
}
}
}

pub type ArgListId = usize;
Expand Down
Loading
Loading