diff --git a/src/asl.ts b/src/asl.ts index 24e436ce..a43a2bce 100644 --- a/src/asl.ts +++ b/src/asl.ts @@ -23,7 +23,6 @@ import { isArgument, isArrayBinding, isArrayLiteralExpr, - isArrowFunctionExpr, isAwaitExpr, isBinaryExpr, isBindingElem, @@ -51,7 +50,7 @@ import { isForInStmt, isForOfStmt, isFunctionDecl, - isFunctionExpr, + isFunctionLike, isIdentifier, isIfStmt, isLabelledStmt, @@ -104,7 +103,6 @@ import { isGetAccessorDecl, isTaggedTemplateExpr, isOmittedExpr, - isFunctionLike, isQuasiString, } from "./guards"; import { @@ -1680,7 +1678,7 @@ export class ASL { const throwTransition = this.throw(expr); const callbackfn = expr.args[0].expr; - if (callbackfn !== undefined && isFunctionExpr(callbackfn)) { + if (callbackfn !== undefined && isFunctionLike(callbackfn)) { const callbackStates = this.evalStmt(callbackfn.body); return this.evalExpr( @@ -2448,7 +2446,7 @@ export class ASL { // detect the immediate for-loop closure surrounding this throw statement // because of how step function's Catch feature works, we need to check if the try // is inside or outside the closure - const mapOrParallelClosure = node.findParent(isFunctionExpr); + const mapOrParallelClosure = node.findParent(isFunctionLike); // catchClause or finallyBlock that will run upon throwing this error const catchOrFinally = node.throw(); @@ -2709,7 +2707,7 @@ export class ASL { expr: CallExpr & { expr: PropAccessExpr } ): ASLGraph.NodeResults { const predicate = expr.args[0]?.expr; - if (!(isFunctionExpr(predicate) || isArrowFunctionExpr(predicate))) { + if (!isFunctionLike(predicate)) { throw new SynthError( ErrorCodes.StepFunction_invalid_filter_syntax, `the 'predicate' argument of slice must be a function or arrow expression, found: ${predicate?.kindName}` @@ -4739,7 +4737,7 @@ function nodeToString( return `[${nodeToString(expr.expr)}]`; } else if (isElementAccessExpr(expr)) { return `${nodeToString(expr.expr)}[${nodeToString(expr.element)}]`; - } else if (isFunctionExpr(expr) || isArrowFunctionExpr(expr)) { + } else if (isFunctionLike(expr)) { return `function(${expr.parameters.map(nodeToString).join(", ")})`; } else if (isIdentifier(expr)) { return expr.name; diff --git a/src/assert.ts b/src/assert.ts index 17aa7ee2..1aaf8ede 100644 --- a/src/assert.ts +++ b/src/assert.ts @@ -69,18 +69,22 @@ export function assertConstantValue(val: any, message?: string): ConstantValue { ); } -export function assertNodeKind( +export function assertNodeKind( node: FunctionlessNode | undefined, - kind: Kind -): NodeInstance { - if (node?.kind !== kind) { - throw Error( - `Expected node of type ${getNodeKindName(kind)} and found ${ - node ? getNodeKindName(node.kind) : "undefined" - }` - ); + ...kinds: Kind +): NodeInstance { + if (node) { + for (const kind of kinds) { + if (node?.kind === kind) { + return >node; + } + } } - return >node; + throw Error( + `Expected node of type ${kinds.map(getNodeKindName).join(", ")} and found ${ + node ? getNodeKindName(node.kind) : "undefined" + }` + ); } // to prevent the closure serializer from trying to import all of functionless. diff --git a/src/compile.ts b/src/compile.ts index d8bb576c..2f07c6bd 100644 --- a/src/compile.ts +++ b/src/compile.ts @@ -296,6 +296,31 @@ export function compile( ]), ]); + const isAsync = + ts.isFunctionDeclaration(impl) || + ts.isFunctionExpression(impl) || + ts.isArrowFunction(impl) || + ts.isMethodDeclaration(impl) + ? [ + impl.modifiers?.find( + (mod) => mod.kind === ts.SyntaxKind.AsyncKeyword + ) + ? ts.factory.createTrue() + : ts.factory.createFalse(), + ] + : []; + + const isAsterisk = + ts.isFunctionDeclaration(impl) || + ts.isFunctionExpression(impl) || + ts.isMethodDeclaration(impl) + ? [ + impl.asteriskToken + ? ts.factory.createTrue() + : ts.factory.createFalse(), + ] + : []; + return newExpr(type, [ ...resolveFunctionName(), ts.factory.createArrayLiteralExpression( @@ -309,6 +334,10 @@ export function compile( ) ), body, + // isAsync for Functions, Arrows and Methods + ...isAsync, + // isAsterisk for Functions and Methods + ...isAsterisk, ]); }); @@ -340,7 +369,9 @@ export function compile( ): ts.Expression { if (node === undefined) { return ts.factory.createIdentifier("undefined"); - } else if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) { + } else if (ts.isArrowFunction(node)) { + return toFunction(NodeKind.ArrowFunctionExpr, node, scope); + } else if (ts.isFunctionExpression(node)) { return toFunction(NodeKind.FunctionExpr, node, scope); } else if (ts.isExpressionStatement(node)) { return newExpr(NodeKind.ExprStmt, [toExpr(node.expression, scope)]); @@ -419,14 +450,29 @@ export function compile( ? ts.factory.createTrue() : ts.factory.createFalse(), ]); + } else if (ts.isPropertyAccessChain(node)) { + return newExpr(NodeKind.PropAccessExpr, [ + toExpr(node.expression, scope), + toExpr(node.name, scope), + node.questionDotToken + ? ts.factory.createTrue() + : ts.factory.createFalse(), + ]); + } else if (ts.isElementAccessChain(node)) { + return newExpr(NodeKind.ElementAccessExpr, [ + toExpr(node.expression, scope), + toExpr(node.argumentExpression, scope), + node.questionDotToken + ? ts.factory.createTrue() + : ts.factory.createFalse(), + ]); } else if (ts.isElementAccessExpression(node)) { - const type = checker.getTypeAtLocation(node.argumentExpression); return newExpr(NodeKind.ElementAccessExpr, [ toExpr(node.expression, scope), toExpr(node.argumentExpression, scope), - type - ? ts.factory.createStringLiteral(checker.typeToString(type)) - : ts.factory.createIdentifier("undefined"), + node.questionDotToken + ? ts.factory.createTrue() + : ts.factory.createFalse(), ]); } else if (ts.isVariableStatement(node)) { return newExpr(NodeKind.VariableStmt, [ diff --git a/src/declaration.ts b/src/declaration.ts index a67c2a0b..55d7903d 100644 --- a/src/declaration.ts +++ b/src/declaration.ts @@ -79,12 +79,40 @@ export class MethodDecl extends BaseDecl { constructor( readonly name: PropName, readonly parameters: ParameterDecl[], - readonly body: BlockStmt + readonly body: BlockStmt, + + /** + * true if this function has an `async` modifier + * ```ts + * class Foo { + * async foo() {} + * + * // asterisk can co-exist + * async *foo() {} + * } + * ``` + */ + readonly isAsync: boolean, + /** + * true if this function has an `*` modifier + * + * ```ts + * class Foo { + * foo*() {} + * + * // async can co-exist + * async *foo() {} + * } + * ``` + */ + readonly isAsterisk: boolean ) { super(NodeKind.MethodDecl, arguments); this.ensure(name, "name", NodeKind.PropName); this.ensureArrayOf(parameters, "parameters", [NodeKind.ParameterDecl]); this.ensure(body, "body", [NodeKind.BlockStmt]); + this.ensure(isAsync, "isAsync", ["boolean"]); + this.ensure(isAsterisk, "isAsterisk", ["boolean"]); } } @@ -142,12 +170,34 @@ export class FunctionDecl< // according to the spec, name is mandatory on a FunctionDecl and FunctionExpr readonly name: string | undefined, readonly parameters: ParameterDecl[], - readonly body: BlockStmt + readonly body: BlockStmt, + /** + * true if this function has an `async` modifier + * ```ts + * async function foo() {} + * // asterisk can co-exist + * async function *foo() {} + * ``` + */ + readonly isAsync: boolean, + /** + * true if this function has an `*` modifier + * + * ```ts + * function foo*() {} + * + * // async can co-exist + * async function *foo() {} + * ``` + */ + readonly isAsterisk: boolean ) { super(NodeKind.FunctionDecl, arguments); this.ensure(name, "name", ["undefined", "string"]); this.ensureArrayOf(parameters, "parameters", [NodeKind.ParameterDecl]); this.ensure(body, "body", [NodeKind.BlockStmt]); + this.ensure(isAsync, "isAsync", ["boolean"]); + this.ensure(isAsterisk, "isAsterisk", ["boolean"]); } } diff --git a/src/ensure.ts b/src/ensure.ts index 0b028e4f..f708e139 100644 --- a/src/ensure.ts +++ b/src/ensure.ts @@ -59,7 +59,7 @@ export function ensure( nodeKind: NodeKind, item: any, fieldName: string, - assertions: Assert[] + assertions: Assert[] | readonly Assert[] ): asserts item is AssertionToInstance { if (!is(item, assertions)) { throw new Error( diff --git a/src/event-bridge/utils.ts b/src/event-bridge/utils.ts index 93692fa5..a69899fb 100644 --- a/src/event-bridge/utils.ts +++ b/src/event-bridge/utils.ts @@ -79,7 +79,11 @@ export const getPropertyAccessKeyFlatten = ( ): string | number => { if (isElementAccessExpr(expr)) { return getPropertyAccessKey( - new ElementAccessExpr(expr.expr, flattenExpression(expr.element, scope)) + new ElementAccessExpr( + expr.expr, + flattenExpression(expr.element, scope), + expr.isOptional + ) ); } return getPropertyAccessKey(expr); @@ -186,7 +190,7 @@ export const flattenExpression = (expr: Expr, scope: EventScope): Expr => { } return typeof key === "string" ? new PropAccessExpr(parent, new Identifier(key), false) - : new ElementAccessExpr(parent, new NumberLiteralExpr(key)); + : new ElementAccessExpr(parent, new NumberLiteralExpr(key), false); } else if (isComputedPropertyNameExpr(expr)) { return flattenExpression(expr.expr, scope); } else if (isArrayLiteralExpr(expr)) { diff --git a/src/expression.ts b/src/expression.ts index 22b14f29..06a4aafb 100644 --- a/src/expression.ts +++ b/src/expression.ts @@ -80,10 +80,21 @@ export class ArrowFunctionExpr< F extends AnyFunction = AnyFunction > extends BaseExpr { readonly _functionBrand?: F; - constructor(readonly parameters: ParameterDecl[], readonly body: BlockStmt) { + constructor( + readonly parameters: ParameterDecl[], + readonly body: BlockStmt, + /** + * true if this function has an `async` modifier + * ```ts + * async () => {} + * ``` + */ + readonly isAsync: boolean + ) { super(NodeKind.ArrowFunctionExpr, arguments); this.ensure(body, "body", [NodeKind.BlockStmt]); this.ensureArrayOf(parameters, "parameters", [NodeKind.ParameterDecl]); + this.ensure(isAsync, "isAsync", ["boolean"]); } } @@ -94,12 +105,34 @@ export class FunctionExpr< constructor( readonly name: string | undefined, readonly parameters: ParameterDecl[], - readonly body: BlockStmt + readonly body: BlockStmt, + /** + * true if this function has an `async` modifier + * ```ts + * async function foo() {} + * // asterisk can co-exist + * async function *foo() {} + * ``` + */ + readonly isAsync: boolean, + /** + * true if this function has an `*` modifier + * + * ```ts + * function foo*() {} + * + * // async can co-exist + * async function *foo() {} + * ``` + */ + readonly isAsterisk: boolean ) { super(NodeKind.FunctionExpr, arguments); this.ensure(name, "name", ["undefined", "string"]); this.ensureArrayOf(parameters, "parameters", [NodeKind.ParameterDecl]); this.ensure(body, "body", [NodeKind.BlockStmt]); + this.ensure(isAsync, "isAsync", ["boolean"]); + this.ensure(isAsterisk, "isAsterisk", ["boolean"]); } } @@ -158,6 +191,12 @@ export class PropAccessExpr extends BaseExpr { constructor( readonly expr: Expr, readonly name: Identifier | PrivateIdentifier, + /** + * Whether this is using optional chaining. + * ```ts + * a?.prop + * ``` + */ readonly isOptional: boolean ) { super(NodeKind.PropAccessExpr, arguments); @@ -167,7 +206,17 @@ export class PropAccessExpr extends BaseExpr { } export class ElementAccessExpr extends BaseExpr { - constructor(readonly expr: Expr, readonly element: Expr) { + constructor( + readonly expr: Expr, + readonly element: Expr, + /** + * Whether this is using optional chaining. + * ```ts + * a?.[element] + * ``` + */ + readonly isOptional: boolean + ) { super(NodeKind.ElementAccessExpr, arguments); this.ensure(expr, "expr", ["Expr"]); this.ensure(element, "element", ["Expr"]); diff --git a/src/node-kind.ts b/src/node-kind.ts index 2a79111f..7099c732 100644 --- a/src/node-kind.ts +++ b/src/node-kind.ts @@ -89,7 +89,7 @@ export namespace NodeKind { NodeKind.Identifier, NodeKind.ReferenceExpr, ...NodeKind.BindingPattern, - ]; + ] as const; export const ClassMember = [ NodeKind.ClassStaticBlockDecl, @@ -106,7 +106,7 @@ export namespace NodeKind { NodeKind.PropAssignExpr, NodeKind.SetAccessorDecl, NodeKind.SpreadAssignExpr, - ]; + ] as const; export const PropName = [ NodeKind.Identifier, @@ -114,9 +114,18 @@ export namespace NodeKind { NodeKind.ComputedPropertyNameExpr, NodeKind.StringLiteralExpr, NodeKind.NumberLiteralExpr, - ]; + ] as const; - export const SwitchClause = [NodeKind.CaseClause, NodeKind.DefaultClause]; + export const SwitchClause = [ + NodeKind.CaseClause, + NodeKind.DefaultClause, + ] as const; + + export const FunctionLike = [ + NodeKind.FunctionDecl, + NodeKind.FunctionExpr, + NodeKind.ArrowFunctionExpr, + ] as const; } export type NodeKindName = typeof NodeKindNames[Kind]; diff --git a/src/node.ts b/src/node.ts index 0a013ac3..0e3b6a61 100644 --- a/src/node.ts +++ b/src/node.ts @@ -134,7 +134,7 @@ export abstract class BaseNode< protected ensure( item: any, fieldName: string, - assertion: Assert[] + assertion: Assert[] | readonly Assert[] ): asserts item is AssertionToInstance { return ensure(this.kind, item, fieldName, assertion); } diff --git a/src/step-function.ts b/src/step-function.ts index 3f48e8d2..7f66d643 100644 --- a/src/step-function.ts +++ b/src/step-function.ts @@ -20,14 +20,13 @@ import { makeEventBusIntegration, } from "./event-bridge/event-bus"; import { Event } from "./event-bridge/types"; -import { CallExpr, FunctionExpr } from "./expression"; +import { CallExpr } from "./expression"; import { NativeIntegration } from "./function"; import { PrewarmClients } from "./function-prewarm"; import { isBindingPattern, isComputedPropertyNameExpr, isErr, - isFunctionExpr, isFunctionLike, isGetAccessorDecl, isIdentifier, @@ -319,7 +318,7 @@ export namespace $SFN { function mapOrForEach(call: CallExpr, context: ASL) { const callbackfn = call.args.length === 3 ? call.args[2]?.expr : call.args[1]?.expr; - if (callbackfn === undefined || !isFunctionExpr(callbackfn)) { + if (callbackfn === undefined || !isFunctionLike(callbackfn)) { throw new Error("missing callbackfn in $SFN.map"); } const callbackStates = context.evalStmt(callbackfn.body); @@ -434,8 +433,8 @@ export namespace $SFN { }> >("parallel", { asl(call, context) { - const paths = call.args.map((arg): FunctionExpr => { - if (isFunctionExpr(arg.expr)) { + const paths = call.args.map((arg): FunctionLike => { + if (isFunctionLike(arg.expr)) { return arg.expr; } else { throw new Error("each parallel path must be an inline FunctionExpr"); diff --git a/src/vtl.ts b/src/vtl.ts index de23ead2..c163d965 100644 --- a/src/vtl.ts +++ b/src/vtl.ts @@ -457,7 +457,10 @@ export abstract class VTL { // list.reduce((result: string[], next) => [...result, next], []); // list.reduce((result, next) => [...result, next]); - const fn = assertNodeKind(node.args[0]?.expr, NodeKind.FunctionExpr); + const fn = assertNodeKind( + node.args[0]?.expr, + ...NodeKind.FunctionLike + ); const initialValue = node.args[1]; // (previousValue: string[], currentValue: string, currentIndex: number, array: string[]) @@ -1009,7 +1012,7 @@ export abstract class VTL { this.evalDecl(array, list); } - const fn = assertNodeKind(call.args[0]?.expr, NodeKind.FunctionExpr); + const fn = assertNodeKind(call.args[0]?.expr, ...NodeKind.FunctionLike); const tmp = returnVariable ? returnVariable : this.newLocalVarName(); @@ -1084,7 +1087,7 @@ export abstract class VTL { * Returns the [value, index, array] arguments if this CallExpr is a `forEach` or `map` call. */ const getMapForEachArgs = (call: CallExpr) => { - return assertNodeKind(call.args[0].expr, NodeKind.FunctionExpr).parameters; + return assertNodeKind(call.args[0].expr, ...NodeKind.FunctionLike).parameters; }; // to prevent the closure serializer from trying to import all of functionless. diff --git a/test/node.test.ts b/test/node.test.ts index 37ac144e..71fc14eb 100644 --- a/test/node.test.ts +++ b/test/node.test.ts @@ -24,7 +24,7 @@ test("node.exit() from catch surrounded by while", () => { new BlockStmt([new TryStmt(new BlockStmt([]), catchClause)]) ); - new FunctionDecl("name", [], new BlockStmt([whileStmt])); + new FunctionDecl("name", [], new BlockStmt([whileStmt]), false, false); const exit = catchClause.exit();