@fincity/kirun-js 2.16.2 → 3.0.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/runtime/expression/ExpressionEvaluatorLengthSubtractionBugTest.ts +384 -0
- package/__tests__/engine/runtime/expression/ExpressionEvaluatorLengthSubtractionTest.ts +338 -0
- package/__tests__/engine/runtime/expression/ExpressionTest.ts +7 -5
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/module.js +1 -1
- package/dist/module.js.map +1 -1
- package/dist/types.d.ts +56 -39
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/engine/function/system/context/SetFunction.ts +226 -75
- package/src/engine/runtime/expression/Expression.ts +267 -71
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +317 -22
- package/src/engine/runtime/expression/ExpressionLexer.ts +365 -0
- package/src/engine/runtime/expression/ExpressionParser.ts +541 -0
- package/src/engine/runtime/expression/ExpressionParserDebug.ts +21 -0
- package/src/engine/runtime/expression/Operation.ts +1 -0
- package/src/engine/runtime/expression/tokenextractor/ObjectValueSetterExtractor.ts +134 -31
- package/src/engine/runtime/expression/tokenextractor/TokenValueExtractor.ts +32 -8
- package/src/engine/util/LinkedList.ts +1 -1
|
@@ -7,6 +7,7 @@ import { ExpressionEvaluationException } from './exception/ExpressionEvaluationE
|
|
|
7
7
|
import { ExpressionToken } from './ExpressionToken';
|
|
8
8
|
import { ExpressionTokenValue } from './ExpressionTokenValue';
|
|
9
9
|
import { Operation } from './Operation';
|
|
10
|
+
import { ExpressionParser } from './ExpressionParser';
|
|
10
11
|
|
|
11
12
|
export class Expression extends ExpressionToken {
|
|
12
13
|
// Data structure for storing tokens
|
|
@@ -18,27 +19,88 @@ export class Expression extends ExpressionToken {
|
|
|
18
19
|
private cachedTokensArray?: ExpressionToken[];
|
|
19
20
|
private cachedOpsArray?: Operation[];
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Create a ternary expression with condition, trueExpr, and falseExpr.
|
|
24
|
+
* Using push() to maintain compatibility with evaluator (push adds to head).
|
|
25
|
+
* Tokens will be in order: [falseExpr, trueExpr, condition] from head to tail.
|
|
26
|
+
*/
|
|
27
|
+
public static createTernary(condition: ExpressionToken, trueExpr: ExpressionToken, falseExpr: ExpressionToken): Expression {
|
|
28
|
+
const expr = new Expression('', undefined, undefined, undefined, true);
|
|
29
|
+
// Push in reverse order so get() returns them correctly
|
|
30
|
+
// push(condition) -> head=condition
|
|
31
|
+
// push(trueExpr) -> head=trueExpr, trueExpr.next=condition
|
|
32
|
+
// push(falseExpr) -> head=falseExpr, falseExpr.next=trueExpr.next=condition
|
|
33
|
+
// get(0)=falseExpr, get(1)=trueExpr, get(2)=condition
|
|
34
|
+
expr.tokens.push(condition);
|
|
35
|
+
expr.tokens.push(trueExpr);
|
|
36
|
+
expr.tokens.push(falseExpr);
|
|
37
|
+
expr.ops.push(Operation.CONDITIONAL_TERNARY_OPERATOR);
|
|
38
|
+
return expr;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Create a leaf expression (identifier/number) without re-parsing.
|
|
43
|
+
* Used by the parser for leaf nodes.
|
|
44
|
+
*/
|
|
45
|
+
public static createLeaf(value: string): Expression {
|
|
46
|
+
const expr = new Expression('', undefined, undefined, undefined, true);
|
|
47
|
+
expr.expression = value;
|
|
48
|
+
// Push a token for the leaf value - the evaluator needs at least one token
|
|
49
|
+
expr.tokens.push(new ExpressionToken(value));
|
|
50
|
+
return expr;
|
|
51
|
+
}
|
|
52
|
+
|
|
21
53
|
public constructor(
|
|
22
54
|
expression?: string,
|
|
23
55
|
l?: ExpressionToken,
|
|
24
56
|
r?: ExpressionToken,
|
|
25
57
|
op?: Operation,
|
|
58
|
+
skipParsing?: boolean,
|
|
26
59
|
) {
|
|
27
60
|
super(expression ? expression : '');
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
61
|
+
|
|
62
|
+
// If skipParsing is true, don't do anything (used by createLeaf/createTernary)
|
|
63
|
+
if (skipParsing) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// If we have left/right tokens and operation, construct directly (for AST building)
|
|
68
|
+
// Using push() to maintain compatibility with evaluator (push adds to head).
|
|
69
|
+
// For binary ops: push(left), push(right) -> get(0)=right, get(1)=left
|
|
70
|
+
if (l !== undefined || r !== undefined || op !== undefined) {
|
|
71
|
+
if (op?.getOperator() == '..') {
|
|
72
|
+
if (!l) l = new ExpressionTokenValue('', '');
|
|
73
|
+
else if (!r) r = new ExpressionTokenValue('', '');
|
|
74
|
+
}
|
|
75
|
+
if (l) this.tokens.push(l);
|
|
76
|
+
if (r) this.tokens.push(r);
|
|
77
|
+
if (op) this.ops.push(op);
|
|
78
|
+
// For constructed expressions, don't re-parse
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// If we have an expression string, use the new parser
|
|
83
|
+
if (expression && expression.trim().length > 0) {
|
|
84
|
+
try {
|
|
85
|
+
const parser = new ExpressionParser(expression);
|
|
86
|
+
const parsed = parser.parse();
|
|
87
|
+
// Copy tokens and operations from parsed expression
|
|
88
|
+
this.tokens = parsed.getTokens();
|
|
89
|
+
this.ops = parsed.getOperations();
|
|
90
|
+
// Invalidate cache
|
|
91
|
+
this.cachedTokensArray = undefined;
|
|
92
|
+
this.cachedOpsArray = undefined;
|
|
93
|
+
} catch (error) {
|
|
94
|
+
// Fall back to old parser if new one fails (for backward compatibility during migration)
|
|
95
|
+
this.evaluate();
|
|
96
|
+
if (
|
|
97
|
+
!this.ops.isEmpty() &&
|
|
98
|
+
this.ops.peekLast().getOperator() == '..' &&
|
|
99
|
+
this.tokens.length == 1
|
|
100
|
+
) {
|
|
101
|
+
this.tokens.push(new ExpressionToken(''));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
42
104
|
}
|
|
43
105
|
}
|
|
44
106
|
|
|
@@ -442,6 +504,17 @@ export class Expression extends ExpressionToken {
|
|
|
442
504
|
if (!pre1 || !pre2) {
|
|
443
505
|
throw new Error('Unknown operators provided');
|
|
444
506
|
}
|
|
507
|
+
// For left-associative operators with same precedence, combine the previous
|
|
508
|
+
// operation before adding the new one. This ensures 5 - 1 - 2 is parsed as
|
|
509
|
+
// (5-1) - 2, not 5 - (1-2).
|
|
510
|
+
// Exception: OBJECT_OPERATOR and ARRAY_OPERATOR should NOT combine -
|
|
511
|
+
// they need to stay flat for path building (e.g., a.b.c).
|
|
512
|
+
if (pre2 === pre1) {
|
|
513
|
+
return op2 !== Operation.OBJECT_OPERATOR &&
|
|
514
|
+
op2 !== Operation.ARRAY_OPERATOR &&
|
|
515
|
+
op1 !== Operation.OBJECT_OPERATOR &&
|
|
516
|
+
op1 !== Operation.ARRAY_OPERATOR;
|
|
517
|
+
}
|
|
445
518
|
return pre2 < pre1;
|
|
446
519
|
}
|
|
447
520
|
|
|
@@ -461,72 +534,195 @@ export class Expression extends ExpressionToken {
|
|
|
461
534
|
return level === 1; // Should be 1 just before the last ')'
|
|
462
535
|
}
|
|
463
536
|
|
|
537
|
+
/**
|
|
538
|
+
* Simple recursive toString() that walks the AST tree.
|
|
539
|
+
* The tree structure is in the tokens LinkedList:
|
|
540
|
+
* - For Expression('', left, right, op): tokens[0] = left, tokens[1] = right
|
|
541
|
+
* - For leaf nodes: just the expression string
|
|
542
|
+
*/
|
|
464
543
|
public toString(): string {
|
|
544
|
+
// Leaf node: no operations, just return the token
|
|
465
545
|
if (this.ops.isEmpty()) {
|
|
466
|
-
if (this.tokens.size() == 1)
|
|
546
|
+
if (this.tokens.size() == 1) {
|
|
547
|
+
return this.tokenToString(this.tokens.get(0));
|
|
548
|
+
}
|
|
549
|
+
// Handle special case: expression string without parsed structure
|
|
550
|
+
if (this.expression && this.expression.length > 0) {
|
|
551
|
+
return this.expression;
|
|
552
|
+
}
|
|
467
553
|
return 'Error: No tokens';
|
|
468
554
|
}
|
|
469
555
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
556
|
+
// Get the single operation and its operands
|
|
557
|
+
const op = this.ops.get(0);
|
|
558
|
+
|
|
559
|
+
// Unary operation: (operator operand)
|
|
560
|
+
if (op.getOperator().startsWith('UN: ')) {
|
|
561
|
+
return this.formatUnaryOperation(op);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Ternary operation: (condition ? trueExpr : falseExpr)
|
|
565
|
+
if (op == Operation.CONDITIONAL_TERNARY_OPERATOR) {
|
|
566
|
+
return this.formatTernaryOperation();
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Binary operation
|
|
570
|
+
return this.formatBinaryOperation(op);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Format a unary operation: (operator operand)
|
|
575
|
+
*/
|
|
576
|
+
private formatUnaryOperation(op: Operation): string {
|
|
577
|
+
const operand = this.tokens.get(0);
|
|
578
|
+
const operandStr = this.tokenToString(operand);
|
|
579
|
+
return '(' + op.getOperator().substring(4) + operandStr + ')';
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Format a ternary operation: (condition ? trueExpr : falseExpr)
|
|
584
|
+
* Note: With push() order, tokens are [falseExpr, trueExpr, condition]
|
|
585
|
+
*/
|
|
586
|
+
private formatTernaryOperation(): string {
|
|
587
|
+
// With push() order: get(0)=falseExpr, get(1)=trueExpr, get(2)=condition
|
|
588
|
+
const falseExpr = this.tokens.get(0);
|
|
589
|
+
const trueExpr = this.tokens.get(1);
|
|
590
|
+
const condition = this.tokens.get(2);
|
|
591
|
+
|
|
592
|
+
const conditionStr = this.tokenToString(condition);
|
|
593
|
+
const trueStr = this.tokenToString(trueExpr);
|
|
594
|
+
const falseStr = this.tokenToString(falseExpr);
|
|
595
|
+
|
|
596
|
+
return '(' + conditionStr + '?' + trueStr + ':' + falseStr + ')';
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Format a binary operation based on operator type
|
|
601
|
+
* Note: With push() order, tokens are [right, left] (push(left), push(right) -> get(0)=right)
|
|
602
|
+
*/
|
|
603
|
+
private formatBinaryOperation(op: Operation): string {
|
|
604
|
+
// Safety check: ensure we have at least 2 tokens for binary operation
|
|
605
|
+
if (this.tokens.size() < 2) {
|
|
606
|
+
// Fall back to expression string if available
|
|
607
|
+
if (this.expression && this.expression.length > 0) {
|
|
608
|
+
return this.expression;
|
|
609
|
+
}
|
|
610
|
+
// Single token case - just return the token
|
|
611
|
+
if (this.tokens.size() === 1) {
|
|
612
|
+
return this.tokenToString(this.tokens.get(0));
|
|
613
|
+
}
|
|
614
|
+
return 'Error: Invalid binary expression';
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// With push() order: get(0)=right, get(1)=left
|
|
618
|
+
const right = this.tokens.get(0);
|
|
619
|
+
const left = this.tokens.get(1);
|
|
620
|
+
|
|
621
|
+
// ARRAY_OPERATOR: left[index] - NO outer parens, brackets are enough
|
|
622
|
+
if (op == Operation.ARRAY_OPERATOR) {
|
|
623
|
+
const leftStr = this.tokenToString(left);
|
|
624
|
+
const indexStr = this.formatArrayIndex(right);
|
|
625
|
+
return leftStr + '[' + indexStr + ']';
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// OBJECT_OPERATOR: left.right or left.(right) if right has operations
|
|
629
|
+
if (op == Operation.OBJECT_OPERATOR) {
|
|
630
|
+
const leftStr = this.tokenToString(left);
|
|
631
|
+
const rightStr = this.tokenToString(right);
|
|
632
|
+
|
|
633
|
+
// Check what operation the right side has
|
|
634
|
+
if (right instanceof Expression) {
|
|
635
|
+
const rightOps = right.getOperations();
|
|
636
|
+
if (!rightOps.isEmpty()) {
|
|
637
|
+
const rightOp = rightOps.get(0);
|
|
638
|
+
// ARRAY_OPERATOR doesn't add outer parens, so we need to wrap
|
|
639
|
+
// OBJECT_OPERATOR and other ops already add parens in their toString()
|
|
640
|
+
if (rightOp == Operation.ARRAY_OPERATOR) {
|
|
641
|
+
return '(' + leftStr + '.(' + rightStr + '))';
|
|
642
|
+
} else {
|
|
643
|
+
// Other ops (like OBJECT_OPERATOR) already include parens
|
|
644
|
+
return '(' + leftStr + '.' + rightStr + ')';
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// No operations - simple identifier
|
|
650
|
+
return '(' + leftStr + '.' + rightStr + ')';
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// ARRAY_RANGE_INDEX_OPERATOR: (start..end)
|
|
654
|
+
if (op == Operation.ARRAY_RANGE_INDEX_OPERATOR) {
|
|
655
|
+
const leftStr = this.tokenToString(left);
|
|
656
|
+
const rightStr = this.tokenToString(right);
|
|
657
|
+
return '(' + leftStr + '..' + rightStr + ')';
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Other binary operators: (left op right)
|
|
661
|
+
const leftStr = this.tokenToString(left);
|
|
662
|
+
const rightStr = this.tokenToString(right);
|
|
663
|
+
return '(' + leftStr + op.getOperator() + rightStr + ')';
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Convert a token to string, handling nested expressions recursively
|
|
668
|
+
*/
|
|
669
|
+
private tokenToString(token: ExpressionToken): string {
|
|
670
|
+
if (token instanceof Expression) {
|
|
671
|
+
return token.toString();
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Check if token is an ExpressionTokenValue (use duck typing for minified code)
|
|
675
|
+
if (token && typeof (token as any).getExpression === 'function' &&
|
|
676
|
+
typeof (token as any).getTokenValue === 'function') {
|
|
677
|
+
const originalExpr = (token as any).getExpression();
|
|
678
|
+
// If it's a quoted string, use the original expression with quotes
|
|
679
|
+
if (originalExpr && (originalExpr.startsWith('"') || originalExpr.startsWith("'"))) {
|
|
680
|
+
return originalExpr;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return token.toString();
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Format array index, preserving quotes for bracket notation
|
|
689
|
+
*/
|
|
690
|
+
private formatArrayIndex(token: ExpressionToken): string {
|
|
691
|
+
if (token instanceof Expression) {
|
|
692
|
+
// Check if this is a simple quoted string
|
|
693
|
+
const expr = token as Expression;
|
|
694
|
+
if (expr.getOperations().isEmpty() && expr.getTokens().size() == 1) {
|
|
695
|
+
const innerToken = expr.getTokens().get(0);
|
|
696
|
+
// Check if innerToken is an ExpressionTokenValue with quoted expression
|
|
697
|
+
if (innerToken && typeof (innerToken as any).getExpression === 'function') {
|
|
698
|
+
const originalExpr = (innerToken as any).getExpression();
|
|
699
|
+
if (originalExpr && (originalExpr.startsWith('"') || originalExpr.startsWith("'"))) {
|
|
700
|
+
return originalExpr;
|
|
701
|
+
}
|
|
515
702
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
.append(')');
|
|
703
|
+
}
|
|
704
|
+
return expr.toString();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Check if token is an ExpressionTokenValue with quoted expression
|
|
708
|
+
if (token && typeof (token as any).getExpression === 'function') {
|
|
709
|
+
const originalExpr = (token as any).getExpression();
|
|
710
|
+
if (originalExpr && (originalExpr.startsWith('"') || originalExpr.startsWith("'"))) {
|
|
711
|
+
return originalExpr;
|
|
526
712
|
}
|
|
527
713
|
}
|
|
714
|
+
|
|
715
|
+
return token.toString();
|
|
716
|
+
}
|
|
528
717
|
|
|
529
|
-
|
|
718
|
+
/**
|
|
719
|
+
* Check if a token is an Expression with operations
|
|
720
|
+
*/
|
|
721
|
+
private tokenHasOperations(token: ExpressionToken): boolean {
|
|
722
|
+
if (token instanceof Expression) {
|
|
723
|
+
return !token.getOperations().isEmpty();
|
|
724
|
+
}
|
|
725
|
+
return false;
|
|
530
726
|
}
|
|
531
727
|
|
|
532
728
|
public equals(o: Expression): boolean {
|