ts2workflows 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,18 +2,18 @@ import { AST_NODE_TYPES } from '@typescript-eslint/typescript-estree';
2
2
  import { AssignStepAST, CallStepAST, ForStepAST, JumpTargetAST, NextStepAST, ParallelStepAST, RaiseStepAST, ReturnStepAST, StepsStepAST, SwitchStepAST, TryStepAST, } from '../ast/steps.js';
3
3
  import { BinaryExpression, FunctionInvocationExpression, PrimitiveExpression, VariableReferenceExpression, isExpression, isFullyQualifiedName, isLiteral, } from '../ast/expressions.js';
4
4
  import { InternalTranspilingError, WorkflowSyntaxError } from '../errors.js';
5
- import { isRecord } from '../utils.js';
5
+ import { flatMapPair, isRecord, mapRecordValues } from '../utils.js';
6
6
  import { transformAST } from './transformations.js';
7
- import { convertExpression, convertMemberExpression, convertObjectExpression, convertObjectAsExpressionValues, isMagicFunction, throwIfSpread, } from './expressions.js';
7
+ import { convertExpression, convertMemberExpression, convertObjectExpression, convertObjectAsExpressionValues, isMagicFunction, throwIfSpread, isMagicFunctionStatmentOnly, asExpression, } from './expressions.js';
8
8
  import { blockingFunctions } from './generated/functionMetadata.js';
9
9
  export function parseStatement(node, ctx, postSteps) {
10
- const steps = parseStatementRecursively(node, ctx);
10
+ const steps = parseStatementRecursively(node, undefined, ctx);
11
11
  return transformAST(steps.concat(postSteps ?? []));
12
12
  }
