From 7e544e2f3f2c31da8d8bb3564a87f7351076ed8a Mon Sep 17 00:00:00 2001 From: Marcio Cruz de Almeida <67694075+marciocadev@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:30:39 -0300 Subject: [PATCH 1/6] fix(sdk): making cron schedule more cloud agnostic (second attempt) (#5956) Some characters like `?` are configurations used by AWS, but to make the schedule more cloud-agnostic, I made some changes to bring it closer to the Unix implementation Now a cron like `* * * * *` will not throw an exception. I removed this test because it no longer made sense. ```wing try { new cloud.Schedule( cron: "* * * * *" ) as "s5"; } catch e { error = e; } assert(error == "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other"); ``` Closes #2849 ## 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) - [ ] 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)*. --- .../04-standard-library/cloud/schedule.md | 10 +- examples/tests/sdk_tests/schedule/init.test.w | 8 - libs/awscdk/src/schedule.ts | 30 +- .../test/__snapshots__/schedule.test.ts.snap | 466 +++++++++- libs/awscdk/test/schedule.test.ts | 77 +- libs/wingsdk/src/cloud/schedule.ts | 15 +- libs/wingsdk/src/shared-aws/schedule.ts | 64 ++ libs/wingsdk/src/target-sim/util.ts | 4 +- libs/wingsdk/src/target-tf-aws/schedule.ts | 11 +- .../__snapshots__/schedule.test.ts.snap | 8 +- libs/wingsdk/test/target-sim/schedule.test.ts | 8 +- libs/wingsdk/test/target-sim/utils.test.ts | 6 +- .../__snapshots__/schedule.test.ts.snap | 799 +++++++++++++++++- .../test/target-tf-aws/schedule.test.ts | 133 ++- .../schedule/on_tick.test.w_compile_tf-aws.md | 2 - 15 files changed, 1561 insertions(+), 80 deletions(-) create mode 100644 libs/wingsdk/src/shared-aws/schedule.ts diff --git a/docs/docs/04-standard-library/cloud/schedule.md b/docs/docs/04-standard-library/cloud/schedule.md index 766c745eb03..c10249df724 100644 --- a/docs/docs/04-standard-library/cloud/schedule.md +++ b/docs/docs/04-standard-library/cloud/schedule.md @@ -301,13 +301,21 @@ Trigger events according to a cron schedule using the UNIX cron format. Timezone is UTC. [minute] [hour] [day of month] [month] [day of week] +'*' means all possible values. +'-' means a range of values. +',' means a list of values. +[minute] allows 0-59. +[hour] allows 0-23. +[day of month] allows 1-31. +[month] allows 1-12 or JAN-DEC. +[day of week] allows 0-6 or SUN-SAT. --- *Example* ```wing -"0/1 * ? * *" +"* * * * *" ``` diff --git a/examples/tests/sdk_tests/schedule/init.test.w b/examples/tests/sdk_tests/schedule/init.test.w index e803edff0bb..dc8c3bd27a0 100644 --- a/examples/tests/sdk_tests/schedule/init.test.w +++ b/examples/tests/sdk_tests/schedule/init.test.w @@ -39,12 +39,4 @@ if (util.env("WING_TARGET") != "sim") { error = e; } assert(error == "cron string must be UNIX cron format [minute] [hour] [day of month] [month] [day of week]"); - - - try { - new cloud.Schedule( cron: "* * * * *" ) as "s5"; - } catch e { - error = e; - } - assert(error == "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other"); } \ No newline at end of file diff --git a/libs/awscdk/src/schedule.ts b/libs/awscdk/src/schedule.ts index 6bdb8471ccb..b30978181d0 100644 --- a/libs/awscdk/src/schedule.ts +++ b/libs/awscdk/src/schedule.ts @@ -9,8 +9,10 @@ import { Construct } from "constructs"; import { App } from "./app"; import { cloud, core, std } from "@winglang/sdk"; import { convertBetweenHandlers } from "@winglang/sdk/lib/shared/convert"; +import { convertUnixCronToAWSCron } from "@winglang/sdk/lib/shared-aws/schedule"; import { isAwsCdkFunction } from "./function"; + /** * AWS implementation of `cloud.Schedule`. * @@ -25,27 +27,15 @@ export class Schedule extends cloud.Schedule { const { rate, cron } = props; - /* - * The schedule cron string is Unix cron format: [minute] [hour] [day of month] [month] [day of week] - * AWS EventBridge Schedule uses a 6 field format which includes year: [minute] [hour] [day of month] [month] [day of week] [year] - * https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based - * - * We append * to the cron string for year field. - */ if (cron) { - const cronArr = cron.split(" "); - let cronOpt: { [k: string]: string } = { - minute: cronArr[0], - hour: cronArr[1], - month: cronArr[3], - year: "*", - }; - if (cronArr[2] !== "?") { - cronOpt.day = cronArr[2]; - } - if (cronArr[4] !== "?") { - cronOpt.weekDay = cronArr[4]; - } + let cronOpt: { [k: string]: string } = {}; + const awsCron = convertUnixCronToAWSCron(cron); + const cronArr = awsCron.split(" "); + if (cronArr[0] !== "*" && cronArr[0] !== "?") { cronOpt.minute = cronArr[0]; } + if (cronArr[1] !== "*" && cronArr[1] !== "?") { cronOpt.hour = cronArr[1]; } + if (cronArr[2] !== "*" && cronArr[2] !== "?") { cronOpt.day = cronArr[2]; } + if (cronArr[3] !== "*" && cronArr[3] !== "?") { cronOpt.month = cronArr[3]; } + if (cronArr[4] !== "*" && cronArr[4] !== "?") { cronOpt.weekDay = cronArr[4]; } this.scheduleExpression = EventSchedule.cron(cronOpt); } else { diff --git a/libs/awscdk/test/__snapshots__/schedule.test.ts.snap b/libs/awscdk/test/__snapshots__/schedule.test.ts.snap index 535a6820c2d..63bb0e132ba 100644 --- a/libs/awscdk/test/__snapshots__/schedule.test.ts.snap +++ b/libs/awscdk/test/__snapshots__/schedule.test.ts.snap @@ -1,5 +1,467 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`convert single dayOfWeek from Unix to AWS 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "Schedule251B1F83": { + "Properties": { + "ScheduleExpression": "cron(* * ? * 0 *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "ScheduleAllowEventRulemyprojectScheduleOnTick0FF908B3EE22FC7ED": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Schedule251B1F83", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ScheduleOnTick059D62C99": { + "DependsOn": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "", + }, + "Environment": { + "Variables": { + "NODE_OPTIONS": "--enable-source-maps", + }, + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogGroup": { + "Ref": "ScheduleOnTick0LogGroup389684B1", + }, + }, + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Timeout": 60, + }, + "Type": "AWS::Lambda::Function", + }, + "ScheduleOnTick0LogGroup389684B1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 30, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ScheduleOnTick0ServiceRole37EF1AE1": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`convert the list of dayOfWeek from Unix to AWS 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "Schedule251B1F83": { + "Properties": { + "ScheduleExpression": "cron(* * ? * 0,2,4,6 *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "ScheduleAllowEventRulemyprojectScheduleOnTick0FF908B3EE22FC7ED": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Schedule251B1F83", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ScheduleOnTick059D62C99": { + "DependsOn": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "", + }, + "Environment": { + "Variables": { + "NODE_OPTIONS": "--enable-source-maps", + }, + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogGroup": { + "Ref": "ScheduleOnTick0LogGroup389684B1", + }, + }, + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Timeout": 60, + }, + "Type": "AWS::Lambda::Function", + }, + "ScheduleOnTick0LogGroup389684B1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 30, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ScheduleOnTick0ServiceRole37EF1AE1": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`convert the range of dayOfWeek from Unix to AWS 1`] = ` +{ + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "Schedule251B1F83": { + "Properties": { + "ScheduleExpression": "cron(* * ? * 0-6 *)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Id": "Target0", + }, + ], + }, + "Type": "AWS::Events::Rule", + }, + "ScheduleAllowEventRulemyprojectScheduleOnTick0FF908B3EE22FC7ED": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ScheduleOnTick059D62C99", + "Arn", + ], + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Schedule251B1F83", + "Arn", + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ScheduleOnTick059D62C99": { + "DependsOn": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + ], + "Properties": { + "Architectures": [ + "arm64", + ], + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "", + }, + "Environment": { + "Variables": { + "NODE_OPTIONS": "--enable-source-maps", + }, + }, + "Handler": "index.handler", + "LoggingConfig": { + "LogGroup": { + "Ref": "ScheduleOnTick0LogGroup389684B1", + }, + }, + "MemorySize": 1024, + "Role": { + "Fn::GetAtt": [ + "ScheduleOnTick0ServiceRole37EF1AE1", + "Arn", + ], + }, + "Runtime": "nodejs20.x", + "Timeout": 60, + }, + "Type": "AWS::Lambda::Function", + }, + "ScheduleOnTick0LogGroup389684B1": { + "DeletionPolicy": "Retain", + "Properties": { + "RetentionInDays": 30, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ScheduleOnTick0ServiceRole37EF1AE1": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + exports[`schedule behavior with cron 1`] = ` { "Parameters": { @@ -12,7 +474,7 @@ exports[`schedule behavior with cron 1`] = ` "Resources": { "Schedule251B1F83": { "Properties": { - "ScheduleExpression": "cron(0/1 * ? * * *)", + "ScheduleExpression": "cron(0/1 * * * ? *)", "State": "ENABLED", "Targets": [ { @@ -320,7 +782,7 @@ exports[`schedule with two functions 1`] = ` "Resources": { "Schedule251B1F83": { "Properties": { - "ScheduleExpression": "cron(0/1 * ? * * *)", + "ScheduleExpression": "cron(0/1 * * * ? *)", "State": "ENABLED", "Targets": [ { diff --git a/libs/awscdk/test/schedule.test.ts b/libs/awscdk/test/schedule.test.ts index 5092f3c4f30..453ae1712cd 100644 --- a/libs/awscdk/test/schedule.test.ts +++ b/libs/awscdk/test/schedule.test.ts @@ -33,7 +33,7 @@ test("schedule behavior with cron", () => { `async handle(event) { console.log("Received: ", event); }` ); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn); const output = app.synth(); @@ -42,7 +42,70 @@ test("schedule behavior with cron", () => { const template = Template.fromJSON(JSON.parse(output)); template.resourceCountIs("AWS::Events::Rule", 1); template.hasResourceProperties("AWS::Events::Rule", { - ScheduleExpression: "cron(0/1 * ? * * *)", + ScheduleExpression: "cron(0/1 * * * ? *)", + }); + expect(awscdkSanitize(template)).toMatchSnapshot(); +}); + +test("convert single dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const fn = simulator.Testing.makeHandler( + `async handle(event) { console.log("Received: ", event); }` + ); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: "cron(* * ? * 0 *)", + }); + expect(awscdkSanitize(template)).toMatchSnapshot(); +}); + +test("convert the range of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const fn = simulator.Testing.makeHandler( + `async handle(event) { console.log("Received: ", event); }` + ); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1-7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: "cron(* * ? * 0-6 *)", + }); + expect(awscdkSanitize(template)).toMatchSnapshot(); +}); + +test("convert the list of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); + const fn = simulator.Testing.makeHandler( + `async handle(event) { console.log("Received: ", event); }` + ); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1,3,5,7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + const template = Template.fromJSON(JSON.parse(output)); + template.resourceCountIs("AWS::Events::Rule", 1); + template.hasResourceProperties("AWS::Events::Rule", { + ScheduleExpression: "cron(* * ? * 0,2,4,6 *)", }); expect(awscdkSanitize(template)).toMatchSnapshot(); }); @@ -54,7 +117,7 @@ test("schedule with two functions", () => { `async handle(event) { console.log("Received: ", event); }` ); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn); const output = app.synth(); @@ -123,7 +186,7 @@ test("schedule with rate less than 1 minute", () => { ).toThrow("rate can not be set to less than 1 minute."); }); -test("cron with Day-of-month and Day-of-week setting with *", () => { +test("cron with day of month and day of week configured at the same time", () => { // GIVEN const app = new awscdk.App({ outdir: mkdtemp(), ...CDK_APP_OPTS }); @@ -131,9 +194,7 @@ test("cron with Day-of-month and Day-of-week setting with *", () => { expect( () => new cloud.Schedule(app, "Schedule", { - cron: "0/1 * * * *", + cron: "* * 1 * 1", }) - ).toThrow( - "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other" - ); + ).toThrow("Cannot restrict both 'day-of-month' and 'day-of-week' in a cron expression, at least one must be '*'"); }); diff --git a/libs/wingsdk/src/cloud/schedule.ts b/libs/wingsdk/src/cloud/schedule.ts index 3f1007be7c6..a452793402e 100644 --- a/libs/wingsdk/src/cloud/schedule.ts +++ b/libs/wingsdk/src/cloud/schedule.ts @@ -24,7 +24,15 @@ export interface ScheduleProps { /** * Trigger events according to a cron schedule using the UNIX cron format. Timezone is UTC. * [minute] [hour] [day of month] [month] [day of week] - * @example "0/1 * ? * *" + * '*' means all possible values. + * '-' means a range of values. + * ',' means a list of values. + * [minute] allows 0-59. + * [hour] allows 0-23. + * [day of month] allows 1-31. + * [month] allows 1-12 or JAN-DEC. + * [day of week] allows 0-6 or SUN-SAT. + * @example "* * * * *" * @default undefined */ readonly cron?: string; @@ -67,11 +75,6 @@ export class Schedule extends Resource { "cron string must be UNIX cron format [minute] [hour] [day of month] [month] [day of week]" ); } - if (cron && cron.split(" ")[2] == "*" && cron.split(" ")[4] == "*") { - throw new Error( - "cannot use * in both the Day-of-month and Day-of-week fields. If you use it in one, you must use ? in the other" - ); - } } /** diff --git a/libs/wingsdk/src/shared-aws/schedule.ts b/libs/wingsdk/src/shared-aws/schedule.ts new file mode 100644 index 00000000000..424fc6f4cd7 --- /dev/null +++ b/libs/wingsdk/src/shared-aws/schedule.ts @@ -0,0 +1,64 @@ +/** + * Convert Unix cron to AWS cron + */ +export const convertUnixCronToAWSCron = (cron: string) => { + const minute = cron.split(" ")[0]; + const hour = cron.split(" ")[1]; + let dayOfMonth = cron.split(" ")[2]; + const month = cron.split(" ")[3]; + let dayOfWeek = cron.split(" ")[4]; + + /* + * The implementation of cron on AWS does not allow [day of month] and [day of week] + * to have the character '*' at the same time. + * Therefore, [day of week] will be replaced by '?'. + */ + if (cron && dayOfMonth == "*" && dayOfWeek == "*") { + dayOfWeek = "?"; + } + + if (cron && dayOfMonth !== "*" && dayOfWeek !== "*") { + throw new Error( + "Cannot restrict both 'day-of-month' and 'day-of-week' in a cron expression, at least one must be '*'" + ); + } + + if (dayOfWeek !== "*" && dayOfWeek !== "?") { + dayOfMonth = "?"; + if (/\d/.test(dayOfWeek)) { + dayOfWeek = convertDayOfWeekFromUnixToAWS(dayOfWeek); + } + } + + /* + * The schedule cron string is Unix cron format: [minute] [hour] [day of month] [month] [day of week] + * AWS EventBridge Schedule uses a 6 field format which includes year: [minute] [hour] [day of month] [month] [day of week] [year] + * https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based + * + * We append * to the cron string for year field. + */ + return ( + minute + + " " + + hour + + " " + + dayOfMonth + + " " + + month + + " " + + dayOfWeek + + " *" + ); +}; + +const convertDayOfWeekFromUnixToAWS = (dayOfWeek: string): string => { + const numbers = dayOfWeek.match(/\d+/g); + + if (numbers) { + for (const number of numbers) { + dayOfWeek = dayOfWeek.replace(number, (parseInt(number) - 1).toString()); + } + } + + return dayOfWeek; +}; diff --git a/libs/wingsdk/src/target-sim/util.ts b/libs/wingsdk/src/target-sim/util.ts index 71a78a6cbc4..17074dec81b 100644 --- a/libs/wingsdk/src/target-sim/util.ts +++ b/libs/wingsdk/src/target-sim/util.ts @@ -75,9 +75,7 @@ export function convertDurationToCronExpression(dur: Duration): string { // for now we just use * for day, month, and year const dayInMonth = "*"; const month = "*"; - // if day of month is "*", day of week should be "?" - // https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html - const dayOfWeek = "?"; + const dayOfWeek = "*"; // Generate cron string based on the duration const cronString = `${minute} ${hour} ${dayInMonth} ${month} ${dayOfWeek}`; diff --git a/libs/wingsdk/src/target-tf-aws/schedule.ts b/libs/wingsdk/src/target-tf-aws/schedule.ts index c660b26b61d..9d0d515cce6 100644 --- a/libs/wingsdk/src/target-tf-aws/schedule.ts +++ b/libs/wingsdk/src/target-tf-aws/schedule.ts @@ -7,6 +7,7 @@ import { CloudwatchEventTarget } from "../.gen/providers/aws/cloudwatch-event-ta import * as cloud from "../cloud"; import * as core from "../core"; import { convertBetweenHandlers } from "../shared/convert"; +import { convertUnixCronToAWSCron } from "../shared-aws/schedule"; import { Node } from "../std"; /** @@ -24,21 +25,13 @@ export class Schedule extends cloud.Schedule { const { rate, cron } = props; - /* - * The schedule cron string is Unix cron format: [minute] [hour] [day of month] [month] [day of week] - * AWS EventBridge Schedule uses a 6 field format which includes year: [minute] [hour] [day of month] [month] [day of week] [year] - * https://docs.aws.amazon.com/scheduler/latest/UserGuide/schedule-types.html#cron-based - * - * We append * to the cron string for year field. - */ this.scheduleExpression = rate ? rate.minutes === 1 ? `rate(${rate.minutes} minute)` : `rate(${rate.minutes} minutes)` - : `cron(${cron} *)`; + : `cron(${convertUnixCronToAWSCron(cron!)})`; this.rule = new CloudwatchEventRule(this, "Schedule", { - isEnabled: true, scheduleExpression: this.scheduleExpression, }); } diff --git a/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap b/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap index 00dcc05c893..c255a568385 100644 --- a/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap +++ b/libs/wingsdk/test/target-sim/__snapshots__/schedule.test.ts.snap @@ -13,7 +13,7 @@ exports[`create a schedule 1`] = ` "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "*/1 * * * ?", + "cronExpression": "*/1 * * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, @@ -182,7 +182,7 @@ console.log("Hello from schedule!"); "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "* */3 * * ?", + "cronExpression": "* */3 * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, @@ -392,7 +392,7 @@ console.log("Hello from schedule!"); "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "*/10 * * * ?", + "cronExpression": "*/10 * * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, @@ -602,7 +602,7 @@ console.log("Hello from schedule!"); "attrs": {}, "path": "root/my_schedule", "props": { - "cronExpression": "* * * * ?", + "cronExpression": "* * * * *", }, "type": "@winglang/sdk.cloud.Schedule", }, diff --git a/libs/wingsdk/test/target-sim/schedule.test.ts b/libs/wingsdk/test/target-sim/schedule.test.ts index 1a6624e4d0f..8bc8d691bb4 100644 --- a/libs/wingsdk/test/target-sim/schedule.test.ts +++ b/libs/wingsdk/test/target-sim/schedule.test.ts @@ -12,7 +12,7 @@ console.log("Hello from schedule!"); test("create a schedule", async () => { // GIVEN const app = new SimApp(); - const cron = "*/1 * * * ?"; + const cron = "*/1 * * * *"; new cloud.Schedule(app, "my_schedule", { cron }); const s = await app.startSimulator(); @@ -39,7 +39,7 @@ test("schedule with one task with cron", async () => { const app = new SimApp(); const handler = Testing.makeHandler(INFLIGHT_CODE); const schedule = new cloud.Schedule(app, "my_schedule", { - cron: "* * * * ?", + cron: "* * * * *", }); schedule.onTick(handler); @@ -59,7 +59,7 @@ test("schedule with one task using rate of 10m", async () => { const schedule = new cloud.Schedule(app, "my_schedule", { rate: Duration.fromMinutes(10), }); - const expectedCron = "*/10 * * * ?"; // every 10 minutes cron expression + const expectedCron = "*/10 * * * *"; // every 10 minutes cron expression schedule.onTick(handler); const s = await app.startSimulator(); @@ -87,7 +87,7 @@ test("schedule with one task using rate of 3h", async () => { const schedule = new cloud.Schedule(app, "my_schedule", { rate: Duration.fromHours(3), }); - const expectedCron = "* */3 * * ?"; // every 3 hours cron expression + const expectedCron = "* */3 * * *"; // every 3 hours cron expression schedule.onTick(handler); const s = await app.startSimulator(); diff --git a/libs/wingsdk/test/target-sim/utils.test.ts b/libs/wingsdk/test/target-sim/utils.test.ts index cd5824ed67a..0ea5c3ea185 100644 --- a/libs/wingsdk/test/target-sim/utils.test.ts +++ b/libs/wingsdk/test/target-sim/utils.test.ts @@ -6,7 +6,7 @@ describe("convertDurationToCronExpression", () => { test("converts a duration from minutes", () => { // GIVEN const dur = Duration.fromMinutes(10); - const expectedCron = "*/10 * * * ?"; + const expectedCron = "*/10 * * * *"; // WHEN const cron = convertDurationToCronExpression(dur); @@ -17,7 +17,7 @@ describe("convertDurationToCronExpression", () => { test("converts a duration from hours", () => { const dur = Duration.fromHours(2); - const expectedCron = "* */2 * * ?"; + const expectedCron = "* */2 * * *"; // WHEN const cron = convertDurationToCronExpression(dur); @@ -29,7 +29,7 @@ describe("convertDurationToCronExpression", () => { test("converts durations with fractional hours", () => { // GIVEN const dur = Duration.fromHours(2.5); - const expectedCron = "*/30 */2 * * ?"; + const expectedCron = "*/30 */2 * * *"; // WHEN const cron = convertDurationToCronExpression(dur); diff --git a/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap b/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap index 1665082908f..5f054a6624f 100644 --- a/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap +++ b/libs/wingsdk/test/target-tf-aws/__snapshots__/schedule.test.ts.snap @@ -1,12 +1,803 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`convert single dayOfWeek from Unix to AWS 1`] = ` +{ + "resource": { + "aws_cloudwatch_event_rule": { + "Schedule_15669BF1": { + "schedule_expression": "cron(* * ? * 0 *)", + }, + }, + "aws_cloudwatch_event_target": { + "Schedule_ScheduleTarget0_12D341DB": { + "arn": "\${aws_lambda_function.Schedule_OnTick0_958638E3.qualified_arn}", + "rule": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.name}", + }, + }, + "aws_cloudwatch_log_group": { + "Schedule_OnTick0_CloudwatchLogGroup_A06DC96E": { + "name": "/aws/lambda/OnTick0-c8e1d4a8", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Schedule_OnTick0_IamRole_478B0576": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Schedule_OnTick0_IamRolePolicy_708CFC38": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Schedule_OnTick0_IamRolePolicyAttachment_5885D6B3": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_lambda_function": { + "Schedule_OnTick0_958638E3": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "OnTick0-c8e1d4a8", + }, + }, + "function_name": "OnTick0-c8e1d4a8", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Schedule_OnTick0_S3Object_95D0AF10.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_lambda_permission": { + "Schedule_OnTick0_InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3_17682171": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_function.Schedule_OnTick0_958638E3.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_function.Schedule_OnTick0_958638E3.version}", + "source_arn": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.arn}", + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Schedule_OnTick0_S3Object_95D0AF10": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`convert single dayOfWeek from Unix to AWS 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "Schedule": { + "children": { + "OnTick0": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Schedule/OnTick0/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Schedule/OnTick0/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Schedule/OnTick0/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Schedule/OnTick0/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Schedule/OnTick0/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Schedule/OnTick0/IamRolePolicyAttachment", + }, + "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + "path": "root/Default/Schedule/OnTick0/InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Schedule/OnTick0/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "OnTick0", + "path": "root/Default/Schedule/OnTick0", + }, + "Schedule": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Schedule", + "path": "root/Default/Schedule/Schedule", + }, + "ScheduleTarget0": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "ScheduleTarget0", + "path": "root/Default/Schedule/ScheduleTarget0", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud schedule to trigger events at regular intervals", + "title": "Schedule", + }, + "id": "Schedule", + "path": "root/Default/Schedule", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + +exports[`convert the list of dayOfWeek from Unix to AWS 1`] = ` +{ + "resource": { + "aws_cloudwatch_event_rule": { + "Schedule_15669BF1": { + "schedule_expression": "cron(* * ? * 0,2,4,6 *)", + }, + }, + "aws_cloudwatch_event_target": { + "Schedule_ScheduleTarget0_12D341DB": { + "arn": "\${aws_lambda_function.Schedule_OnTick0_958638E3.qualified_arn}", + "rule": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.name}", + }, + }, + "aws_cloudwatch_log_group": { + "Schedule_OnTick0_CloudwatchLogGroup_A06DC96E": { + "name": "/aws/lambda/OnTick0-c8e1d4a8", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Schedule_OnTick0_IamRole_478B0576": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Schedule_OnTick0_IamRolePolicy_708CFC38": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Schedule_OnTick0_IamRolePolicyAttachment_5885D6B3": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_lambda_function": { + "Schedule_OnTick0_958638E3": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "OnTick0-c8e1d4a8", + }, + }, + "function_name": "OnTick0-c8e1d4a8", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Schedule_OnTick0_S3Object_95D0AF10.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_lambda_permission": { + "Schedule_OnTick0_InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3_17682171": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_function.Schedule_OnTick0_958638E3.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_function.Schedule_OnTick0_958638E3.version}", + "source_arn": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.arn}", + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Schedule_OnTick0_S3Object_95D0AF10": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`convert the list of dayOfWeek from Unix to AWS 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "Schedule": { + "children": { + "OnTick0": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Schedule/OnTick0/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Schedule/OnTick0/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Schedule/OnTick0/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Schedule/OnTick0/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Schedule/OnTick0/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Schedule/OnTick0/IamRolePolicyAttachment", + }, + "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + "path": "root/Default/Schedule/OnTick0/InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Schedule/OnTick0/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "OnTick0", + "path": "root/Default/Schedule/OnTick0", + }, + "Schedule": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Schedule", + "path": "root/Default/Schedule/Schedule", + }, + "ScheduleTarget0": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "ScheduleTarget0", + "path": "root/Default/Schedule/ScheduleTarget0", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud schedule to trigger events at regular intervals", + "title": "Schedule", + }, + "id": "Schedule", + "path": "root/Default/Schedule", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + +exports[`convert the range of dayOfWeek from Unix to AWS 1`] = ` +{ + "resource": { + "aws_cloudwatch_event_rule": { + "Schedule_15669BF1": { + "schedule_expression": "cron(* * ? * 0-6 *)", + }, + }, + "aws_cloudwatch_event_target": { + "Schedule_ScheduleTarget0_12D341DB": { + "arn": "\${aws_lambda_function.Schedule_OnTick0_958638E3.qualified_arn}", + "rule": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.name}", + }, + }, + "aws_cloudwatch_log_group": { + "Schedule_OnTick0_CloudwatchLogGroup_A06DC96E": { + "name": "/aws/lambda/OnTick0-c8e1d4a8", + "retention_in_days": 30, + }, + }, + "aws_iam_role": { + "Schedule_OnTick0_IamRole_478B0576": { + "assume_role_policy": "{"Version":"2012-10-17","Statement":[{"Action":"sts:AssumeRole","Principal":{"Service":"lambda.amazonaws.com"},"Effect":"Allow"}]}", + }, + }, + "aws_iam_role_policy": { + "Schedule_OnTick0_IamRolePolicy_708CFC38": { + "policy": "{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"none:null","Resource":"*"}]}", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_iam_role_policy_attachment": { + "Schedule_OnTick0_IamRolePolicyAttachment_5885D6B3": { + "policy_arn": "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.name}", + }, + }, + "aws_lambda_function": { + "Schedule_OnTick0_958638E3": { + "architectures": [ + "arm64", + ], + "environment": { + "variables": { + "NODE_OPTIONS": "--enable-source-maps", + "WING_FUNCTION_NAME": "OnTick0-c8e1d4a8", + }, + }, + "function_name": "OnTick0-c8e1d4a8", + "handler": "index.handler", + "memory_size": 1024, + "publish": true, + "role": "\${aws_iam_role.Schedule_OnTick0_IamRole_478B0576.arn}", + "runtime": "nodejs20.x", + "s3_bucket": "\${aws_s3_bucket.Code.bucket}", + "s3_key": "\${aws_s3_object.Schedule_OnTick0_S3Object_95D0AF10.key}", + "timeout": 60, + "vpc_config": { + "security_group_ids": [], + "subnet_ids": [], + }, + }, + }, + "aws_lambda_permission": { + "Schedule_OnTick0_InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3_17682171": { + "action": "lambda:InvokeFunction", + "function_name": "\${aws_lambda_function.Schedule_OnTick0_958638E3.function_name}", + "principal": "events.amazonaws.com", + "qualifier": "\${aws_lambda_function.Schedule_OnTick0_958638E3.version}", + "source_arn": "\${aws_cloudwatch_event_rule.Schedule_15669BF1.arn}", + }, + }, + "aws_s3_bucket": { + "Code": { + "bucket_prefix": "code-c84a50b1-", + }, + }, + "aws_s3_object": { + "Schedule_OnTick0_S3Object_95D0AF10": { + "bucket": "\${aws_s3_bucket.Code.bucket}", + "key": "", + "source": "", + }, + }, + }, +} +`; + +exports[`convert the range of dayOfWeek from Unix to AWS 2`] = ` +{ + "tree": { + "children": { + "root": { + "children": { + "Default": { + "children": { + "Code": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Code", + "path": "root/Default/Code", + }, + "ParameterRegistrar": { + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "ParameterRegistrar", + "path": "root/Default/ParameterRegistrar", + }, + "Schedule": { + "children": { + "OnTick0": { + "children": { + "Asset": { + "constructInfo": { + "fqn": "cdktf.TerraformAsset", + "version": "0.20.3", + }, + "id": "Asset", + "path": "root/Default/Schedule/OnTick0/Asset", + }, + "CloudwatchLogGroup": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "CloudwatchLogGroup", + "path": "root/Default/Schedule/OnTick0/CloudwatchLogGroup", + }, + "Default": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Default", + "path": "root/Default/Schedule/OnTick0/Default", + }, + "IamRole": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRole", + "path": "root/Default/Schedule/OnTick0/IamRole", + }, + "IamRolePolicy": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicy", + "path": "root/Default/Schedule/OnTick0/IamRolePolicy", + }, + "IamRolePolicyAttachment": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "IamRolePolicyAttachment", + "path": "root/Default/Schedule/OnTick0/IamRolePolicyAttachment", + }, + "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + "path": "root/Default/Schedule/OnTick0/InvokePermission-c8b3fc394731d07e61c00e422c6b234372c09bc3b3", + }, + "S3Object": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "S3Object", + "path": "root/Default/Schedule/OnTick0/S3Object", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud function (FaaS)", + "title": "Function", + }, + "id": "OnTick0", + "path": "root/Default/Schedule/OnTick0", + }, + "Schedule": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "Schedule", + "path": "root/Default/Schedule/Schedule", + }, + "ScheduleTarget0": { + "constructInfo": { + "fqn": "cdktf.TerraformResource", + "version": "0.20.3", + }, + "id": "ScheduleTarget0", + "path": "root/Default/Schedule/ScheduleTarget0", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "display": { + "description": "A cloud schedule to trigger events at regular intervals", + "title": "Schedule", + }, + "id": "Schedule", + "path": "root/Default/Schedule", + }, + "aws": { + "constructInfo": { + "fqn": "cdktf.TerraformProvider", + "version": "0.20.3", + }, + "id": "aws", + "path": "root/Default/aws", + }, + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0", + }, + "id": "Default", + "path": "root/Default", + }, + "backend": { + "constructInfo": { + "fqn": "cdktf.LocalBackend", + "version": "0.20.3", + }, + "id": "backend", + "path": "root/backend", + }, + }, + "constructInfo": { + "fqn": "cdktf.TerraformStack", + "version": "0.20.3", + }, + "id": "root", + "path": "root", + }, + }, + "constructInfo": { + "fqn": "cdktf.App", + "version": "0.20.3", + }, + "id": "App", + "path": "", + }, + "version": "tree-0.1", +} +`; + exports[`schedule behavior with cron 1`] = ` { "resource": { "aws_cloudwatch_event_rule": { "Schedule_15669BF1": { - "is_enabled": true, - "schedule_expression": "cron(0/1 * ? * * *)", + "schedule_expression": "cron(0/1 * * * ? *)", }, }, "aws_cloudwatch_event_target": { @@ -270,7 +1061,6 @@ exports[`schedule behavior with rate 1`] = ` "resource": { "aws_cloudwatch_event_rule": { "Schedule_15669BF1": { - "is_enabled": true, "schedule_expression": "rate(2 minutes)", }, }, @@ -535,8 +1325,7 @@ exports[`schedule with two functions 1`] = ` "resource": { "aws_cloudwatch_event_rule": { "Schedule_15669BF1": { - "is_enabled": true, - "schedule_expression": "cron(0/1 * ? * * *)", + "schedule_expression": "cron(0/1 * * * ? *)", }, }, "aws_cloudwatch_event_target": { diff --git a/libs/wingsdk/test/target-tf-aws/schedule.test.ts b/libs/wingsdk/test/target-tf-aws/schedule.test.ts index f3c53b36bb2..00e867c7534 100644 --- a/libs/wingsdk/test/target-tf-aws/schedule.test.ts +++ b/libs/wingsdk/test/target-tf-aws/schedule.test.ts @@ -49,7 +49,7 @@ test("schedule behavior with cron", () => { const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); const fn = Testing.makeHandler(CODE_LOG_EVENT); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn); const output = app.synth(); @@ -72,7 +72,115 @@ test("schedule behavior with cron", () => { output, "aws_cloudwatch_event_rule", { - schedule_expression: "cron(0/1 * ? * * *)", + schedule_expression: "cron(0/1 * * * ? *)", + } + ) + ).toEqual(true); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("convert single dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const fn = Testing.makeHandler(CODE_LOG_EVENT); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + expect(tfResourcesOf(output)).toEqual([ + "aws_cloudwatch_event_rule", // main schedule event + "aws_cloudwatch_event_target", // schedule target + "aws_cloudwatch_log_group", // log group for function + "aws_iam_role", // role for function + "aws_iam_role_policy", // policy for role + "aws_iam_role_policy_attachment", // execution policy for role + "aws_lambda_function", // processor function + "aws_lambda_permission", // function permission + "aws_s3_bucket", // S3 bucket for code + "aws_s3_object", // S3 object for code + ]); + expect( + cdktf.Testing.toHaveResourceWithProperties( + output, + "aws_cloudwatch_event_rule", + { + schedule_expression: "cron(* * ? * 0 *)", + } + ) + ).toEqual(true); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("convert the range of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const fn = Testing.makeHandler(CODE_LOG_EVENT); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1-7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + expect(tfResourcesOf(output)).toEqual([ + "aws_cloudwatch_event_rule", // main schedule event + "aws_cloudwatch_event_target", // schedule target + "aws_cloudwatch_log_group", // log group for function + "aws_iam_role", // role for function + "aws_iam_role_policy", // policy for role + "aws_iam_role_policy_attachment", // execution policy for role + "aws_lambda_function", // processor function + "aws_lambda_permission", // function permission + "aws_s3_bucket", // S3 bucket for code + "aws_s3_object", // S3 object for code + ]); + expect( + cdktf.Testing.toHaveResourceWithProperties( + output, + "aws_cloudwatch_event_rule", + { + schedule_expression: "cron(* * ? * 0-6 *)", + } + ) + ).toEqual(true); + expect(tfSanitize(output)).toMatchSnapshot(); + expect(treeJsonOf(app.outdir)).toMatchSnapshot(); +}); + +test("convert the list of dayOfWeek from Unix to AWS", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + const fn = Testing.makeHandler(CODE_LOG_EVENT); + const schedule = new cloud.Schedule(app, "Schedule", { + cron: "* * * * 1,3,5,7", + }); + schedule.onTick(fn); + const output = app.synth(); + + // THEN + expect(tfResourcesOf(output)).toEqual([ + "aws_cloudwatch_event_rule", // main schedule event + "aws_cloudwatch_event_target", // schedule target + "aws_cloudwatch_log_group", // log group for function + "aws_iam_role", // role for function + "aws_iam_role_policy", // policy for role + "aws_iam_role_policy_attachment", // execution policy for role + "aws_lambda_function", // processor function + "aws_lambda_permission", // function permission + "aws_s3_bucket", // S3 bucket for code + "aws_s3_object", // S3 object for code + ]); + expect( + cdktf.Testing.toHaveResourceWithProperties( + output, + "aws_cloudwatch_event_rule", + { + schedule_expression: "cron(* * ? * 0,2,4,6 *)", } ) ).toEqual(true); @@ -86,7 +194,7 @@ test("schedule with two functions", () => { const fn1 = Testing.makeHandler(CODE_LOG_EVENT); const fn2 = Testing.makeHandler(CODE_LOG_EVENT); const schedule = new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * *", + cron: "0/1 * * * *", }); schedule.onTick(fn1); schedule.onTick(fn2); @@ -118,7 +226,7 @@ test("schedule with rate and cron simultaneously", () => { () => new cloud.Schedule(app, "Schedule", { rate: std.Duration.fromSeconds(30), - cron: "0/1 * ? * *", + cron: "0/1 * * * ?", }) ).toThrow("rate and cron cannot be configured simultaneously."); }); @@ -131,7 +239,7 @@ test("cron with more than five values", () => { expect( () => new cloud.Schedule(app, "Schedule", { - cron: "0/1 * ? * * *", + cron: "0/1 * * * * *", }) ).toThrow( "cron string must be UNIX cron format [minute] [hour] [day of month] [month] [day of week]" @@ -160,3 +268,18 @@ test("schedule with rate less than 1 minute", () => { }) ).toThrow("rate can not be set to less than 1 minute."); }); + +test("cron with day of month and day of week configured at the same time", () => { + // GIVEN + const app = new tfaws.App({ outdir: mkdtemp(), entrypointDir: __dirname }); + + // THEN + expect( + () => + new cloud.Schedule(app, "Schedule", { + cron: "* * 1 * 1", + }) + ).toThrow( + "Cannot restrict both 'day-of-month' and 'day-of-week' in a cron expression, at least one must be '*'" + ); +}); diff --git a/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md index ef9c211713d..c71573ca178 100644 --- a/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/sdk_tests/schedule/on_tick.test.w_compile_tf-aws.md @@ -25,7 +25,6 @@ "uniqueId": "from_cron_Schedule_6C1613E8" } }, - "is_enabled": true, "schedule_expression": "cron(* * * * ? *)" }, "from_rate_Schedule_5B82E706": { @@ -35,7 +34,6 @@ "uniqueId": "from_rate_Schedule_5B82E706" } }, - "is_enabled": true, "schedule_expression": "rate(1 minute)" } }, From a46220d1db114f56ead44d0c3cd70e201d8d0b4f Mon Sep 17 00:00:00 2001 From: Mark McCulloh Date: Mon, 18 Mar 2024 14:48:30 -0400 Subject: [PATCH 2/6] fix: react-vite template fails to `npm install` on Windows (#5982) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seems like a bug on Windows where using `--prefix` causes npm to use the wrong postinstall hook 🤷 *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)*. --- apps/wing/project-templates/wing/react-vite/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/wing/project-templates/wing/react-vite/package.json b/apps/wing/project-templates/wing/react-vite/package.json index 8892b2dd0b4..c3f0f57a50b 100644 --- a/apps/wing/project-templates/wing/react-vite/package.json +++ b/apps/wing/project-templates/wing/react-vite/package.json @@ -5,8 +5,8 @@ "author": "Your Name", "license": "MIT", "scripts": { - "install:backend": "npm install --prefix backend", - "install:frontend": "npm install --prefix frontend", + "install:backend": "cd backend && npm install", + "install:frontend": "cd frontend && npm install", "postinstall": "npm run install:backend && npm run install:frontend" }, "dependencies": { From ed54e0cd92ba1b9325e1e96f39c4b8babfbe9fa5 Mon Sep 17 00:00:00 2001 From: Mark McCulloh Date: Mon, 18 Mar 2024 23:00:54 -0400 Subject: [PATCH 3/6] feat(vscode): improved wing debugging and docs (breakpoints) (#5981) image To support this, a change was made to the simulator to bundle inflights next to the entrypoint. This helps VSCode (and other debugger probably) to discover the file that's running. *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)*. --- apps/vscode-wing/.projenrc.ts | 1 + apps/vscode-wing/package.json | 5 +++ docs/docs/06-tools/03-debugging.md | 31 +++++++++++++++++++ libs/wingsdk/src/shared/legacy-sandbox.ts | 7 ++--- libs/wingsdk/src/shared/sandbox.ts | 28 ++++++++--------- libs/wingsdk/test/simulator/simulator.test.ts | 21 +++++++++++-- libs/wingsdk/test/util.ts | 12 +++++-- 7 files changed, 81 insertions(+), 24 deletions(-) create mode 100644 docs/docs/06-tools/03-debugging.md diff --git a/apps/vscode-wing/.projenrc.ts b/apps/vscode-wing/.projenrc.ts index 03d9b167440..2907435a702 100644 --- a/apps/vscode-wing/.projenrc.ts +++ b/apps/vscode-wing/.projenrc.ts @@ -97,6 +97,7 @@ vscodeIgnore.addPatterns( ); const contributes: VSCodeExtensionContributions = { + breakpoints: [{ language: "wing" }], languages: [ { id: "wing", diff --git a/apps/vscode-wing/package.json b/apps/vscode-wing/package.json index b16e8855c76..db78d4e1169 100644 --- a/apps/vscode-wing/package.json +++ b/apps/vscode-wing/package.json @@ -89,6 +89,11 @@ "onLanguage:wing" ], "contributes": { + "breakpoints": [ + { + "language": "wing" + } + ], "languages": [ { "id": "wing", diff --git a/docs/docs/06-tools/03-debugging.md b/docs/docs/06-tools/03-debugging.md new file mode 100644 index 00000000000..7b4433efcf7 --- /dev/null +++ b/docs/docs/06-tools/03-debugging.md @@ -0,0 +1,31 @@ +--- +title: Debugging +id: Debugging +description: Learn how to debug your Wing application +keywords: [debugging, debug, test, vscode] +--- + +## Overview + +Internally Wing uses JavaScript to execute preflight and inflight code, so standard JavaScript debugging tools can be used to debug your Wing application. The best-supported debugger is the built-in VS Code one so this guide will focus on that. + +### Local/Simulator Debugging + +To start, open your .w file in VS Code and set a breakpoint by clicking in the gutter to the left of the line number. Breakpoints can also be set in extern files. There are several ways to start the debugger, but let's use the "JavaScript Debug Terminal". +Open the command palette and type "Debug: Open JavaScript Debug Terminal". This works for any wing commands like `wing test` and `wing it`, although keep in mind that `wing compile` will only debug preflight code. + +### Limitations + +- ([Issue](https://github.com/winglang/wing/issues/5988)) When using the Wing Console (`wing it`) and attempting to debug inflight code in a `test` or Function, the first execution of the test will not hit a breakpoint and will need to be run again +- ([Issue](https://github.com/winglang/wing/issues/5986)) inflight code by default has a timeout that continues during debugging, so if execution is paused for too long the program is terminate +- Caught/Unhandled will often not stop at expected places + +#### Non-VSCode Support + +The Wing CLI itself is a Node.js application, so you can use the `--inspect` flag to debug it and expose a debug server. + +```bash +node --inspect $(which wing) +``` + +Note that inflight code will be executed among multiple child processes, so it's recommended to use a debugger that supports automatically attaching to child processes. diff --git a/libs/wingsdk/src/shared/legacy-sandbox.ts b/libs/wingsdk/src/shared/legacy-sandbox.ts index f7f3dcadaf8..b27c7d0573f 100644 --- a/libs/wingsdk/src/shared/legacy-sandbox.ts +++ b/libs/wingsdk/src/shared/legacy-sandbox.ts @@ -1,6 +1,4 @@ -import { mkdtemp, readFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import path from "node:path"; +import { readFile } from "node:fs/promises"; import * as util from "node:util"; import * as vm from "node:vm"; import { createBundle } from "./bundling"; @@ -90,8 +88,7 @@ export class LegacySandbox { private async createBundle() { // load bundle into context on first run - const workdir = await mkdtemp(path.join(tmpdir(), "wing-bundles-")); - const bundle = createBundle(this.entrypoint, [], workdir); + const bundle = createBundle(this.entrypoint); this.entrypoint = bundle.entrypointPath; this.code = await readFile(this.entrypoint, "utf-8"); diff --git a/libs/wingsdk/src/shared/sandbox.ts b/libs/wingsdk/src/shared/sandbox.ts index 4d73c7ce1be..820b48a37df 100644 --- a/libs/wingsdk/src/shared/sandbox.ts +++ b/libs/wingsdk/src/shared/sandbox.ts @@ -1,8 +1,7 @@ import * as cp from "child_process"; import { writeFileSync } from "fs"; -import { mkdtemp, readFile, stat } from "fs/promises"; -import { tmpdir } from "os"; -import path from "path"; +import { readFile, stat } from "fs/promises"; +import { url as inspectorUrl } from "inspector"; import { Bundle, createBundle } from "./bundling"; import { processStream } from "./stream-processor"; @@ -39,9 +38,7 @@ export class Sandbox { entrypoint: string, log?: (message: string) => void ): Promise { - const workdir = await mkdtemp(path.join(tmpdir(), "wing-bundles-")); - - let contents = (await readFile(entrypoint)).toString(); + let contents = await readFile(entrypoint, "utf-8"); // log a warning if contents includes __dirname or __filename if (contents.includes("__dirname") || contents.includes("__filename")) { @@ -52,9 +49,7 @@ export class Sandbox { // wrap contents with a shim that handles the communication with the parent process // we insert this shim before bundling to ensure source maps are generated correctly - contents = ` -"use strict"; -${contents} + contents += ` process.on("message", async (message) => { const { fn, args } = message; try { @@ -67,7 +62,7 @@ process.on("message", async (message) => { `; const wrappedPath = entrypoint.replace(/\.js$/, ".sandbox.js"); writeFileSync(wrappedPath, contents); // async fsPromises.writeFile "flush" option is not available in Node 20 - const bundle = createBundle(wrappedPath, [], workdir); + const bundle = createBundle(wrappedPath); if (process.env.DEBUG) { const fileStats = await stat(entrypoint); @@ -118,14 +113,19 @@ process.on("message", async (message) => { public async initialize() { this.debugLog("Initializing sandbox."); const childEnv = this.options.env ?? {}; - if ( - process.env.NODE_OPTIONS?.includes("--inspect") || - process.execArgv.some((a) => a.startsWith("--inspect")) - ) { + if (inspectorUrl?.()) { // We're exposing a debugger, let's attempt to ensure the child process automatically attaches childEnv.NODE_OPTIONS = (childEnv.NODE_OPTIONS ?? "") + (process.env.NODE_OPTIONS ?? ""); + // If the child process is not already configured to attach a debugger, add a flag to do so + if ( + !childEnv.NODE_OPTIONS.includes("--inspect") && + !process.execArgv.includes("--inspect") + ) { + childEnv.NODE_OPTIONS += " --inspect=0"; + } + // VSCode's debugger adds some environment variables that we want to pass to the child process for (const key in process.env) { if (key.startsWith("VSCODE_")) { diff --git a/libs/wingsdk/test/simulator/simulator.test.ts b/libs/wingsdk/test/simulator/simulator.test.ts index dd494bf6627..89c8236c84e 100644 --- a/libs/wingsdk/test/simulator/simulator.test.ts +++ b/libs/wingsdk/test/simulator/simulator.test.ts @@ -1,14 +1,13 @@ import * as fs from "fs"; +import * as inspector from "inspector"; import { Construct } from "constructs"; import { test, expect, describe } from "vitest"; import { Api, Bucket, Function, - IApiClient, IBucketClient, IFunctionClient, - IServiceClient, OnDeploy, Service, } from "../../src/cloud"; @@ -593,6 +592,24 @@ describe("in-place updates", () => { "root/OnDeploy started", ]); }); + + test("debugging inspector inherited by sandbox", async () => { + const app = new SimApp(); + const handler = Testing.makeHandler( + `async handle() { if(require('inspector').url() === undefined) { throw new Error('inspector not available'); } }` + ); + new OnDeploy(app, "OnDeploy", handler); + + inspector.open(0); + const sim = await app.startSimulator(); + await sim.stop(); + + expect( + sim + .listTraces() + .some((t) => t.data.message.startsWith("Debugger listening on ")) + ); + }); }); test("tryGetResource returns undefined if the resource not found", async () => { diff --git a/libs/wingsdk/test/util.ts b/libs/wingsdk/test/util.ts index eb211185981..1de711dab9e 100644 --- a/libs/wingsdk/test/util.ts +++ b/libs/wingsdk/test/util.ts @@ -130,6 +130,15 @@ export function directorySnapshot(initialRoot: string) { if (f === "node_modules") { continue; } + // skip sandbox entrypoints since they are mostly a duplicate of the original + if (f.endsWith(".sandbox.js")) { + continue; + } + // skip esbuild output + if (f.endsWith(".js.bundle")) { + continue; + } + const relpath = join(subdir, f); const abspath = join(root, relpath); const key = prefix + relpath; @@ -149,9 +158,6 @@ export function directorySnapshot(initialRoot: string) { break; case ".js": - if (f.endsWith(".sandbox.js")) { - continue; - } const code = readFileSync(abspath, "utf-8"); snapshot[key] = sanitizeCode(code); break; From d55ea52140cd32dccc5e52d5221297196a53dd78 Mon Sep 17 00:00:00 2001 From: yoav-steinberg Date: Tue, 19 Mar 2024 09:44:41 +0200 Subject: [PATCH 4/6] feat(compiler): allow explicit lift qualifications of preflight objects (#5935) Fixes: #76 Creates a `lift` builtin function that can be used in inflight code to explicitly add lift qualifications to a method: ```wing bring cloud; let bucket = new cloud.Bucket(); bucket.addObject("k", "value"); let some_ops = ["put", "list"]; // We can define a list of ops in preflight code to be used when explicitly qualifying a lift class Foo { pub inflight mehtod() { lift(bucket, some_ops); // Explicitly add some permissions to `bucket` using a preflight expression lift(bucket, ["delete"]); // Add more permissions to bucket using a literal log(bucket.get("k")); // Good old implicit qualification adds `get` permissions let b = bucket; // We can now use an inflight variable `b` to reference a preflight object `bucket` b.put("k2", "value2"); // We don't get a compiler error here, because explicit lifts are being used in the method disabling compiler qualification errors for k in b.list() { // `list` works on `bucket` because of explicit qualification and `b` references `bucket` log(k); } b.delete("k2"); // `delete` also works because of explicit qualification assert(bucket.tryGet("k2") == nil); `yay!` } } let foo = new Foo(); test "a test" { foo.mehtod(); } ``` ## 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)*. --- .../02-concepts/01-preflight-and-inflight.md | 59 ++++ docs/docs/03-language-reference.md | 1 + .../explicit_lift_qualification.test.w | 40 +++ .../valid/explicit_lift_qualification.test.w | 43 +++ libs/wingc/src/ast.rs | 31 -- libs/wingc/src/jsify.rs | 11 +- .../snapshots/base_class_lift_indirect.snap | 2 +- .../calls_methods_on_preflight_object.snap | 2 +- .../snapshots/fail_unqualified_lift.snap | 2 +- .../fail_unqualified_lift_as_arg.snap | 2 +- ...ft_element_from_collection_of_objects.snap | 2 +- .../fail_unqualified_lift_return.snap | 2 +- .../src/jsify/snapshots/preflight_object.snap | 2 +- .../preflight_object_with_operations.snap | 2 +- ...ce_preflight_field_from_inflight_expr.snap | 2 +- .../snapshots/reference_preflight_fields.snap | 2 +- libs/wingc/src/lifting.rs | 260 ++++++++++++--- .../src/lsp/snapshots/completions/empty.snap | 12 + .../hide_parent_symbols_defined_later.snap | 12 + libs/wingc/src/type_check.rs | 69 +++- libs/wingc/src/type_check/lifts.rs | 43 ++- .../__snapshots__/compatibility-spy.ts.snap | 8 +- tools/hangar/__snapshots__/invalid.ts.snap | 103 +++++- .../bucket_keys.test.w_compile_tf-aws.md | 2 +- ...inflight_variants.test.w_compile_tf-aws.md | 2 +- ...capture_in_binary.test.w_compile_tf-aws.md | 2 +- ...gable_class_field.test.w_compile_tf-aws.md | 2 +- ...resource_and_data.test.w_compile_tf-aws.md | 2 +- .../valid/captures.test.w_compile_tf-aws.md | 4 +- .../valid/class.test.w_compile_tf-aws.md | 2 +- .../closure_class.test.w_compile_tf-aws.md | 2 +- .../valid/debug_env.test.w_test_sim.md | 2 +- ...ift_qualification.test.w_compile_tf-aws.md | 303 ++++++++++++++++++ ...icit_lift_qualification.test.w_test_sim.md | 13 + ...rn_implementation.test.w_compile_tf-aws.md | 2 +- ..._inflight_closure.test.w_compile_tf-aws.md | 4 +- ...handler_singleton.test.w_compile_tf-aws.md | 4 +- .../inflight_init.test.w_compile_tf-aws.md | 2 +- ...ce_class_inflight.test.w_compile_tf-aws.md | 2 +- .../lift_via_closure.test.w_compile_tf-aws.md | 2 +- .../valid/nil.test.w_compile_tf-aws.md | 2 +- ..._method_on_string.test.w_compile_tf-aws.md | 2 +- .../valid/redis.test.w_compile_tf-aws.md | 2 +- .../valid/resource.test.w_compile_tf-aws.md | 10 +- ...resource_captures.test.w_compile_tf-aws.md | 8 +- ..._captures_globals.test.w_compile_tf-aws.md | 2 +- .../valid/std_string.test.w_compile_tf-aws.md | 2 +- .../test_bucket.test.w_compile_tf-aws.md | 4 +- 48 files changed, 932 insertions(+), 164 deletions(-) create mode 100644 examples/tests/invalid/explicit_lift_qualification.test.w create mode 100644 examples/tests/valid/explicit_lift_qualification.test.w create mode 100644 tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_compile_tf-aws.md create mode 100644 tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_test_sim.md diff --git a/docs/docs/02-concepts/01-preflight-and-inflight.md b/docs/docs/02-concepts/01-preflight-and-inflight.md index f857da0da82..c3208888995 100644 --- a/docs/docs/02-concepts/01-preflight-and-inflight.md +++ b/docs/docs/02-concepts/01-preflight-and-inflight.md @@ -268,6 +268,65 @@ inflight () => { }; ``` +### Lift qualification + +Preflight objects referenced inflight are called "lifted" objects: + +```js playground +let preflight_str = "hello from preflight"; +inflight () => { + log(preflight_str); // `preflight_str` is "lifted" into inflight. +}; +``` + +During the lifting process the compiler tries to figure out in what way the lifted objects are being used. +This is how Winglang generats least privilage permissions. Consider the case of lifting a [`cloud.Bucket`](../04-standard-library/cloud/bucket.md) object: + +```js playground +bring cloud; +let bucket = new cloud.Bucket(); +new cloud.Function(inflight () => { + bucket.put("key", "value"); // `bucket` is lifted and `put` is being used on it +}); +``` + +In this example the compiler generates the correct _write_ access permissions for the [`cloud.Function`](../04-standard-library/cloud/function.md) on `bucket` based on the fact we're `put`ing into it. We say `bucket`'s lift is qualified with `put`. + +#### Explicit lift qualification +In some cases the compiler can't figure out (yet) the lift qualifications, and therefore will report an error: + +```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 () => { + let var b = main_bucket; + if !use_main { + b = secondary_bucket; + } + b.put("key", "value"); // Error: the compiler doesn't know the possible values for `b` and therefore can't qualify the lift. +}); +``` + +To explicitly qualify lifts in an inflight closure or inflight method and supress the above compiler error use the `lift()` utility function: + +```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" +}); +``` + ## Phase-independent code The global functions `log`, `assert`, and `throw` can all be used in both preflight and inflight code. diff --git a/docs/docs/03-language-reference.md b/docs/docs/03-language-reference.md index a90d6681e46..67f81559896 100644 --- a/docs/docs/03-language-reference.md +++ b/docs/docs/03-language-reference.md @@ -553,6 +553,7 @@ log("UTC: {t1.utc.toIso())}"); // output: 2023-02-09T06:21:03.000Z | `assert` | checks a condition and _throws_ if evaluated to false | | `unsafeCast` | cast a value into a different type | | `nodeof` | obtain the [tree node](./02-concepts/02-application-tree.md) of a preflight object | +| `lift` | explicitly qualify a [lift](./02-concepts/01-preflight-and-inflight.md#explicit-lift-qualification) of a preflight object | > ```TS > log("Hello {name}"); diff --git a/examples/tests/invalid/explicit_lift_qualification.test.w b/examples/tests/invalid/explicit_lift_qualification.test.w new file mode 100644 index 00000000000..e7f1a44d383 --- /dev/null +++ b/examples/tests/invalid/explicit_lift_qualification.test.w @@ -0,0 +1,40 @@ +bring cloud; + +let bucket = new cloud.Bucket(); + +let prelight_string = "hi"; + +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 + } + } + } + + 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 + } +} diff --git a/examples/tests/valid/explicit_lift_qualification.test.w b/examples/tests/valid/explicit_lift_qualification.test.w new file mode 100644 index 00000000000..4089d6b6953 --- /dev/null +++ b/examples/tests/valid/explicit_lift_qualification.test.w @@ -0,0 +1,43 @@ +bring cloud; + +let bucket = new cloud.Bucket(); +bucket.addObject("k", "value"); + +let put_and_list = ["put", "list"]; + +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); + } +} + +let foo = new Foo(); + +test "explicit method lift qualification" { + foo.mehtod(); +} + +// 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"); +}; + +test "explicit closure lift qualification" { + inflight_closure(); +} diff --git a/libs/wingc/src/ast.rs b/libs/wingc/src/ast.rs index 6f45c3044a1..8487093d516 100644 --- a/libs/wingc/src/ast.rs +++ b/libs/wingc/src/ast.rs @@ -304,37 +304,6 @@ pub struct Stmt { pub idx: usize, } -#[derive(Debug)] -pub enum UtilityFunctions { - Log, - Assert, - UnsafeCast, - Nodeof, -} - -impl UtilityFunctions { - /// Returns all utility functions. - pub fn all() -> Vec { - vec![ - UtilityFunctions::Log, - UtilityFunctions::Assert, - UtilityFunctions::UnsafeCast, - UtilityFunctions::Nodeof, - ] - } -} - -impl Display for UtilityFunctions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - UtilityFunctions::Log => write!(f, "log"), - UtilityFunctions::Assert => write!(f, "assert"), - UtilityFunctions::UnsafeCast => write!(f, "unsafeCast"), - UtilityFunctions::Nodeof => write!(f, "nodeof"), - } - } -} - #[derive(Debug)] pub struct ElifBlock { pub condition: Expr, diff --git a/libs/wingc/src/jsify.rs b/libs/wingc/src/jsify.rs index 16406647d00..d60ae2aec7e 100644 --- a/libs/wingc/src/jsify.rs +++ b/libs/wingc/src/jsify.rs @@ -1855,8 +1855,15 @@ impl<'a> JSifier<'a> { for (method_name, method_qual) in lift_qualifications { bind_method.open(format!("\"{method_name}\": [",)); for (code, method_lift_qual) in method_qual { - let ops_strings = method_lift_qual.ops.iter().map(|op| format!("\"{}\"", op)).join(", "); - bind_method.line(format!("[{code}, [{ops_strings}]],",)); + let ops = method_lift_qual.ops.iter().join(", "); + // To keep the code concise treat no ops, single op and multiple ops differenly here, although the multiple ops is the generic case + if method_lift_qual.ops.len() == 0 { + bind_method.line(format!("[{code}, []],")); + } else if method_lift_qual.ops.len() == 1 { + bind_method.line(format!("[{code}, {ops}],")); + } else { + bind_method.line(format!("[{code}, [].concat({ops})],")); + } } bind_method.close("],"); } diff --git a/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap b/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap index f78004d09d4..649c199d1e0 100644 --- a/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap +++ b/libs/wingc/src/jsify/snapshots/base_class_lift_indirect.snap @@ -106,7 +106,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "put": [ - [this.b, ["list", "put"]], + [this.b, [].concat(["put"], ["list"])], ], "$inflight_init": [ [this.b, []], diff --git a/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap b/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap index 53f3b82011e..0f0f4e5b970 100644 --- a/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap +++ b/libs/wingc/src/jsify/snapshots/calls_methods_on_preflight_object.snap @@ -78,7 +78,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [b, ["list", "put"]], + [b, [].concat(["put"], ["list"])], ], "$inflight_init": [ [b, []], diff --git a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift.snap b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift.snap index ef45ec54a2a..d663f71bbfd 100644 --- a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift.snap +++ b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift.snap @@ -2,4 +2,4 @@ source: libs/wingc/src/jsify/tests.rs --- ## Errors -Expression of type "Queue" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) 7:6 +Expression of type "Queue" references an unknown preflight object, can't qualify its capabilities. Use `lift()` to explicitly qualify the preflight object to disable this error. 7:6 diff --git a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_as_arg.snap b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_as_arg.snap index 1115197e304..2ce6bfcd445 100644 --- a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_as_arg.snap +++ b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_as_arg.snap @@ -2,4 +2,4 @@ source: libs/wingc/src/jsify/tests.rs --- ## Errors -Expression of type "Queue" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) 6:6 +Expression of type "Queue" references an unknown preflight object, can't qualify its capabilities. Use `lift()` to explicitly qualify the preflight object to disable this error. 6:6 diff --git a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_element_from_collection_of_objects.snap b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_element_from_collection_of_objects.snap index 66138dd01b6..55ca5b933c7 100644 --- a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_element_from_collection_of_objects.snap +++ b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_element_from_collection_of_objects.snap @@ -2,4 +2,4 @@ source: libs/wingc/src/jsify/tests.rs --- ## Errors -Expression of type "Bucket" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) 6:6 +Expression of type "Bucket" references an unknown preflight object, can't qualify its capabilities. Use `lift()` to explicitly qualify the preflight object to disable this error. 6:6 diff --git a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_return.snap b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_return.snap index d700f7e5a34..a3165260454 100644 --- a/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_return.snap +++ b/libs/wingc/src/jsify/snapshots/fail_unqualified_lift_return.snap @@ -2,4 +2,4 @@ source: libs/wingc/src/jsify/tests.rs --- ## Errors -Expression of type "Queue" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) 11:6 +Expression of type "Queue" references an unknown preflight object, can't qualify its capabilities. Use `lift()` to explicitly qualify the preflight object to disable this error. 11:6 diff --git a/libs/wingc/src/jsify/snapshots/preflight_object.snap b/libs/wingc/src/jsify/snapshots/preflight_object.snap index e6d69f3c784..203f9e4fc28 100644 --- a/libs/wingc/src/jsify/snapshots/preflight_object.snap +++ b/libs/wingc/src/jsify/snapshots/preflight_object.snap @@ -130,7 +130,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [pf_obj, ["goodbye", "hello"]], + [pf_obj, [].concat(["hello"], ["goodbye"])], ], "$inflight_init": [ [pf_obj, []], diff --git a/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap b/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap index 2af7944c3cb..78cca399191 100644 --- a/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap +++ b/libs/wingc/src/jsify/snapshots/preflight_object_with_operations.snap @@ -77,7 +77,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [b, ["list", "put"]], + [b, [].concat(["list"], ["put"])], ], "$inflight_init": [ [b, []], diff --git a/libs/wingc/src/jsify/snapshots/reference_preflight_field_from_inflight_expr.snap b/libs/wingc/src/jsify/snapshots/reference_preflight_field_from_inflight_expr.snap index 04c1b4dbdd9..7370ff0727b 100644 --- a/libs/wingc/src/jsify/snapshots/reference_preflight_field_from_inflight_expr.snap +++ b/libs/wingc/src/jsify/snapshots/reference_preflight_field_from_inflight_expr.snap @@ -3,4 +3,4 @@ source: libs/wingc/src/jsify/tests.rs --- ## Errors Can't access preflight member "x" on inflight instance of type "A" 9:14 -Expression of type "A" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) 9:12 +Expression of type "A" references an unknown preflight object, can't qualify its capabilities. Use `lift()` to explicitly qualify the preflight object to disable this error. 9:12 diff --git a/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap b/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap index acf17e637c4..f6356ca41fd 100644 --- a/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap +++ b/libs/wingc/src/jsify/snapshots/reference_preflight_fields.snap @@ -100,7 +100,7 @@ class $Root extends $stdlib.std.Resource { [this.s, ["length"]], ], "bam": [ - [this.b, ["get", "put"]], + [this.b, [].concat(["put"], ["get"])], ], "$inflight_init": [ [this.b, []], diff --git a/libs/wingc/src/lifting.rs b/libs/wingc/src/lifting.rs index cdd3565b524..69af9b1f7ca 100644 --- a/libs/wingc/src/lifting.rs +++ b/libs/wingc/src/lifting.rs @@ -1,6 +1,7 @@ use crate::{ ast::{ - Class, Expr, ExprKind, FunctionBody, FunctionDefinition, Phase, Reference, Scope, Stmt, Symbol, UserDefinedType, + ArgList, CalleeKind, Class, Expr, ExprKind, FunctionBody, FunctionDefinition, Phase, Reference, Scope, Stmt, + StmtKind, Symbol, UserDefinedType, }, comp_ctx::{CompilationContext, CompilationPhase}, diagnostic::{report_diagnostic, Diagnostic}, @@ -9,7 +10,7 @@ use crate::{ lifts::{Liftable, Lifts}, resolve_user_defined_type, symbol_env::LookupResult, - ClassLike, ResolveSource, SymbolKind, TypeRef, CLOSURE_CLASS_HANDLE_METHOD, + ClassLike, ResolveSource, SymbolKind, TypeRef, UtilityFunctions, CLOSURE_CLASS_HANDLE_METHOD, }, visit::{self, Visit}, visit_context::{VisitContext, VisitorWithContext}, @@ -19,6 +20,7 @@ pub struct LiftVisitor<'a> { ctx: VisitContext, jsify: &'a JSifier<'a>, lifts_stack: Vec, + in_inner_inflight_class: usize, } impl<'a> LiftVisitor<'a> { @@ -27,6 +29,7 @@ impl<'a> LiftVisitor<'a> { jsify: jsifier, ctx: VisitContext::new(), lifts_stack: vec![], + in_inner_inflight_class: 0, } } @@ -151,6 +154,12 @@ impl<'a> LiftVisitor<'a> { udt_js } } + + // Used for generating a js array represtining a lift qualificaiton (an operation done on a lifted preflight object) + // lift qualifcations are in array format so multiple ops can be bunched together in some cases. + fn jsify_symbol_to_op_array(&self, symb: &Symbol) -> String { + format!("[\"{symb}\"]") + } } impl<'a> Visit<'a> for LiftVisitor<'a> { @@ -197,13 +206,16 @@ impl<'a> Visit<'a> for LiftVisitor<'a> { return; } + // If the expression is a call to the `lift` builtin, use this opportunity to qualify the lift + v.check_explicit_lift(node); + // Inflight expressions that evaluate to a preflight type are currently unsupported because // we can't determine exactly which preflight object is being accessed and therefore can't // qualify the original lift expression. - if expr_phase == Phase::Inflight && expr_type.is_preflight_class() && v.ctx.current_property().is_some() { + if expr_phase == Phase::Inflight && expr_type.is_preflight_class() && v.ctx.current_property().is_some() && !v.ignore_unknown_preflight_object_error() { report_diagnostic(Diagnostic { message: format!( - "Expression of type \"{expr_type}\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details)" + "Expression of type \"{expr_type}\" references an unknown preflight object, can't qualify its capabilities. Use `lift()` to explicitly qualify the preflight object to disable this error." ), span: Some(node.span.clone()), annotations: vec![], @@ -214,9 +226,7 @@ impl<'a> Visit<'a> for LiftVisitor<'a> { //--------------- // LIFT if expr_phase == Phase::Preflight { - // jsify the expression so we can get the preflight code - let code = v.jsify_expr(&node); - + // Get the property being accessed on the preflight expression, this is used to qualify the lift let property = if let Some(property) = v.ctx.current_property() { Some(property) } else if expr_type.is_closure() { @@ -233,9 +243,17 @@ impl<'a> Visit<'a> for LiftVisitor<'a> { return; } + // jsify the expression so we can get the preflight code + let code = v.jsify_expr(&node); + let mut lifts = v.lifts_stack.pop().unwrap(); let is_field = code.contains("this."); // TODO: starts_with? - lifts.lift(v.ctx.current_method().map(|(m,_)|m), property, &code); + lifts.lift( + v.ctx.current_method().map(|(m,_)|m).expect("a method"), + property.map(|p| v.jsify_symbol_to_op_array(&p)), + &code, + false + ); lifts.capture(&Liftable::Expr(node.id), &code, is_field); v.lifts_stack.push(lifts); return; @@ -295,7 +313,12 @@ impl<'a> Visit<'a> for LiftVisitor<'a> { } let mut lifts = self.lifts_stack.pop().unwrap(); - lifts.lift(self.ctx.current_method().map(|(m, _)| m), property, &code); + lifts.lift( + self.ctx.current_method().map(|(m, _)| m).expect("a method"), + property.map(|p| self.jsify_symbol_to_op_array(&p)), + &code, + false, + ); self.lifts_stack.push(lifts); } @@ -320,66 +343,108 @@ impl<'a> Visit<'a> for LiftVisitor<'a> { fn visit_function_definition(&mut self, node: &'a FunctionDefinition) { match &node.body { FunctionBody::Statements(scope) => { - self.ctx.push_function_definition( - node.name.as_ref(), - &node.signature, - node.is_static, - self.jsify.types.get_scope_env(&scope), - ); + // If this is a method (of a non-inner inflight class), make sure there are no `lift()` calls that aren't at the top of the method + if node.name.is_some() && self.in_inner_inflight_class == 0 { + // Skip all statments that are a lift call and then search to see if there are further lift calls + let stmts = scope.statements.iter(); + let lift_stmts = stmts + .skip_while(|s| { + if let StmtKind::Expression(expr) = &s.kind { + return Self::is_lift_builtin_call(expr).is_some(); + } + false + }) + .filter( + // If we find a lift call after the first non-lift call statement, report an error + |s| { + if let StmtKind::Expression(expr) = &s.kind { + return Self::is_lift_builtin_call(expr).is_some(); + } + false + }, + ); + + for lift_stmt in lift_stmts { + report_diagnostic(Diagnostic { + span: Some(lift_stmt.span.clone()), + message: "lift() calls must be at the top of the method".to_string(), + annotations: vec![], + hints: vec![], + }); + } + } else { + // This isn't a method, don't allow any lift statments + let lift_stmts = scope.statements.iter().filter(|s| { + if let StmtKind::Expression(expr) = &s.kind { + return Self::is_lift_builtin_call(expr).is_some(); + } + false + }); + for lift_stmt in lift_stmts { + report_diagnostic(Diagnostic { + span: Some(lift_stmt.span.clone()), + message: "lift() calls are only allowed in inflight methods and closures defined in preflight" + .to_string(), + annotations: vec![], + hints: vec![], + }); + } + } + + // If we're in an inner inflight class then we don't need to track this inner method since lifts are + // collected for methods of classes defined preflight. + if self.in_inner_inflight_class == 0 { + self.ctx.push_function_definition( + node.name.as_ref(), + &node.signature, + node.is_static, + self.jsify.types.get_scope_env(&scope), + ); + } visit::visit_function_definition(self, node); - self.ctx.pop_function_definition(); + + if self.in_inner_inflight_class == 0 { + self.ctx.pop_function_definition(); + } } FunctionBody::External(_) => visit::visit_function_definition(self, node), } } fn visit_class(&mut self, node: &'a Class) { - // nothing to do if we are emitting an inflight class from within an inflight scope - if self.ctx.current_phase() == Phase::Inflight && node.phase == Phase::Inflight { - self.visit_symbol(&node.name); + let in_inner_inflight_class = self.ctx.current_phase() == Phase::Inflight && node.phase == Phase::Inflight; + if in_inner_inflight_class { + // nothing to do if we are emitting an inflight class from within an inflight scope: + // inner inflight classes collect lifts in their outer class, just mark we're in such a class and do nothing + self.in_inner_inflight_class += 1; + } else { + self.ctx.push_class(node); - visit::visit_function_definition(self, &node.initializer); - visit::visit_function_definition(self, &node.inflight_initializer); + self.lifts_stack.push(Lifts::new()); - for field in node.fields.iter() { - self.visit_symbol(&field.name); - self.visit_type_annotation(&field.member_type); - } - for (name, def) in node.methods.iter() { - self.visit_symbol(&name); - visit::visit_function_definition(self, &def); - } if let Some(parent) = &node.parent { - self.visit_user_defined_type(&parent); + let mut lifts = self.lifts_stack.pop().unwrap(); + lifts.capture(&Liftable::Type(parent.clone()), &self.jsify_udt(&parent), false); + self.lifts_stack.push(lifts); } - for interface in node.implements.iter() { - self.visit_user_defined_type(&interface); - } - return; - } - - self.ctx.push_class(node); - - self.lifts_stack.push(Lifts::new()); - - if let Some(parent) = &node.parent { - let mut lifts = self.lifts_stack.pop().unwrap(); - lifts.capture(&Liftable::Type(parent.clone()), &self.jsify_udt(&parent), false); - self.lifts_stack.push(lifts); } visit::visit_class(self, node); - self.ctx.pop_class(); - - let lifts = self.lifts_stack.pop().expect("Unable to pop class tokens"); + if in_inner_inflight_class { + self.in_inner_inflight_class -= 1; + } else { + let lifts = self.lifts_stack.pop().expect("Unable to pop class tokens"); - if let Some(env) = self.ctx.current_env() { - if let Some(mut t) = resolve_user_defined_type(&UserDefinedType::for_class(node), env, 0).ok() { - let mut_class = t.as_class_mut().unwrap(); - mut_class.set_lifts(lifts); + if let Some(env) = self.ctx.current_env() { + if let Some(mut t) = resolve_user_defined_type(&UserDefinedType::for_class(node), env, 0).ok() { + let mut_class = t.as_class_mut().unwrap(); + mut_class.set_lifts(lifts); + } } + + self.ctx.pop_class(); } } @@ -398,6 +463,99 @@ impl<'a> Visit<'a> for LiftVisitor<'a> { } } +impl LiftVisitor<'_> { + /// Helper function to check if the current method has explicit lifts. If it does then ignore + /// inflight expressions that reference unknown preflight objects assuming that the explicit lifts + /// qualify the capabilities of the preflight objects correctly. + fn ignore_unknown_preflight_object_error(&mut self) -> bool { + let lifts = self.lifts_stack.pop().expect("lifts"); + let current_method = self.ctx.current_method().map(|(m, _)| m).expect("a method"); + let res = lifts.has_explicit_lifts(¤t_method.name); + self.lifts_stack.push(lifts); + res + } + + fn is_lift_builtin_call(node: &Expr) -> Option<&ArgList> { + if let ExprKind::Call { + callee: CalleeKind::Expr(callee_expr), + arg_list, + } = &node.kind + { + if let ExprKind::Reference(Reference::Identifier(Symbol { name, .. })) = &callee_expr.kind { + if UtilityFunctions::Lift.to_string().eq(name) { + return Some(arg_list); + } + } + } + + None + } + + /// Helper function to check if the given expression is a call to the `lift` builtin. + /// If it is, we'll qualify the passed preflight object based on the passed capabilities. + fn check_explicit_lift(&mut self, node: &Expr) { + let Some(arg_list) = Self::is_lift_builtin_call(node) else { + return; + }; + + // Get the preflight object's expression, which is the first argument to the `lift` call + let preflight_object_expr = &arg_list.pos_args[0]; + + // Make sure this really is a preflight expression + let obj_phase = self + .jsify + .types + .get_expr_phase(preflight_object_expr) + .expect("an expr phase"); + if obj_phase != Phase::Preflight { + report_diagnostic(Diagnostic { + span: Some(preflight_object_expr.span.clone()), + message: format!( + "Expected a preflight object as first argument to `lift` builtin, found {obj_phase} expression instead" + ), + annotations: vec![], + hints: vec![], + }); + return; + } + + // Make sure the second argument, the qualifications, isn't an inflight expression since we'll need to evaluate it preflihgt + let qualifications_expr = &arg_list.pos_args[1]; + let qualifications_phase = self + .jsify + .types + .get_expr_phase(qualifications_expr) + .expect("an expr phase"); + if qualifications_phase == Phase::Inflight { + report_diagnostic(Diagnostic { + span: Some(qualifications_expr.span.clone()), + message: "Qualification list must not contain any inflight elements".to_string(), + annotations: vec![], + hints: vec![], + }); + return; + } + + // This seems like a valid explicit lift qualification, add it + + // jsify the expression so we can get the preflight code + let code = self.jsify_expr(&preflight_object_expr); + + // jsify the explicit lift qualifications + let qualification_code = self.jsify_expr(qualifications_expr); + + let mut lifts = self.lifts_stack.pop().unwrap(); + + lifts.lift( + self.ctx.current_method().map(|(m, _)| m).expect("a method"), + Some(qualification_code), + &code, + true, + ); + self.lifts_stack.push(lifts); + } +} + /// Check if an expression is a reference to an inflight field (`this.`). /// in this case, we don't need to lift the field because it is already available fn is_inflight_field(expr: &Expr, expr_type: TypeRef, property: &Option) -> bool { diff --git a/libs/wingc/src/lsp/snapshots/completions/empty.snap b/libs/wingc/src/lsp/snapshots/completions/empty.snap index 0efb78a34f6..94861948cb9 100644 --- a/libs/wingc/src/lsp/snapshots/completions/empty.snap +++ b/libs/wingc/src/lsp/snapshots/completions/empty.snap @@ -20,6 +20,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: lift + kind: 3 + detail: "inflight (preflightObject: Resource, qualifications: Array): void" + documentation: + kind: markdown + value: "```wing\nlift: inflight (preflightObject: Resource, qualifications: Array): void\n```\n---\nExplicitly apply qualifications to a preflight object used in the current method/function\n### Parameters\n- `preflightObject` — `Resource` — The preflight object to qualify\n- `qualifications` — `Array` — \n\t\t\t\t\t\t\tThe qualifications to apply to the preflight object.\n\n\t\t\t\t\t\t\tThis is an array of strings denoting members of the object that are accessed in the current method/function.\n\n\t\t\t\t\t\t\tFor example, if the method accesses the `push` and `pop` members of a `cloud.Queue` object, the qualifications should be `[\"push\", \"pop\"]`." + sortText: cc|lift + insertText: lift($1) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: log kind: 3 detail: "(message: str): void" diff --git a/libs/wingc/src/lsp/snapshots/completions/hide_parent_symbols_defined_later.snap b/libs/wingc/src/lsp/snapshots/completions/hide_parent_symbols_defined_later.snap index 549cc206761..65a1305e669 100644 --- a/libs/wingc/src/lsp/snapshots/completions/hide_parent_symbols_defined_later.snap +++ b/libs/wingc/src/lsp/snapshots/completions/hide_parent_symbols_defined_later.snap @@ -27,6 +27,18 @@ source: libs/wingc/src/lsp/completions.rs command: title: triggerParameterHints command: editor.action.triggerParameterHints +- label: lift + kind: 3 + detail: "inflight (preflightObject: Resource, qualifications: Array): void" + documentation: + kind: markdown + value: "```wing\nlift: inflight (preflightObject: Resource, qualifications: Array): void\n```\n---\nExplicitly apply qualifications to a preflight object used in the current method/function\n### Parameters\n- `preflightObject` — `Resource` — The preflight object to qualify\n- `qualifications` — `Array` — \n\t\t\t\t\t\t\tThe qualifications to apply to the preflight object.\n\n\t\t\t\t\t\t\tThis is an array of strings denoting members of the object that are accessed in the current method/function.\n\n\t\t\t\t\t\t\tFor example, if the method accesses the `push` and `pop` members of a `cloud.Queue` object, the qualifications should be `[\"push\", \"pop\"]`." + sortText: cc|lift + insertText: lift($1) + insertTextFormat: 2 + command: + title: triggerParameterHints + command: editor.action.triggerParameterHints - label: log kind: 3 detail: "(message: str): void" diff --git a/libs/wingc/src/type_check.rs b/libs/wingc/src/type_check.rs index 4f7623860b5..b211b79f3c0 100644 --- a/libs/wingc/src/type_check.rs +++ b/libs/wingc/src/type_check.rs @@ -8,7 +8,7 @@ pub(crate) mod type_reference_transform; use crate::ast::{ self, AccessModifier, AssignmentKind, BringSource, CalleeKind, ClassField, ExprId, FunctionDefinition, IfLet, New, - TypeAnnotationKind, UtilityFunctions, + TypeAnnotationKind, }; use crate::ast::{ ArgList, BinaryOperator, Class as AstClass, Elifs, Enum as AstEnum, Expr, ExprKind, FunctionBody, @@ -1740,6 +1740,28 @@ impl Types { } } +/// Enum of builtin functions, this are defined as hard coded AST nodes in `add_builtins` +#[derive(Debug)] +pub enum UtilityFunctions { + Log, + Assert, + UnsafeCast, + Nodeof, + Lift, +} + +impl Display for UtilityFunctions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UtilityFunctions::Log => write!(f, "log"), + UtilityFunctions::Assert => write!(f, "assert"), + UtilityFunctions::UnsafeCast => write!(f, "unsafeCast"), + UtilityFunctions::Nodeof => write!(f, "nodeof"), + UtilityFunctions::Lift => write!(f, "lift"), + } + } +} + pub struct TypeChecker<'a> { types: &'a mut Types, @@ -2021,9 +2043,44 @@ impl<'a> TypeChecker<'a> { }), scope, ); + + let str_array_type = self.types.add_type(Type::Array(self.types.string())); + self.add_builtin( + &UtilityFunctions::Lift.to_string(), + Type::Function(FunctionSignature { + this_type: None, + parameters: vec![ + FunctionParameter { + name: "preflightObject".into(), + typeref: self.types.resource_base_type(), + docs: Docs::with_summary("The preflight object to qualify"), + variadic: false, + }, + FunctionParameter { + name: "qualifications".into(), + typeref: str_array_type, + docs: Docs::with_summary(" + The qualifications to apply to the preflight object.\n + This is an array of strings denoting members of the object that are accessed in the current method/function.\n + For example, if the method accesses the `push` and `pop` members of a `cloud.Queue` object, the qualifications should be `[\"push\", \"pop\"]`." + ), + variadic: false, + }, + ], + return_type: self.types.void(), + phase: Phase::Inflight, + // This builtin actually compiles to nothing in JS, it's a marker that behaves like a function in the type checker + // and is used during the lifting phase to explicitly define lifts for an inflight method + js_override: Some("".to_string()), + docs: Docs::with_summary( + "Explicitly apply qualifications to a preflight object used in the current method/function", + ), + }), + scope, + ) } - pub fn add_builtin(&mut self, name: &str, typ: Type, scope: &mut Scope) { + fn add_builtin(&mut self, name: &str, typ: Type, scope: &mut Scope) { let sym = Symbol::global(name); let mut scope_env = self.types.get_scope_env(&scope); scope_env @@ -2493,9 +2550,11 @@ impl<'a> TypeChecker<'a> { (self.types.add_type(Type::Array(inner_type)), inner_type) }; - // Verify all types are the same as the inferred type + // Verify all types are the same as the inferred type and find the aggregate phase of all the items + let mut phase = Phase::Independent; for item in items { - let (t, _) = self.type_check_exp(item, env); + let (t, item_phase) = self.type_check_exp(item, env); + phase = combine_phases(phase, item_phase); if t.is_json() && !matches!(*element_type, Type::Json(Some(..))) { // This is an array of JSON, change the element type to reflect that @@ -2533,7 +2592,7 @@ impl<'a> TypeChecker<'a> { *inner = element_type; } - (container_type, env.phase) + (container_type, phase) } ExprKind::MapLiteral { fields, type_ } => { // Infer type based on either the explicit type or the value in one of the fields diff --git a/libs/wingc/src/type_check/lifts.rs b/libs/wingc/src/type_check/lifts.rs index e98b84b35d0..6ed6980ebbd 100644 --- a/libs/wingc/src/type_check/lifts.rs +++ b/libs/wingc/src/type_check/lifts.rs @@ -1,4 +1,6 @@ -use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::collections::{BTreeMap, HashMap}; + +use indexmap::IndexSet; use crate::ast::{Symbol, UserDefinedType}; @@ -30,7 +32,9 @@ pub enum Liftable { #[derive(Debug)] pub struct LiftQualification { /// The operations that qualify the lift (the property names) - pub ops: BTreeSet, + pub ops: IndexSet, + /// Whether this lift was explicitly defined via a `lift` statement. + pub explicit: bool, } /// A record that describes a lift from a class. @@ -57,29 +61,46 @@ impl Lifts { } /// Adds a lift for an expression. - pub fn lift(&mut self, method: Option, property: Option, code: &str) { - let method = method.map(|m| m.name).unwrap_or(Default::default()); - - self.add_lift(method, code, property.as_ref().map(|s| s.name.clone())); + pub fn lift(&mut self, method: Symbol, qualification: Option, code: &str, explicit: bool) { + self.add_lift( + method.to_string(), + code, + qualification.as_ref().map(|s| s.clone()), + explicit, + ); // Add a lift to the inflight initializer to signify this class requires access to that preflight object. // "this" is a special case since it's already in scope and doesn't need to be lifted. if code != "this" { - self.add_lift(CLASS_INFLIGHT_INIT_NAME.to_string(), code, None); + self.add_lift(CLASS_INFLIGHT_INIT_NAME.to_string(), code, None, explicit); } } - fn add_lift(&mut self, method: String, code: &str, property: Option) { + fn add_lift(&mut self, method: String, code: &str, qualification: Option, explicit: bool) { let lift = self .lifts_qualifications .entry(method) .or_default() .entry(code.to_string()) - .or_insert(LiftQualification { ops: BTreeSet::new() }); + .or_insert(LiftQualification { + ops: IndexSet::new(), + explicit, + }); - if let Some(op) = &property { - lift.ops.insert(op.clone()); + if let Some(op) = qualification { + lift.ops.insert(op); } + + // If there are explicit lifts in the method, then the lift is explicit. + lift.explicit |= explicit; + } + + pub fn has_explicit_lifts(&self, method: &str) -> bool { + self + .lifts_qualifications + .get(method) + .map(|lifts| lifts.values().any(|lift| lift.explicit)) + .unwrap_or(false) } /// Returns the token for a liftable. Called by the jsifier when emitting inflight code. diff --git a/tools/hangar/__snapshots__/compatibility-spy.ts.snap b/tools/hangar/__snapshots__/compatibility-spy.ts.snap index 30af07fc9ba..434fe516f82 100644 --- a/tools/hangar/__snapshots__/compatibility-spy.ts.snap +++ b/tools/hangar/__snapshots__/compatibility-spy.ts.snap @@ -622,8 +622,8 @@ exports[`peek.test.w 1`] = ` "args": { "methods": { "Counter": [ - "inc", "peek", + "inc", ], "TestRunner": [ "findTests", @@ -693,8 +693,8 @@ exports[`peek.test.w 1`] = ` "args": { "methods": { "Counter": [ - "inc", "peek", + "inc", ], "TestRunner": [ "findTests", @@ -778,8 +778,8 @@ exports[`set.test.w 1`] = ` "args": { "methods": { "Counter": [ - "peek", "set", + "peek", ], "TestRunner": [ "findTests", @@ -868,8 +868,8 @@ exports[`set.test.w 1`] = ` "args": { "methods": { "Counter": [ - "peek", "set", + "peek", ], "TestRunner": [ "findTests", diff --git a/tools/hangar/__snapshots__/invalid.ts.snap b/tools/hangar/__snapshots__/invalid.ts.snap index 70aa1aaa646..da92ba95642 100644 --- a/tools/hangar/__snapshots__/invalid.ts.snap +++ b/tools/hangar/__snapshots__/invalid.ts.snap @@ -1388,6 +1388,77 @@ error: Property not found +Tests 1 failed (1) +Test Files 1 failed (1) +Duration " +`; + +exports[`explicit_lift_qualification.test.w 1`] = ` +"error: Expected type to be \\"Resource\\", but got \\"str\\" instead + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:14:10 + | +14 | lift(prelight_string, [\\"contains\\"]); // Explicit qualification on preflight non-class + | ^^^^^^^^^^^^^^^ Expected type to be \\"Resource\\", but got \\"str\\" instead + + +error: lift() calls must be at the top of the method + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:10:5 + | +10 | lift(b, [\\"put\\"]); // Explicit qualification with inflight object, lift call as non first statement + | ^^^^^^^^^^^^^^^^^ lift() calls must be at the top of the method + + +error: lift() calls must be at the top of the method + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:14:5 + | +14 | lift(prelight_string, [\\"contains\\"]); // Explicit qualification on preflight non-class + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lift() calls must be at the top of the method + + +error: lift() calls must be at the top of the method + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:19:5 + | +19 | lift(bucket, [inflight_qualifier]); // Explicit qualification with inflight qualifiers, lift call as non first statement + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lift() calls must be at the top of the method + + +error: Expected a preflight object as first argument to \`lift\` builtin, found inflight expression instead + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:10:10 + | +10 | 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 + + +error: Qualification list must not contain any inflight elements + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:19:18 + | +19 | lift(bucket, [inflight_qualifier]); // Explicit qualification with inflight qualifiers, lift call as non first statement + | ^^^^^^^^^^^^^^^^^^^^ Qualification list must not contain any inflight elements + + +error: lift() calls are only allowed in inflight methods and closures defined in preflight + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:24:7 + | +24 | lift(bucket, [\\"get\\"]); // lift() call in inner closure + | ^^^^^^^^^^^^^^^^^^^^^^ lift() calls are only allowed in inflight methods and closures defined in preflight + + +error: lift() calls are only allowed in inflight methods and closures defined in preflight + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:29:9 + | +29 | lift(bucket, [\\"get\\"]); // lift() call in inner class + | ^^^^^^^^^^^^^^^^^^^^^^ lift() calls are only allowed in inflight methods and closures defined in preflight + + +error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. + --> ../../../examples/tests/invalid/explicit_lift_qualification.test.w:37:5 + | +37 | b.put(\\"k\\", \\"v\\"); // With no explicit qualification this should be an error + | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. + + + + Tests 1 failed (1) Test Files 1 failed (1) Duration " @@ -2246,25 +2317,25 @@ Duration " `; exports[`inflight_ref_explicit_ops.test.w 1`] = ` -"error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +"error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_explicit_ops.test.w:36:5 | 36 | x.put(\\"hello\\", \\"world\\"); - | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. -error: Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +error: Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_explicit_ops.test.w:43:5 | 43 | q.push(\\"push!\\"); - | ^ Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. -error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_explicit_ops.test.w:48:12 | 48 | assert(b.list().length == 0); - | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. @@ -2275,18 +2346,18 @@ Duration " `; exports[`inflight_ref_resource_sub_method.test.w 1`] = ` -"error: Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +"error: Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_resource_sub_method.test.w:32:5 | 32 | q.push(\\"push!\\"); - | ^ Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. -error: Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +error: Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_resource_sub_method.test.w:35:5 | 35 | q2.push(\\"push!\\"); - | ^^ Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^^ Expression of type \\"Queue\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. @@ -2297,18 +2368,18 @@ Duration " `; exports[`inflight_ref_unknown_op.test.w 1`] = ` -"error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +"error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_unknown_op.test.w:15:5 | 15 | x.put(\\"hello\\", \\"world\\"); - | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. -error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/inflight_ref_unknown_op.test.w:19:5 | 19 | y.put(\\"boom\\", \\"shakalaka\\"); - | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. @@ -3329,11 +3400,11 @@ Duration " `; exports[`resource_captures.test.w 1`] = ` -"error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) +"error: Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. --> ../../../examples/tests/invalid/resource_captures.test.w:15:5 | 15 | b.put(\\"hello\\", \\"world\\"); - | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities (see https://github.com/winglang/wing/issues/76 for details) + | ^ Expression of type \\"Bucket\\" references an unknown preflight object, can't qualify its capabilities. Use \`lift()\` to explicitly qualify the preflight object to disable this error. diff --git a/tools/hangar/__snapshots__/test_corpus/valid/bucket_keys.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/bucket_keys.test.w_compile_tf-aws.md index 6ba9a4aed63..dc33a561442 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/bucket_keys.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/bucket_keys.test.w_compile_tf-aws.md @@ -103,7 +103,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [b, ["list", "put"]], + [b, [].concat(["put"], ["list"])], ], "$inflight_init": [ [b, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/calling_inflight_variants.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/calling_inflight_variants.test.w_compile_tf-aws.md index 265a30568da..49b039a4526 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/calling_inflight_variants.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/calling_inflight_variants.test.w_compile_tf-aws.md @@ -218,7 +218,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [foo, ["callFn", "callFn2"]], + [foo, [].concat(["callFn"], ["callFn2"])], ], "$inflight_init": [ [foo, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/capture_in_binary.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/capture_in_binary.test.w_compile_tf-aws.md index 665a6651af6..50b0ec169e5 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/capture_in_binary.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/capture_in_binary.test.w_compile_tf-aws.md @@ -96,7 +96,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [b, ["get", "put"]], + [b, [].concat(["put"], ["get"])], [x, []], ], "$inflight_init": [ diff --git a/tools/hangar/__snapshots__/test_corpus/valid/capture_reassigable_class_field.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/capture_reassigable_class_field.test.w_compile_tf-aws.md index 5deaa47678a..0242fd1b421 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/capture_reassigable_class_field.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/capture_reassigable_class_field.test.w_compile_tf-aws.md @@ -300,7 +300,7 @@ class $Root extends $stdlib.std.Resource { return ({ "handle": [ [counter, ["peek"]], - [kv, ["get", "set"]], + [kv, [].concat(["set"], ["get"])], ], "$inflight_init": [ [counter, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/capture_resource_and_data.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/capture_resource_and_data.test.w_compile_tf-aws.md index 32b4e74448c..ef29ca8fa97 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/capture_resource_and_data.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/capture_resource_and_data.test.w_compile_tf-aws.md @@ -113,7 +113,7 @@ class $Root extends $stdlib.std.Resource { "handle": [ [data.size, []], [queue, ["push"]], - [res, ["get", "put"]], + [res, [].concat(["put"], ["get"])], ], "$inflight_init": [ [data.size, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md index b0c89a46799..0ff31be9756 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/captures.test.w_compile_tf-aws.md @@ -678,8 +678,8 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [bucket1, ["list", "publicUrl", "put"]], - [bucket2, ["get", "publicUrl"]], + [bucket1, [].concat(["put"], ["list"], ["publicUrl"])], + [bucket2, [].concat(["get"], ["publicUrl"])], [bucket3, ["get"]], ], "$inflight_init": [ diff --git a/tools/hangar/__snapshots__/test_corpus/valid/class.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/class.test.w_compile_tf-aws.md index 250ba08f852..d05d70e988d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/class.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/class.test.w_compile_tf-aws.md @@ -546,7 +546,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [c5, ["set", "x", "y"]], + [c5, [].concat(["x"], ["y"], ["set"])], ], "$inflight_init": [ [c5, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/closure_class.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/closure_class.test.w_compile_tf-aws.md index 0c40e12ee68..6912cae53ed 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/closure_class.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/closure_class.test.w_compile_tf-aws.md @@ -135,7 +135,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [fn, ["another", "handle"]], + [fn, [].concat(["handle"], ["another"])], ], "$inflight_init": [ [fn, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md index 96fe5dcd58f..7752e30e05a 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/debug_env.test.w_test_sim.md @@ -4,7 +4,7 @@ ```log [symbol environment at debug_env.test.w:7:5] level 0: { this => A } -level 1: { A => A [type], assert => (condition: bool): void, cloud => cloud [namespace], log => (message: str): void, nodeof => preflight (construct: IConstruct): Node, std => std [namespace], this => Construct, unsafeCast => (value: any): any } +level 1: { A => A [type], assert => (condition: bool): void, cloud => cloud [namespace], lift => inflight (preflightObject: Resource, qualifications: Array): void, log => (message: str): void, nodeof => preflight (construct: IConstruct): Node, std => std [namespace], this => Construct, unsafeCast => (value: any): any } ``` ## stdout.log diff --git a/tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_compile_tf-aws.md new file mode 100644 index 00000000000..ea064e2eb81 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_compile_tf-aws.md @@ -0,0 +1,303 @@ +# [explicit_lift_qualification.test.w](../../../../../examples/tests/valid/explicit_lift_qualification.test.w) | compile | tf-aws + +## inflight.$Closure1-1.js +```js +"use strict"; +const $helpers = require("@winglang/sdk/lib/helpers"); +module.exports = function({ $foo }) { + class $Closure1 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + (await $foo.mehtod()); + } + } + return $Closure1; +} +//# sourceMappingURL=inflight.$Closure1-1.js.map +``` + +## inflight.$Closure2-1.js +```js +"use strict"; +const $helpers = require("@winglang/sdk/lib/helpers"); +module.exports = function({ $bucket }) { + class $Closure2 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + ; + const b = $bucket; + (await b.put("k3", "value3")); + $helpers.assert($helpers.eq((await $bucket.get("k3")), "value3"), "bucket.get(\"k3\") == \"value3\""); + } + } + return $Closure2; +} +//# sourceMappingURL=inflight.$Closure2-1.js.map +``` + +## inflight.$Closure3-1.js +```js +"use strict"; +const $helpers = require("@winglang/sdk/lib/helpers"); +module.exports = function({ $inflight_closure }) { + class $Closure3 { + constructor({ }) { + const $obj = (...args) => this.handle(...args); + Object.setPrototypeOf($obj, this); + return $obj; + } + async handle() { + (await $inflight_closure()); + } + } + return $Closure3; +} +//# sourceMappingURL=inflight.$Closure3-1.js.map +``` + +## inflight.Foo-1.js +```js +"use strict"; +const $helpers = require("@winglang/sdk/lib/helpers"); +module.exports = function({ $bucket, $put_and_list }) { + class Foo { + constructor({ }) { + } + async mehtod() { + ; + ; + const b = $bucket; + (await b.put("k2", "value2")); + $helpers.assert($helpers.eq((await b.list()), ["k", "k2"]), "b.list() == [\"k\", \"k2\"]"); + (await b.delete("k2")); + $helpers.assert($helpers.eq((await $bucket.tryGet("k2")), undefined), "bucket.tryGet(\"k2\") == nil"); + } + } + return Foo; +} +//# sourceMappingURL=inflight.Foo-1.js.map +``` + +## main.tf.json +```json +{ + "//": { + "metadata": { + "backend": "local", + "stackName": "root", + "version": "0.20.3" + }, + "outputs": {} + }, + "provider": { + "aws": [ + {} + ] + }, + "resource": { + "aws_s3_bucket": { + "Bucket": { + "//": { + "metadata": { + "path": "root/Default/Default/Bucket/Default", + "uniqueId": "Bucket" + } + }, + "bucket_prefix": "bucket-c88fdc5f-", + "force_destroy": false + } + }, + "aws_s3_object": { + "Bucket_S3Object-k_D126CC53": { + "//": { + "metadata": { + "path": "root/Default/Default/Bucket/S3Object-k", + "uniqueId": "Bucket_S3Object-k_D126CC53" + } + }, + "bucket": "${aws_s3_bucket.Bucket.bucket}", + "content": "value", + "key": "k" + } + } + } +} +``` + +## preflight.js +```js +"use strict"; +const $stdlib = require('@winglang/sdk'); +const $platforms = ((s) => !s ? [] : s.split(';'))(process.env.WING_PLATFORMS); +const $outdir = process.env.WING_SYNTH_DIR ?? "."; +const $wing_is_test = process.env.WING_IS_TEST === "true"; +const std = $stdlib.std; +const $helpers = $stdlib.helpers; +const cloud = $stdlib.cloud; +class $Root extends $stdlib.std.Resource { + constructor($scope, $id) { + super($scope, $id); + class Foo extends $stdlib.std.Resource { + constructor($scope, $id, ) { + super($scope, $id); + } + static _toInflightType() { + return ` + require("${$helpers.normalPath(__dirname)}/inflight.Foo-1.js")({ + $bucket: ${$stdlib.core.liftObject(bucket)}, + $put_and_list: ${$stdlib.core.liftObject(put_and_list)}, + }) + `; + } + _toInflight() { + return ` + (await (async () => { + const FooClient = ${Foo._toInflightType()}; + const client = new FooClient({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `; + } + get _liftMap() { + return ({ + "mehtod": [ + [bucket, [].concat(put_and_list, ["delete"], ["tryGet"])], + [put_and_list, []], + ], + "$inflight_init": [ + [bucket, []], + [put_and_list, []], + ], + }); + } + } + class $Closure1 extends $stdlib.std.AutoIdResource { + _id = $stdlib.core.closureId(); + constructor($scope, $id, ) { + super($scope, $id); + $helpers.nodeof(this).hidden = true; + } + static _toInflightType() { + return ` + require("${$helpers.normalPath(__dirname)}/inflight.$Closure1-1.js")({ + $foo: ${$stdlib.core.liftObject(foo)}, + }) + `; + } + _toInflight() { + return ` + (await (async () => { + const $Closure1Client = ${$Closure1._toInflightType()}; + const client = new $Closure1Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `; + } + get _liftMap() { + return ({ + "handle": [ + [foo, ["mehtod"]], + ], + "$inflight_init": [ + [foo, []], + ], + }); + } + } + class $Closure2 extends $stdlib.std.AutoIdResource { + _id = $stdlib.core.closureId(); + constructor($scope, $id, ) { + super($scope, $id); + $helpers.nodeof(this).hidden = true; + } + static _toInflightType() { + return ` + require("${$helpers.normalPath(__dirname)}/inflight.$Closure2-1.js")({ + $bucket: ${$stdlib.core.liftObject(bucket)}, + }) + `; + } + _toInflight() { + return ` + (await (async () => { + const $Closure2Client = ${$Closure2._toInflightType()}; + const client = new $Closure2Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `; + } + get _liftMap() { + return ({ + "handle": [ + [bucket, [].concat(["put"], ["get"])], + ], + "$inflight_init": [ + [bucket, []], + ], + }); + } + } + class $Closure3 extends $stdlib.std.AutoIdResource { + _id = $stdlib.core.closureId(); + constructor($scope, $id, ) { + super($scope, $id); + $helpers.nodeof(this).hidden = true; + } + static _toInflightType() { + return ` + require("${$helpers.normalPath(__dirname)}/inflight.$Closure3-1.js")({ + $inflight_closure: ${$stdlib.core.liftObject(inflight_closure)}, + }) + `; + } + _toInflight() { + return ` + (await (async () => { + const $Closure3Client = ${$Closure3._toInflightType()}; + const client = new $Closure3Client({ + }); + if (client.$inflight_init) { await client.$inflight_init(); } + return client; + })()) + `; + } + get _liftMap() { + return ({ + "handle": [ + [inflight_closure, ["handle"]], + ], + "$inflight_init": [ + [inflight_closure, []], + ], + }); + } + } + const bucket = this.node.root.new("@winglang/sdk.cloud.Bucket", cloud.Bucket, this, "Bucket"); + (bucket.addObject("k", "value")); + const put_and_list = ["put", "list"]; + const foo = new Foo(this, "Foo"); + this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:explicit method lift qualification", new $Closure1(this, "$Closure1")); + const inflight_closure = new $Closure2(this, "$Closure2"); + this.node.root.new("@winglang/sdk.std.Test", std.Test, this, "test:explicit closure lift qualification", new $Closure3(this, "$Closure3")); + } +} +const $PlatformManager = new $stdlib.platform.PlatformManager({platformPaths: $platforms}); +const $APP = $PlatformManager.createApp({ outdir: $outdir, name: "explicit_lift_qualification.test", rootConstruct: $Root, isTestEnvironment: $wing_is_test, entrypointDir: process.env['WING_SOURCE_DIR'], rootId: process.env['WING_ROOT_ID'] }); +$APP.synth(); +//# sourceMappingURL=preflight.js.map +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_test_sim.md b/tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_test_sim.md new file mode 100644 index 00000000000..6078d33c7e7 --- /dev/null +++ b/tools/hangar/__snapshots__/test_corpus/valid/explicit_lift_qualification.test.w_test_sim.md @@ -0,0 +1,13 @@ +# [explicit_lift_qualification.test.w](../../../../../examples/tests/valid/explicit_lift_qualification.test.w) | test | sim + +## stdout.log +```log +pass ─ explicit_lift_qualification.test.wsim » root/env0/test:explicit method lift qualification +pass ─ explicit_lift_qualification.test.wsim » root/env1/test:explicit closure lift qualification + + +Tests 2 passed (2) +Test Files 1 passed (1) +Duration +``` + diff --git a/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md index a6ecbf35cf2..e9460d4823d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/extern_implementation.test.w_compile_tf-aws.md @@ -148,7 +148,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "call": [ - [Foo, ["getData", "getUuid", "regexInflight"]], + [Foo, [].concat(["regexInflight"], ["getUuid"], ["getData"])], ], "$inflight_init": [ [Foo, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_outside_inflight_closure.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_outside_inflight_closure.test.w_compile_tf-aws.md index 87dc7f77b94..aba3f04abc0 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_outside_inflight_closure.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inflight_class_outside_inflight_closure.test.w_compile_tf-aws.md @@ -98,10 +98,10 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "add": [ - [this, ["lhs", "rhs"]], + [this, [].concat(["lhs"], ["rhs"])], ], "$inflight_init": [ - [this, ["lhs", "rhs"]], + [this, [].concat(["lhs"], ["rhs"])], ], "lhs": [ ], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inflight_handler_singleton.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inflight_handler_singleton.test.w_compile_tf-aws.md index 2400f91c48d..f11c5f4fe99 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inflight_handler_singleton.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inflight_handler_singleton.test.w_compile_tf-aws.md @@ -615,7 +615,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [foo, ["get", "inc"]], + [foo, [].concat(["inc"], ["get"])], ], "$inflight_init": [ [foo, []], @@ -652,7 +652,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [fn3, ["invoke", "invokeAsync"]], + [fn3, [].concat(["invokeAsync"], ["invoke"])], ], "$inflight_init": [ [fn3, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inflight_init.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inflight_init.test.w_compile_tf-aws.md index a0e4b3c093d..49ccade7dad 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inflight_init.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inflight_init.test.w_compile_tf-aws.md @@ -222,7 +222,7 @@ class $Root extends $stdlib.std.Resource { "get_six": [ ], "$inflight_init": [ - [this, ["field1", "field2", "get_six"]], + [this, [].concat(["field1"], ["get_six"], ["field2"])], ], "field1": [ ], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/inheritance_class_inflight.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/inheritance_class_inflight.test.w_compile_tf-aws.md index 06174909c5f..3c4ce05a66c 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/inheritance_class_inflight.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/inheritance_class_inflight.test.w_compile_tf-aws.md @@ -188,7 +188,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [foo, ["bang", "bug", "over_inflight"]], + [foo, [].concat(["bang"], ["bug"], ["over_inflight"])], ], "$inflight_init": [ [foo, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/lift_via_closure.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/lift_via_closure.test.w_compile_tf-aws.md index 35f36be1c68..38ef8c86c52 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/lift_via_closure.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/lift_via_closure.test.w_compile_tf-aws.md @@ -293,7 +293,7 @@ class $Root extends $stdlib.std.Resource { return ({ "handle": [ [bucket2, ["get"]], - [fn2, ["handle", "listFiles"]], + [fn2, [].concat(["handle"], ["listFiles"])], [fn2.bucket, ["get"]], ], "$inflight_init": [ diff --git a/tools/hangar/__snapshots__/test_corpus/valid/nil.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/nil.test.w_compile_tf-aws.md index 03ba212c004..00c2b52fc96 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/nil.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/nil.test.w_compile_tf-aws.md @@ -206,7 +206,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [foo, ["getOptionalValue", "setOptionalValue"]], + [foo, [].concat(["getOptionalValue"], ["setOptionalValue"])], ], "$inflight_init": [ [foo, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/phase_independent_method_on_string.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/phase_independent_method_on_string.test.w_compile_tf-aws.md index c6507b47de5..99301d9f45f 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/phase_independent_method_on_string.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/phase_independent_method_on_string.test.w_compile_tf-aws.md @@ -171,7 +171,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [api.url, ["length", "startsWith"]], + [api.url, [].concat(["startsWith"], ["length"])], [token_len, []], [url_regex, []], ], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md index 17853cd2844..d9fb0566bf3 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/redis.test.w_compile_tf-aws.md @@ -612,7 +612,7 @@ class $Root extends $stdlib.std.Resource { "handle": [ [queue, ["push"]], [r, ["get"]], - [r2, ["get", "set"]], + [r2, [].concat(["set"], ["get"])], ], "$inflight_init": [ [queue, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md index d0d8e26abdb..f234c9defb4 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/resource.test.w_compile_tf-aws.md @@ -745,7 +745,7 @@ class $Root extends $stdlib.std.Resource { [this.c, ["peek"]], ], "$inflight_init": [ - [this.c, ["dec", "inc"]], + [this.c, [].concat(["inc"], ["dec"])], ], "inflightField": [ ], @@ -792,8 +792,8 @@ class $Root extends $stdlib.std.Resource { return ({ "myMethod": [ [Foo, ["fooStatic"]], - [this.b, ["get", "put"]], - [this.foo, ["fooGet", "fooInc"]], + [this.b, [].concat(["put"], ["get"])], + [this.foo, [].concat(["fooInc"], ["fooGet"])], ], "testTypeAccess": [ [Bar, ["barStatic"]], @@ -846,7 +846,7 @@ class $Root extends $stdlib.std.Resource { return ({ "handle": [ [bucket, ["list"]], - [res, ["myMethod", "testTypeAccess"]], + [res, [].concat(["myMethod"], ["testTypeAccess"])], [res.foo, ["inflightField"]], ], "$inflight_init": [ @@ -1043,7 +1043,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [bigOlPublisher, ["getObjectCount", "publish"]], + [bigOlPublisher, [].concat(["publish"], ["getObjectCount"])], ], "$inflight_init": [ [bigOlPublisher, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/resource_captures.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/resource_captures.test.w_compile_tf-aws.md index a1cb4c1c64f..3b2d6e1a63a 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/resource_captures.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/resource_captures.test.w_compile_tf-aws.md @@ -391,13 +391,13 @@ class $Root extends $stdlib.std.Resource { [this.myOptStr, []], ], "testCaptureResource": [ - [this.myResource, ["get", "list", "put"]], + [this.myResource, [].concat(["put"], ["get"], ["list"])], ], "testNestedInflightField": [ [this.another.myField, []], ], "testNestedResource": [ - [this.another.first.myResource, ["get", "list", "put"]], + [this.another.first.myResource, [].concat(["list"], ["put"], ["get"])], [this.myStr, []], ], "testExpressionRecursive": [ @@ -409,7 +409,7 @@ class $Root extends $stdlib.std.Resource { [this.extNum, []], ], "testUserDefinedResource": [ - [this.another, ["anotherFunc", "meaningOfLife"]], + [this.another, [].concat(["meaningOfLife"], ["anotherFunc"])], ], "testInflightField": [ ], @@ -466,7 +466,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [r, ["testCaptureCollectionsOfData", "testCaptureOptional", "testCapturePrimitives", "testCaptureResource", "testExpressionRecursive", "testExternal", "testInflightField", "testNestedInflightField", "testNestedResource", "testNoCapture", "testUserDefinedResource"]], + [r, [].concat(["testNoCapture"], ["testCaptureCollectionsOfData"], ["testCapturePrimitives"], ["testCaptureOptional"], ["testCaptureResource"], ["testNestedInflightField"], ["testNestedResource"], ["testExpressionRecursive"], ["testExternal"], ["testUserDefinedResource"], ["testInflightField"])], ], "$inflight_init": [ [r, []], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/resource_captures_globals.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/resource_captures_globals.test.w_compile_tf-aws.md index c4cbc019215..76cb035a51d 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/resource_captures_globals.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/resource_captures_globals.test.w_compile_tf-aws.md @@ -418,7 +418,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "myMethod": [ - [globalCounter, ["inc", "peek"]], + [globalCounter, [].concat(["inc"], ["peek"])], ], "$inflight_init": [ [globalCounter, ["peek"]], diff --git a/tools/hangar/__snapshots__/test_corpus/valid/std_string.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/std_string.test.w_compile_tf-aws.md index dc3d682f55e..62af4b4f7d7 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/std_string.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/std_string.test.w_compile_tf-aws.md @@ -81,7 +81,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [s1, ["concat", "indexOf", "split"]], + [s1, [].concat(["indexOf"], ["split"], ["concat"])], [s2, []], ], "$inflight_init": [ diff --git a/tools/hangar/__snapshots__/test_corpus/valid/test_bucket.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/test_bucket.test.w_compile_tf-aws.md index d262a72910d..215fe5a92b3 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/test_bucket.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/test_bucket.test.w_compile_tf-aws.md @@ -116,7 +116,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [b, ["list", "put"]], + [b, [].concat(["list"], ["put"])], ], "$inflight_init": [ [b, []], @@ -151,7 +151,7 @@ class $Root extends $stdlib.std.Resource { get _liftMap() { return ({ "handle": [ - [b, ["get", "put"]], + [b, [].concat(["put"], ["get"])], ], "$inflight_init": [ [b, []], From 234d6332b65a362c8cbf954f70c62ac28b9a58b0 Mon Sep 17 00:00:00 2001 From: Chris Rybicki Date: Tue, 19 Mar 2024 03:51:27 -0400 Subject: [PATCH 5/6] fix(compiler): cannot chain ?? expressions (#5987) Fixes #1875 This fixes the issue by updating the precedent rules so the operation is right-associative. For now, expressions like `a ?? b` or `a ?? b ?? c` still have to return a non-optional value (like Rust's unwrap_or), but we can consider making it more flexible / open in the way that C# or Swift allows if appropriate in the future. ## 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) - [ ] 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)*. --- examples/tests/valid/optionals.test.w | 5 ++-- libs/tree-sitter-wing/grammar.js | 10 +++++++- .../test/corpus/expressions.txt | 16 +++++++++---- libs/wingc/src/parser.rs | 2 +- .../valid/optionals.test.w_compile_tf-aws.md | 23 ++++++++++--------- 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/examples/tests/valid/optionals.test.w b/examples/tests/valid/optionals.test.w index 511be164e9c..1c7772a72cb 100644 --- a/examples/tests/valid/optionals.test.w +++ b/examples/tests/valid/optionals.test.w @@ -16,13 +16,14 @@ class Super { class Sub extends Super { new() { this.name = "Sub"; } } -class Sub1 extends Super { - new() { this.name = "Sub"; } +class SubSub extends Sub { + new() { this.name = "SubSub"; } } let optionalSup: Super? = new Super(); let s = optionalSup ?? new Sub(); assert(s.name == "Super"); +let s2 = optionalSup ?? optionalSup ?? new SubSub(); struct Name { first: str; diff --git a/libs/tree-sitter-wing/grammar.js b/libs/tree-sitter-wing/grammar.js index 7bfe12e3485..4d48990e565 100644 --- a/libs/tree-sitter-wing/grammar.js +++ b/libs/tree-sitter-wing/grammar.js @@ -326,6 +326,7 @@ module.exports = grammar({ expression: ($) => choice( + $.unwrap_or, $.binary_expression, $.unary_expression, $.new_expression, @@ -568,6 +569,14 @@ module.exports = grammar({ _container_value_type: ($) => seq("<", field("type_parameter", $._type), ">"), + unwrap_or: ($) => prec.right(PREC.UNWRAP_OR, + seq( + field("left", $.expression), + field("op", "??"), + field("right", $.expression) + ) + ), + optional_unwrap: ($) => prec.right(PREC.OPTIONAL_UNWRAP, seq($.expression, "!")), @@ -614,7 +623,6 @@ module.exports = grammar({ //['<<', PREC.SHIFT], //['>>', PREC.SHIFT], //['>>>', PREC.SHIFT], - ["??", PREC.UNWRAP_OR], ]; return choice( diff --git a/libs/tree-sitter-wing/test/corpus/expressions.txt b/libs/tree-sitter-wing/test/corpus/expressions.txt index 3d2b9c650d3..925bdb0b9ab 100644 --- a/libs/tree-sitter-wing/test/corpus/expressions.txt +++ b/libs/tree-sitter-wing/test/corpus/expressions.txt @@ -298,17 +298,18 @@ Template string maybeVal ?? 2; maybeVal ?? 2 + 2; maybeVal ?? 2 == 2; +maybeVal ?? "hi" ?? 2; -------------------------------------------------------------------------------- (source (expression_statement - (binary_expression + (unwrap_or (reference (reference_identifier)) (number))) (expression_statement - (binary_expression + (unwrap_or (reference (reference_identifier)) (binary_expression @@ -316,11 +317,18 @@ maybeVal ?? 2 == 2; (number)))) (expression_statement (binary_expression - (binary_expression + (unwrap_or (reference (reference_identifier)) (number)) - (number)))) + (number))) + (expression_statement + (unwrap_or + (reference + (reference_identifier)) + (unwrap_or + (string) + (number))))) ================================================================================ Unwrap expression diff --git a/libs/wingc/src/parser.rs b/libs/wingc/src/parser.rs index 31945b6b531..e78dc59dd07 100644 --- a/libs/wingc/src/parser.rs +++ b/libs/wingc/src/parser.rs @@ -2021,7 +2021,7 @@ impl<'s> Parser<'s> { expression_span, )) } - "binary_expression" => Ok(Expr::new( + "binary_expression" | "unwrap_or" => Ok(Expr::new( ExprKind::Binary { left: Box::new(self.build_expression(&expression_node.child_by_field_name("left").unwrap(), phase)?), right: Box::new(self.build_expression(&expression_node.child_by_field_name("right").unwrap(), phase)?), diff --git a/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md b/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md index ad5e1181cae..2a75aef7ff9 100644 --- a/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md +++ b/tools/hangar/__snapshots__/test_corpus/valid/optionals.test.w_compile_tf-aws.md @@ -52,19 +52,19 @@ module.exports = function({ $Super }) { //# sourceMappingURL=inflight.Sub-1.js.map ``` -## inflight.Sub1-1.js +## inflight.SubSub-1.js ```js "use strict"; const $helpers = require("@winglang/sdk/lib/helpers"); -module.exports = function({ $Super }) { - class Sub1 extends $Super { +module.exports = function({ $Sub }) { + class SubSub extends $Sub { constructor({ }) { super({ }); } } - return Sub1; + return SubSub; } -//# sourceMappingURL=inflight.Sub1-1.js.map +//# sourceMappingURL=inflight.SubSub-1.js.map ``` ## inflight.Super-1.js @@ -187,23 +187,23 @@ class $Root extends $stdlib.std.Resource { }); } } - class Sub1 extends Super { + class SubSub extends Sub { constructor($scope, $id, ) { super($scope, $id); - this.name = "Sub"; + this.name = "SubSub"; } static _toInflightType() { return ` - require("${$helpers.normalPath(__dirname)}/inflight.Sub1-1.js")({ - $Super: ${$stdlib.core.liftObject(Super)}, + require("${$helpers.normalPath(__dirname)}/inflight.SubSub-1.js")({ + $Sub: ${$stdlib.core.liftObject(Sub)}, }) `; } _toInflight() { return ` (await (async () => { - const Sub1Client = ${Sub1._toInflightType()}; - const client = new Sub1Client({ + const SubSubClient = ${SubSub._toInflightType()}; + const client = new SubSubClient({ }); if (client.$inflight_init) { await client.$inflight_init(); } return client; @@ -298,6 +298,7 @@ class $Root extends $stdlib.std.Resource { const optionalSup = new Super(this, "Super"); const s = (optionalSup ?? new Sub(this, "Sub")); $helpers.assert($helpers.eq(s.name, "Super"), "s.name == \"Super\""); + const s2 = (optionalSup ?? (optionalSup ?? new SubSub(this, "SubSub"))); let name = ({"first": "John", "last": "Doe"}); { const $if_let_value = name; From 8dfd1bcabf08673d0122e14d4d6d0369756096fb Mon Sep 17 00:00:00 2001 From: wingbot <109207340+monadabot@users.noreply.github.com> Date: Tue, 19 Mar 2024 05:02:16 -0400 Subject: [PATCH 6/6] chore(docs): compatibility matrix update (#5977) chore(docs): update compatibility matrix [Workflow Run]: https://github.com/winglang/wing/actions/runs/8339365002 ------ *Automatically created via the "matrix-update" workflow* --- docs/docs/04-standard-library/compatibility/compatibility.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/docs/04-standard-library/compatibility/compatibility.json b/docs/docs/04-standard-library/compatibility/compatibility.json index 580378d019f..f899e79c280 100644 --- a/docs/docs/04-standard-library/compatibility/compatibility.json +++ b/docs/docs/04-standard-library/compatibility/compatibility.json @@ -820,6 +820,9 @@ "initial": { "tf-aws": { "implemented": true + }, + "sim": { + "implemented": true } } },