@fincity/kirun-js 3.1.4 → 3.2.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.
- package/__tests__/engine/dsl/GraphDebugTest.ts +316 -0
- package/__tests__/engine/runtime/expression/ExpressionParsingTest.ts +402 -14
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/module.js +15 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +416 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/engine/dsl/DSLCompiler.ts +104 -0
- package/src/engine/dsl/index.ts +30 -0
- package/src/engine/dsl/lexer/DSLLexer.ts +518 -0
- package/src/engine/dsl/lexer/DSLToken.ts +74 -0
- package/src/engine/dsl/lexer/Keywords.ts +80 -0
- package/src/engine/dsl/lexer/LexerError.ts +37 -0
- package/src/engine/dsl/monaco/DSLFunctionProvider.ts +187 -0
- package/src/engine/dsl/parser/DSLParser.ts +1075 -0
- package/src/engine/dsl/parser/DSLParserError.ts +29 -0
- package/src/engine/dsl/parser/ast/ASTNode.ts +23 -0
- package/src/engine/dsl/parser/ast/ArgumentNode.ts +43 -0
- package/src/engine/dsl/parser/ast/ComplexValueNode.ts +22 -0
- package/src/engine/dsl/parser/ast/EventDeclNode.ts +27 -0
- package/src/engine/dsl/parser/ast/ExpressionNode.ts +33 -0
- package/src/engine/dsl/parser/ast/FunctionCallNode.ts +29 -0
- package/src/engine/dsl/parser/ast/FunctionDefNode.ts +37 -0
- package/src/engine/dsl/parser/ast/ParameterDeclNode.ts +25 -0
- package/src/engine/dsl/parser/ast/SchemaLiteralNode.ts +26 -0
- package/src/engine/dsl/parser/ast/SchemaNode.ts +23 -0
- package/src/engine/dsl/parser/ast/StatementNode.ts +41 -0
- package/src/engine/dsl/parser/ast/index.ts +14 -0
- package/src/engine/dsl/transformer/ASTToJSON.ts +378 -0
- package/src/engine/dsl/transformer/ExpressionHandler.ts +48 -0
- package/src/engine/dsl/transformer/JSONToText.ts +694 -0
- package/src/engine/dsl/transformer/SchemaTransformer.ts +110 -0
- package/src/engine/model/FunctionDefinition.ts +23 -0
- package/src/engine/runtime/expression/Expression.ts +5 -1
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +152 -139
- package/src/engine/runtime/expression/ExpressionParser.ts +80 -27
- package/src/engine/util/duplicate.ts +3 -1
- package/src/index.ts +1 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ArgumentNode,
|
|
3
|
+
ComplexValueNode,
|
|
4
|
+
EventDeclNode,
|
|
5
|
+
ExpressionNode,
|
|
6
|
+
FunctionDefNode,
|
|
7
|
+
ParameterDeclNode,
|
|
8
|
+
SchemaLiteralNode,
|
|
9
|
+
StatementNode,
|
|
10
|
+
} from '../parser/ast';
|
|
11
|
+
import { SchemaTransformer } from './SchemaTransformer';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* AST to JSON Transformer
|
|
15
|
+
* Converts parsed AST to FunctionDefinition JSON
|
|
16
|
+
*/
|
|
17
|
+
export class ASTToJSONTransformer {
|
|
18
|
+
/**
|
|
19
|
+
* Transform AST to FunctionDefinition JSON
|
|
20
|
+
* Only includes non-empty/non-default fields
|
|
21
|
+
*/
|
|
22
|
+
public transform(ast: FunctionDefNode): any {
|
|
23
|
+
const json: any = {
|
|
24
|
+
name: ast.name,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Only add namespace if not empty
|
|
28
|
+
if (ast.namespace) {
|
|
29
|
+
json.namespace = ast.namespace;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Only add parameters if not empty
|
|
33
|
+
const parameters = this.transformParameters(ast.parameters);
|
|
34
|
+
if (Object.keys(parameters).length > 0) {
|
|
35
|
+
json.parameters = parameters;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Only add events if not empty
|
|
39
|
+
const events = this.transformEvents(ast.events);
|
|
40
|
+
if (Object.keys(events).length > 0) {
|
|
41
|
+
json.events = events;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Always add steps
|
|
45
|
+
json.steps = this.transformSteps(ast.logic);
|
|
46
|
+
|
|
47
|
+
return json;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Transform parameters
|
|
52
|
+
*/
|
|
53
|
+
private transformParameters(params: ParameterDeclNode[]): any {
|
|
54
|
+
const result: any = {};
|
|
55
|
+
for (const param of params) {
|
|
56
|
+
result[param.name] = {
|
|
57
|
+
parameterName: param.name,
|
|
58
|
+
schema: SchemaTransformer.transform(param.schema.schemaSpec),
|
|
59
|
+
variableArgument: false,
|
|
60
|
+
type: 'EXPRESSION',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Transform events
|
|
68
|
+
*/
|
|
69
|
+
private transformEvents(events: EventDeclNode[]): any {
|
|
70
|
+
const result: any = {};
|
|
71
|
+
for (const event of events) {
|
|
72
|
+
result[event.name] = {
|
|
73
|
+
name: event.name,
|
|
74
|
+
parameters: this.transformEventParameters(event.parameters),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Transform event parameters (these are schemas, not Parameter objects)
|
|
82
|
+
*/
|
|
83
|
+
private transformEventParameters(params: ParameterDeclNode[]): any {
|
|
84
|
+
const result: any = {};
|
|
85
|
+
for (const param of params) {
|
|
86
|
+
result[param.name] = SchemaTransformer.transform(param.schema.schemaSpec);
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Transform statements (top-level and nested)
|
|
93
|
+
*/
|
|
94
|
+
private transformSteps(statements: StatementNode[]): any {
|
|
95
|
+
const result: any = {};
|
|
96
|
+
for (const stmt of statements) {
|
|
97
|
+
result[stmt.statementName] = this.transformStatement(stmt);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Transform single statement
|
|
104
|
+
* Only includes non-empty/non-default fields
|
|
105
|
+
*/
|
|
106
|
+
private transformStatement(stmt: StatementNode): any {
|
|
107
|
+
const json: any = {
|
|
108
|
+
statementName: stmt.statementName,
|
|
109
|
+
namespace: stmt.functionCall.namespace,
|
|
110
|
+
name: stmt.functionCall.name,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Only add parameterMap if not empty
|
|
114
|
+
const parameterMap = this.transformParameterMap(stmt.functionCall.argumentsMap);
|
|
115
|
+
if (Object.keys(parameterMap).length > 0) {
|
|
116
|
+
json.parameterMap = parameterMap;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Only add dependentStatements if not empty
|
|
120
|
+
const dependentStatements = this.createDependentStatementsMap(stmt);
|
|
121
|
+
if (Object.keys(dependentStatements).length > 0) {
|
|
122
|
+
json.dependentStatements = dependentStatements;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Only add executeIftrue if not empty
|
|
126
|
+
const executeIftrue = this.createExecuteIfMap(stmt.executeIfSteps);
|
|
127
|
+
if (Object.keys(executeIftrue).length > 0) {
|
|
128
|
+
json.executeIftrue = executeIftrue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Only add comment if not empty
|
|
132
|
+
if (stmt.comment && stmt.comment.trim()) {
|
|
133
|
+
json.comment = stmt.comment;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return json;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Create dependentStatements map from AFTER clause and nested blocks
|
|
141
|
+
*/
|
|
142
|
+
private createDependentStatementsMap(stmt: StatementNode): any {
|
|
143
|
+
const result: any = {};
|
|
144
|
+
|
|
145
|
+
// Add explicit AFTER dependencies
|
|
146
|
+
for (const stepRef of stmt.afterSteps) {
|
|
147
|
+
result[stepRef] = true; // true = depends on this step
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add implicit dependencies from nested blocks
|
|
151
|
+
// Nested statements depend on their parent's specific events
|
|
152
|
+
for (const [blockName, nestedStmts] of stmt.nestedBlocks) {
|
|
153
|
+
// Create dependency key based on block name
|
|
154
|
+
// e.g., "Steps.loop.iteration", "Steps.if.true"
|
|
155
|
+
const dependencyKey = `Steps.${stmt.statementName}.${blockName}`;
|
|
156
|
+
|
|
157
|
+
// Mark nested statements as depending on this block
|
|
158
|
+
// (These will be added when we transform nested statements)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Create executeIftrue map from IF clause
|
|
166
|
+
*/
|
|
167
|
+
private createExecuteIfMap(executeIfSteps: string[]): any {
|
|
168
|
+
const result: any = {};
|
|
169
|
+
for (const stepRef of executeIfSteps) {
|
|
170
|
+
result[stepRef] = true;
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Transform parameter map (function arguments)
|
|
177
|
+
* Skips parameters with empty values
|
|
178
|
+
* Supports multi-value parameters
|
|
179
|
+
*/
|
|
180
|
+
private transformParameterMap(argsMap: Map<string, ArgumentNode>): any {
|
|
181
|
+
const result: any = {};
|
|
182
|
+
|
|
183
|
+
for (const [paramName, arg] of argsMap) {
|
|
184
|
+
// Skip parameters with empty values
|
|
185
|
+
if (this.isEmptyArgument(arg)) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
result[paramName] = {};
|
|
190
|
+
|
|
191
|
+
// Handle multi-value parameters
|
|
192
|
+
if (arg.isMultiValue()) {
|
|
193
|
+
for (let i = 0; i < arg.values.length; i++) {
|
|
194
|
+
const paramRef = this.transformArgumentValue(arg.values[i], i + 1);
|
|
195
|
+
result[paramName][paramRef.key] = paramRef;
|
|
196
|
+
}
|
|
197
|
+
} else {
|
|
198
|
+
const paramRef = this.transformArgumentValue(arg.value, 1);
|
|
199
|
+
result[paramName][paramRef.key] = paramRef;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Check if an argument is empty (no value provided)
|
|
208
|
+
* Note: Empty expressions and undefined values are valid and should NOT be filtered out
|
|
209
|
+
* Only filter out parser artifacts like stray delimiters
|
|
210
|
+
*/
|
|
211
|
+
private isEmptyArgument(arg: ArgumentNode): boolean {
|
|
212
|
+
if (arg.value instanceof ExpressionNode) {
|
|
213
|
+
const expr = arg.value.expressionText.trim();
|
|
214
|
+
// Only filter out parser artifacts (stray delimiters)
|
|
215
|
+
// Empty expressions ('') are valid and should be preserved
|
|
216
|
+
return expr === ',' || expr === ')';
|
|
217
|
+
}
|
|
218
|
+
// Both null and undefined are valid values and should be preserved
|
|
219
|
+
// ComplexValueNode is never "empty" - it always has a value (even if null/undefined)
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Transform single argument value to ParameterReference
|
|
225
|
+
* @param value The argument value (expression, complex value, or schema literal)
|
|
226
|
+
* @param order The order index for multi-value parameters (1-based)
|
|
227
|
+
*/
|
|
228
|
+
private transformArgumentValue(
|
|
229
|
+
value: ExpressionNode | ComplexValueNode | SchemaLiteralNode,
|
|
230
|
+
order: number,
|
|
231
|
+
): any {
|
|
232
|
+
const key = this.generateUUID();
|
|
233
|
+
|
|
234
|
+
if (value instanceof ExpressionNode) {
|
|
235
|
+
return {
|
|
236
|
+
key,
|
|
237
|
+
type: 'EXPRESSION',
|
|
238
|
+
expression: value.expressionText,
|
|
239
|
+
value: undefined,
|
|
240
|
+
order: order,
|
|
241
|
+
};
|
|
242
|
+
} else if (value instanceof ComplexValueNode) {
|
|
243
|
+
return {
|
|
244
|
+
key,
|
|
245
|
+
type: 'VALUE',
|
|
246
|
+
value: value.value,
|
|
247
|
+
expression: undefined,
|
|
248
|
+
order: order,
|
|
249
|
+
};
|
|
250
|
+
} else if (value instanceof SchemaLiteralNode) {
|
|
251
|
+
// Schema literal with optional default value
|
|
252
|
+
const schema = SchemaTransformer.transform(value.schema.schemaSpec);
|
|
253
|
+
|
|
254
|
+
if (value.defaultValue) {
|
|
255
|
+
// Has default value - evaluate it
|
|
256
|
+
return {
|
|
257
|
+
key,
|
|
258
|
+
type: 'VALUE',
|
|
259
|
+
value: this.evaluateDefaultValue(value.defaultValue, schema),
|
|
260
|
+
expression: undefined,
|
|
261
|
+
order: order,
|
|
262
|
+
};
|
|
263
|
+
} else {
|
|
264
|
+
// No default value - just pass the schema
|
|
265
|
+
return {
|
|
266
|
+
key,
|
|
267
|
+
type: 'VALUE',
|
|
268
|
+
value: schema,
|
|
269
|
+
expression: undefined,
|
|
270
|
+
order: order,
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fallback
|
|
276
|
+
return {
|
|
277
|
+
key,
|
|
278
|
+
type: 'VALUE',
|
|
279
|
+
value: null,
|
|
280
|
+
expression: undefined,
|
|
281
|
+
order: order,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Evaluate default value for schema literal
|
|
287
|
+
*/
|
|
288
|
+
private evaluateDefaultValue(defaultValueExpr: ExpressionNode, schema: any): any {
|
|
289
|
+
const exprText = defaultValueExpr.expressionText.trim();
|
|
290
|
+
|
|
291
|
+
// Try to parse as literal value
|
|
292
|
+
if (exprText === '[]') {
|
|
293
|
+
return [];
|
|
294
|
+
}
|
|
295
|
+
if (exprText === '{}') {
|
|
296
|
+
return {};
|
|
297
|
+
}
|
|
298
|
+
if (exprText === 'null') {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
if (exprText === 'true') {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
if (exprText === 'false') {
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Try to parse as number
|
|
309
|
+
const num = parseFloat(exprText);
|
|
310
|
+
if (!isNaN(num)) {
|
|
311
|
+
return num;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Try to parse as string literal
|
|
315
|
+
if (
|
|
316
|
+
(exprText.startsWith('"') && exprText.endsWith('"')) ||
|
|
317
|
+
(exprText.startsWith("'") && exprText.endsWith("'"))
|
|
318
|
+
) {
|
|
319
|
+
return exprText.slice(1, -1);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Otherwise, return as-is (might be an expression)
|
|
323
|
+
return exprText;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Generate UUID for parameter reference keys
|
|
328
|
+
*/
|
|
329
|
+
private generateUUID(): string {
|
|
330
|
+
// Simple UUID v4 generation
|
|
331
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
332
|
+
const r = (Math.random() * 16) | 0;
|
|
333
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
334
|
+
return v.toString(16);
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Flatten nested blocks into steps
|
|
340
|
+
* This adds all nested statements to the top-level steps object
|
|
341
|
+
* and sets their dependentStatements appropriately
|
|
342
|
+
*/
|
|
343
|
+
public flattenNestedBlocks(ast: FunctionDefNode): void {
|
|
344
|
+
const allStatements: StatementNode[] = [...ast.logic];
|
|
345
|
+
|
|
346
|
+
// Process each top-level statement
|
|
347
|
+
for (const stmt of ast.logic) {
|
|
348
|
+
this.collectNestedStatements(stmt, allStatements);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Update ast.logic with all statements (flattened)
|
|
352
|
+
ast.logic = allStatements;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Recursively collect nested statements
|
|
357
|
+
* NOTE: Nesting is implicit (derived from expression analysis)
|
|
358
|
+
* We do NOT add block dependencies to dependentStatements
|
|
359
|
+
* Only explicit AFTER clauses become dependentStatements
|
|
360
|
+
*/
|
|
361
|
+
private collectNestedStatements(stmt: StatementNode, allStatements: StatementNode[]): void {
|
|
362
|
+
for (const [, nestedStmts] of stmt.nestedBlocks) {
|
|
363
|
+
for (const nestedStmt of nestedStmts) {
|
|
364
|
+
// Do NOT add implicit block dependency - nesting is derived from expressions
|
|
365
|
+
// Only explicit AFTER clauses should become dependentStatements
|
|
366
|
+
|
|
367
|
+
// Add to all statements
|
|
368
|
+
allStatements.push(nestedStmt);
|
|
369
|
+
|
|
370
|
+
// Recursively process this statement's nested blocks
|
|
371
|
+
this.collectNestedStatements(nestedStmt, allStatements);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Clear nested blocks after flattening
|
|
376
|
+
stmt.nestedBlocks.clear();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Expression } from '../../runtime/expression/Expression';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Expression Handler
|
|
5
|
+
* Utilities for working with KIRun expressions
|
|
6
|
+
*/
|
|
7
|
+
export class ExpressionHandler {
|
|
8
|
+
/**
|
|
9
|
+
* Parse expression text using KIRun Expression parser
|
|
10
|
+
*/
|
|
11
|
+
public static parse(expressionText: string): Expression {
|
|
12
|
+
return new Expression(expressionText);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Validate expression syntax
|
|
17
|
+
*/
|
|
18
|
+
public static validate(expressionText: string): boolean {
|
|
19
|
+
try {
|
|
20
|
+
new Expression(expressionText);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Check if a value is an expression (has isExpression flag)
|
|
29
|
+
*/
|
|
30
|
+
public static isExpression(value: any): boolean {
|
|
31
|
+
return (
|
|
32
|
+
value &&
|
|
33
|
+
typeof value === 'object' &&
|
|
34
|
+
value.isExpression === true &&
|
|
35
|
+
typeof value.value === 'string'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract expression text from value object
|
|
41
|
+
*/
|
|
42
|
+
public static extractExpressionText(value: any): string | null {
|
|
43
|
+
if (this.isExpression(value)) {
|
|
44
|
+
return value.value;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|