13
- function parseStatementRecursively(node, ctx) {
13
+ function parseStatementRecursively(node, nextNode, ctx) {
14
14
  switch (node.type) {
15
15
  case AST_NODE_TYPES.BlockStatement:
16
- return node.body.flatMap((statement) => parseStatementRecursively(statement, ctx));
16
+ return flatMapPair(node.body, (statement, nextStatement) => parseStatementRecursively(statement, nextStatement, ctx));
17
17
  case AST_NODE_TYPES.VariableDeclaration:
18
18
  return convertVariableDeclarations(node.declarations, ctx);
19
19
  case AST_NODE_TYPES.ExpressionStatement:
@@ -21,13 +21,13 @@ function parseStatementRecursively(node, ctx) {
21
21
  return assignmentExpressionToSteps(node.expression, ctx);
22
22
  }
23
23
  else if (node.expression.type === AST_NODE_TYPES.CallExpression) {
24
- return [callExpressionToStep(node.expression, undefined, ctx)];
24
+ return callExpressionToStep(node.expression, undefined, ctx);
25
25
  }
26
26
  else {
27
27
  return [generalExpressionToAssignStep(node.expression)];
28
28
  }
29
29
  case AST_NODE_TYPES.ReturnStatement:
30
- return [returnStatementToReturnStep(node)];
30
+ return [returnStatementToReturnStep(node, ctx)];
31
31
  case AST_NODE_TYPES.ThrowStatement:
32
32
  return [throwStatementToRaiseStep(node)];
33
33
  case AST_NODE_TYPES.IfStatement:
@@ -46,8 +46,14 @@ function parseStatementRecursively(node, ctx) {
46
46
  return [breakStatementToNextStep(node, ctx)];
47
47
  case AST_NODE_TYPES.ContinueStatement:
48
48
  return [continueStatementToNextStep(node, ctx)];
49
- case AST_NODE_TYPES.TryStatement:
50
- return [tryStatementToTryStep(node, ctx)];
49
+ case AST_NODE_TYPES.TryStatement: {
50
+ let retryPolicy = undefined;
51
+ if (nextNode?.type === AST_NODE_TYPES.ExpressionStatement &&
52
+ nextNode.expression.type === AST_NODE_TYPES.CallExpression) {
53
+ retryPolicy = parseRetryPolicy(nextNode.expression);
54
+ }
55
+ return tryStatementToTrySteps(node, retryPolicy, ctx);
56
+ }
51
57
  case AST_NODE_TYPES.LabeledStatement:
52
58
  return labeledStep(node, ctx);
53
59
  case AST_NODE_TYPES.EmptyStatement:
@@ -64,22 +70,25 @@ function parseStatementRecursively(node, ctx) {
64
70
  }
65
71
  }
66
72
  function convertVariableDeclarations(declarations, ctx) {
67
- return declarations.map((decl) => {
68
- if (decl.type !== AST_NODE_TYPES.VariableDeclarator) {
69
- throw new WorkflowSyntaxError('Not a VariableDeclarator', decl.loc);
70
- }
73
+ return declarations.flatMap((decl) => {
71
74
  if (decl.id.type !== AST_NODE_TYPES.Identifier) {
72
75
  throw new WorkflowSyntaxError('Expected Identifier', decl.loc);
73
76
  }
74
77
  const targetName = decl.id.name;
75
78
  if (decl.init?.type === AST_NODE_TYPES.CallExpression) {
79
+ const calleeName = decl.init.callee.type === AST_NODE_TYPES.Identifier
80
+ ? decl.init.callee.name
81
+ : undefined;
82
+ if (calleeName && isMagicFunctionStatmentOnly(calleeName)) {
83
+ throw new WorkflowSyntaxError(`"${calleeName}" can't be called as part of an expression`, decl.init.callee.loc);
84
+ }
76
85
  return callExpressionToStep(decl.init, targetName, ctx);
77
86
  }
78
87
  else {
79
88
  const value = decl.init == null
80
89
  ? new PrimitiveExpression(null)
81
90
  : convertExpression(decl.init);
82
- return new AssignStepAST([[targetName, value]]);
91
+ return [new AssignStepAST([[targetName, value]])];
83
92
  }
84
93
  });
85
94
  }
@@ -123,10 +132,14 @@ function assignmentExpressionToSteps(node, ctx) {
123
132
  if (node.right.type === AST_NODE_TYPES.CallExpression &&
124
133
  node.right.callee.type === AST_NODE_TYPES.Identifier &&
125
134
  isMagicFunction(node.right.callee.name)) {
135
+ const calleeName = node.right.callee.name;
136
+ if (isMagicFunctionStatmentOnly(calleeName)) {
137
+ throw new WorkflowSyntaxError(`"${calleeName}" can't be called as part of an expression`, node.right.callee.loc);
138
+ }
126
139
  const needsTempVariable = compoundOperator === undefined ||
127
140
  node.left.type !== AST_NODE_TYPES.Identifier;
128
141
  const resultVariable = needsTempVariable ? '__temp' : targetName;
129
- steps.push(callExpressionToStep(node.right, resultVariable, ctx));
142
+ steps.push(...callExpressionToStep(node.right, resultVariable, ctx));
130
143
  if (!needsTempVariable) {
131
144
  return steps;
132
145
  }
@@ -147,20 +160,25 @@ function callExpressionToStep(node, resultVariable, ctx) {
147
160
  const calleeName = calleeExpression.toString();
148
161
  if (calleeName === 'parallel') {
149
162
  // A custom implementation for "parallel"
150
- return callExpressionToParallelStep(node, ctx);
163
+ return [callExpressionToParallelStep(node, ctx)];
151
164
  }
152
165
  else if (calleeName === 'retry_policy') {
153
- return callExpressionToCallStep(calleeName, node.arguments, resultVariable);
166
+ // retry_policy() is handled by AST_NODE_TYPES.TryStatement and therefore ignored here
167
+ return [];
154
168
  }
155
169
  else if (calleeName === 'call_step') {
156
- return createCallStep(node.arguments, resultVariable);
170
+ return [createCallStep(node, node.arguments, resultVariable)];
157
171
  }
158
172
  else if (blockingFunctions.has(calleeName)) {
159
173
  const argumentNames = blockingFunctions.get(calleeName) ?? [];
160
- return blockingFunctionCallStep(calleeName, argumentNames, node.arguments, resultVariable);
174
+ return [
175
+ blockingFunctionCallStep(calleeName, argumentNames, node.arguments, resultVariable),
176
+ ];
161
177
  }
162
178
  else {
163
- return callExpressionAssignStep(calleeName, node.arguments, resultVariable);
179
+ return [
180
+ callExpressionAssignStep(calleeName, node.arguments, resultVariable),
181
+ ];
164
182
  }
165
183
  }
166
184
  else {
@@ -176,17 +194,9 @@ function callExpressionAssignStep(functionName, argumentsNode, resultVariable) {
176
194
  ],
177
195
  ]);
178
196
  }
179
- function callExpressionToCallStep(functionName, argumentsNode, resultVariable) {
180
- if (argumentsNode.length < 1 ||
181
- argumentsNode[0].type !== AST_NODE_TYPES.ObjectExpression) {
182
- throw new WorkflowSyntaxError('Expected one object parameter', argumentsNode[0].loc);
183
- }
184
- const workflowArguments = convertObjectAsExpressionValues(argumentsNode[0]);
185
- return new CallStepAST(functionName, workflowArguments, resultVariable);
186
- }
187
- function createCallStep(argumentsNode, resultVariable) {
197
+ function createCallStep(node, argumentsNode, resultVariable) {
188
198
  if (argumentsNode.length < 1) {
189
- throw new WorkflowSyntaxError('The first argument must be a Function', argumentsNode[0].loc);
199
+ throw new WorkflowSyntaxError('The first argument must be a Function', node.loc);
190
200
  }
191
201
  let functionName;
192
202
  if (argumentsNode[0].type === AST_NODE_TYPES.Identifier) {
@@ -320,8 +330,13 @@ function parseParallelOptions(node) {
320
330
  function generalExpressionToAssignStep(node) {
321
331
  return new AssignStepAST([['__temp', convertExpression(node)]]);
322
332
  }
323
- function returnStatementToReturnStep(node) {
333
+ function returnStatementToReturnStep(node, ctx) {
324
334
  const value = node.argument ? convertExpression(node.argument) : undefined;
335
+ if (ctx.finalizerTargets && ctx.finalizerTargets.length > 0) {
336
+ // If we are in try statement with a finally block, return statements are
337
+ // replaced by a jump to finally back with a captured return value.
338
+ return delayedReturnAndJumpToFinalizer(value, ctx);
339
+ }
325
340
  return new ReturnStepAST(value);
326
341
  }
327
342
  function throwStatementToRaiseStep(node) {
@@ -457,6 +472,11 @@ function doWhileStatementSteps(node, ctx) {
457
472
  return steps;
458
473
  }
459
474
  function breakStatementToNextStep(node, ctx) {
475
+ if (ctx.finalizerTargets) {
476
+ // TODO: would need to detect if this breaks out of the try or catch block,
477
+ // execute the finally block first and then do the break.
478
+ throw new WorkflowSyntaxError('break is not supported inside a try-finally block', node.loc);
479
+ }
460
480
  let target;
461
481
  if (node.label) {
462
482
  target = node.label.name;
@@ -470,6 +490,11 @@ function breakStatementToNextStep(node, ctx) {
470
490
  return new NextStepAST(target);
471
491
  }
472
492
  function continueStatementToNextStep(node, ctx) {
493
+ if (ctx.finalizerTargets) {
494
+ // TODO: would need to detect if continue breaks out of the try or catch block,
495
+ // execute the finally block first and then do the continue.
496
+ throw new WorkflowSyntaxError('continue is not supported inside a try-finally block', node.loc);
497
+ }
473
498
  let target;
474
499
  if (node.label) {
475
500
  target = node.label.name;
@@ -482,25 +507,129 @@ function continueStatementToNextStep(node, ctx) {
482
507
  }
483
508
  return new NextStepAST(target);
484
509
  }
485
- function tryStatementToTryStep(node, ctx) {
486
- const steps = parseStatement(node.block, ctx);
487
- let exceptSteps = [];
510
+ function tryStatementToTrySteps(node, retryPolicy, ctx) {
511
+ if (!node.finalizer) {
512
+ // Basic try-catch without a finally block
513
+ const baseTryStep = parseTryCatchRetry(node, ctx, retryPolicy);
514
+ return [baseTryStep];
515
+ }
516
+ else {
517
+ // Try-finally is translated to two nested try blocks. The innermost try is
518
+ // the actual try body with control flow statements (return, in the future
519
+ // also break/continue) replaced by jumps to the finally block.
520
+ //
521
+ // The outer try's catch block saved the exception and continues to the
522
+ // finally block.
523
+ //
524
+ // The nested try blocks are followed by the finally block and a swith for
525
+ // checking if we need to perform a delayed return/raise.
526
+ const startOfFinalizer = new JumpTargetAST();
527
+ const targets = ctx.finalizerTargets ?? [];
528
+ targets.push(startOfFinalizer.label);
529
+ ctx = Object.assign({}, ctx, { finalizerTargets: targets });
530
+ const [conditionVariable, valueVariable] = finalizerVariables(ctx);
531
+ const innerTry = parseTryCatchRetry(node, ctx, retryPolicy);
532
+ const outerTry = new TryStepAST([innerTry], finalizerDelayedException('__fin_exc', conditionVariable, valueVariable), undefined, '__fin_exc');
533
+ // Reset ctx before parsing the finally block because we don't want to
534
+ // transform returns in finally block in to delayed returns
535
+ if (ctx.finalizerTargets && ctx.finalizerTargets.length <= 1) {
536
+ delete ctx.finalizerTargets;
537
+ }
538
+ else {
539
+ ctx.finalizerTargets?.pop();
540
+ }
541
+ const finallyBlock = parseStatement(node.finalizer, ctx);
542
+ return [
543
+ finalizerInitializer(conditionVariable, valueVariable),
544
+ outerTry,
545
+ startOfFinalizer,
546
+ ...finallyBlock,
547
+ finalizerFooter(conditionVariable, valueVariable),
548
+ ];
549
+ }
550
+ }
551
+ function finalizerVariables(ctx) {
552
+ const targets = ctx.finalizerTargets ?? [];
553
+ const nestingLevel = targets.length > 0 ? `${targets.length}` : '';
554
+ const conditionVariable = `__t2w_finally_condition${nestingLevel}`;
555
+ const valueVariable = `__t2w_finally_value${nestingLevel}`;
556
+ return [conditionVariable, valueVariable];
557
+ }
558
+ function parseTryCatchRetry(node, ctx, retryPolicy) {
559
+ const trySteps = parseStatement(node.block, ctx);
560
+ let exceptSteps = undefined;
488
561
  let errorVariable = undefined;
489
562
  if (node.handler) {
490
563
  exceptSteps = parseStatement(node.handler.body, ctx);
491
- const handlerParam = node.handler.param;
492
- if (handlerParam) {
493
- if (handlerParam.type !== AST_NODE_TYPES.Identifier) {
494
- throw new WorkflowSyntaxError('The error variable must be an identifier', handlerParam.loc);
495
- }
496
- errorVariable = handlerParam.name;
497
- }
564
+ errorVariable = extractErrorVariableName(node.handler.param);
498
565
  }
499
- if (node.finalizer !== null) {
500
- // TODO
501
- throw new WorkflowSyntaxError('finally block not yet supported', node.finalizer.loc);
566
+ const baseTryStep = new TryStepAST(trySteps, exceptSteps, retryPolicy, errorVariable);
567
+ return baseTryStep;
568
+ }
569
+ function extractErrorVariableName(param) {
570
+ if (!param) {
571
+ return undefined;
572
+ }
573
+ if (param.type !== AST_NODE_TYPES.Identifier) {
574
+ throw new WorkflowSyntaxError('The error variable must be an identifier', param.loc);
502
575
  }
503
- return new TryStepAST(steps, exceptSteps, undefined, errorVariable);
576
+ return param.name;
577
+ }
578
+ /**
579
+ * The shared header for try-finally for initializing the temp variables
580
+ */
581
+ function finalizerInitializer(conditionVariable, valueVariable) {
582
+ return new AssignStepAST([
583
+ [conditionVariable, new PrimitiveExpression(null)],
584
+ [valueVariable, new PrimitiveExpression(null)],
585
+ ]);
586
+ }
587
+ /**
588
+ * The shared footer of a finally block that re-throws the exception or
589
+ * returns the value returned by the try body.
590
+ *
591
+ * The footer code in TypeScript:
592
+ *
593
+ * if (__t2w_finally_condition == "return") {
594
+ * return __t2w_finally_value
595
+ * } elseif (__t2w_finally_condition == "raise") {
596
+ * throw __t2w_finally_value
597
+ * }
598
+ */
599
+ function finalizerFooter(conditionVariable, valueVariable) {
600
+ return new SwitchStepAST([
601
+ {
602
+ condition: new BinaryExpression(new VariableReferenceExpression(conditionVariable), '==', new PrimitiveExpression('return')),
603
+ steps: [
604
+ new ReturnStepAST(new VariableReferenceExpression(valueVariable)),
605
+ ],
606
+ },
607
+ {
608
+ condition: new BinaryExpression(new VariableReferenceExpression(conditionVariable), '==', new PrimitiveExpression('raise')),
609
+ steps: [new RaiseStepAST(new VariableReferenceExpression(valueVariable))],
610
+ },
611
+ ]);
612
+ }
613
+ function finalizerDelayedException(exceptionVariableName, conditionVariableName, valueVariableName) {
614
+ return [
615
+ new AssignStepAST([
616
+ [conditionVariableName, new PrimitiveExpression('raise')],
617
+ [
618
+ valueVariableName,
619
+ new VariableReferenceExpression(exceptionVariableName),
620
+ ],
621
+ ]),
622
+ ];
623
+ }
624
+ function delayedReturnAndJumpToFinalizer(value, ctx) {
625
+ const finalizerTarget = ctx.finalizerTargets && ctx.finalizerTargets.length > 0
626
+ ? ctx.finalizerTargets[ctx.finalizerTargets.length - 1]
627
+ : undefined;
628
+ const [conditionVariable, valueVariable] = finalizerVariables(ctx);
629
+ return new AssignStepAST([
630
+ [conditionVariable, new PrimitiveExpression('return')],
631
+ [valueVariable, value ?? new PrimitiveExpression(null)],
632
+ ], finalizerTarget);
504
633
  }
505
634
  function labeledStep(node, ctx) {
506
635
  const steps = parseStatement(node.body, ctx);
@@ -509,3 +638,66 @@ function labeledStep(node, ctx) {
509
638
  }
510
639
  return steps;
511
640
  }
641
+ function parseRetryPolicy(node) {
642
+ const callee = node.callee;
643
+ if (callee.type !== AST_NODE_TYPES.Identifier ||
644
+ callee.name !== 'retry_policy') {
645
+ // Ignore everything else besides retry_policy()
646
+ return undefined;
647
+ }
648
+ if (node.arguments.length < 1) {
649
+ throw new WorkflowSyntaxError('Required argument missing', node.loc);
650
+ }
651
+ const arg0 = throwIfSpread(node.arguments).map(convertExpression)[0];
652
+ const argsLoc = node.arguments[0].loc;
653
+ if (isFullyQualifiedName(arg0)) {
654
+ return arg0.toString();
655
+ }
656
+ else if (arg0.expressionType === 'primitive' && isRecord(arg0.value)) {
657
+ return retryPolicyFromParams(arg0.value, argsLoc);
658
+ }
659
+ else {
660
+ throw new WorkflowSyntaxError('Unexpected type', argsLoc);
661
+ }
662
+ }
663
+ function retryPolicyFromParams(paramsObject, argsLoc) {
664
+ const params = mapRecordValues(paramsObject, asExpression);
665
+ if ('backoff' in params) {
666
+ let predicate = '';
667
+ const predicateEx = params.predicate;
668
+ if (predicateEx === undefined) {
669
+ predicate = undefined;
670
+ }
671
+ else if (isFullyQualifiedName(predicateEx)) {
672
+ predicate = predicateEx.toString();
673
+ }
674
+ else {
675
+ throw new WorkflowSyntaxError('"predicate" must be a function name', argsLoc);
676
+ }
677
+ const backoffEx = params.backoff;
678
+ if (backoffEx.expressionType === 'primitive' && isRecord(backoffEx.value)) {
679
+ const backoffLit = backoffEx.value;
680
+ return {
681
+ predicate,
682
+ maxRetries: params.max_retries,
683
+ backoff: {
684
+ initialDelay: backoffLit.initial_delay
685
+ ? asExpression(backoffLit.initial_delay)
686
+ : undefined,
687
+ maxDelay: backoffLit.max_delay
688
+ ? asExpression(backoffLit.max_delay)
689
+ : undefined,
690
+ multiplier: backoffLit.multiplier
691
+ ? asExpression(backoffLit.multiplier)
692
+ : undefined,
693
+ },
694
+ };
695
+ }
696
+ else {
697
+ throw new WorkflowSyntaxError('Expected an object literal', argsLoc);
698
+ }
699
+ }
700
+ else {
701
+ throw new WorkflowSyntaxError('Some required retry policy parameters are missing', argsLoc);
702
+ }
703
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"transformations.d.ts","sourceRoot":"","sources":["../../src/transpiler/transformations.ts"],"names":[],"mappings":"AAAA,OAAO,EAUL,eAAe,EAChB,MAAM,iBAAiB,CAAA;AAuBxB;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAUxE;AA4JD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,eAAe,EAAE,GACvB,eAAe,EAAE,CAqBnB"}
1
+ {"version":3,"file":"transformations.d.ts","sourceRoot":"","sources":["../../src/transpiler/transformations.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,eAAe,EAChB,MAAM,iBAAiB,CAAA;AAqBxB;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,eAAe,EAAE,GAAG,eAAe,EAAE,CAQxE;AAiCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CACxC,KAAK,EAAE,eAAe,EAAE,GACvB,eAAe,EAAE,CAqBnB"}