@fincity/kirun-js 2.15.1 → 2.16.1
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/function/system/MakeTest.ts +317 -0
- package/__tests__/engine/runtime/KIRuntimeTest.ts +2 -1
- package/__tests__/engine/runtime/KIRuntimeVarArgsTest.ts +101 -0
- package/__tests__/engine/runtime/expression/ExpressionEvaluationTest.ts +66 -0
- package/__tests__/engine/runtime/expression/ExpressionEvaluatorStringLiteralTest.ts +1 -1
- 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 +6 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/engine/function/system/Make.ts +83 -0
- package/src/engine/repository/KIRunFunctionRepository.ts +2 -0
- package/src/engine/runtime/KIRuntime.ts +189 -98
- package/src/engine/runtime/expression/Expression.ts +37 -5
- package/src/engine/runtime/expression/ExpressionEvaluator.ts +360 -55
- package/src/engine/runtime/expression/tokenextractor/ExpressionInternalValueExtractor.ts +2 -1
- package/src/engine/runtime/expression/tokenextractor/TokenValueExtractor.ts +100 -18
- package/src/engine/runtime/graph/ExecutionGraph.ts +9 -0
- package/src/engine/runtime/graph/GraphVertex.ts +9 -0
- package/src/engine/runtime/tokenextractor/ArgumentsTokenValueExtractor.ts +2 -1
- package/src/engine/runtime/tokenextractor/ContextTokenValueExtractor.ts +2 -1
- package/src/engine/runtime/tokenextractor/OutputMapTokenValueExtractor.ts +1 -1
|
@@ -47,6 +47,9 @@ export class KIRuntime extends AbstractFunction {
|
|
|
47
47
|
private fd: FunctionDefinition;
|
|
48
48
|
|
|
49
49
|
private debugMode: boolean = false;
|
|
50
|
+
|
|
51
|
+
// Cache for function lookups to avoid repeated async calls
|
|
52
|
+
private functionCache: Map<string, Function> = new Map();
|
|
50
53
|
|
|
51
54
|
public constructor(fd: FunctionDefinition, debugMode: boolean = false) {
|
|
52
55
|
super();
|
|
@@ -67,13 +70,37 @@ export class KIRuntime extends AbstractFunction {
|
|
|
67
70
|
return this.fd;
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
private async getCachedFunction(
|
|
74
|
+
fRepo: Repository<Function>,
|
|
75
|
+
namespace: string,
|
|
76
|
+
name: string,
|
|
77
|
+
): Promise<Function | undefined> {
|
|
78
|
+
const key = `${namespace}.${name}`;
|
|
79
|
+
if (this.functionCache.has(key)) {
|
|
80
|
+
return this.functionCache.get(key);
|
|
81
|
+
}
|
|
82
|
+
const fun = await fRepo.find(namespace, name);
|
|
83
|
+
if (fun) {
|
|
84
|
+
this.functionCache.set(key, fun);
|
|
85
|
+
}
|
|
86
|
+
return fun;
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
public async getExecutionPlan(
|
|
71
90
|
fRepo: Repository<Function>,
|
|
72
91
|
sRepo: Repository<Schema>,
|
|
73
92
|
): Promise<ExecutionGraph<string, StatementExecution>> {
|
|
74
93
|
let g: ExecutionGraph<string, StatementExecution> = new ExecutionGraph();
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
|
|
95
|
+
// Parallelize statement preparation
|
|
96
|
+
const statements = Array.from(this.fd.getSteps().values());
|
|
97
|
+
const statementExecutions = await Promise.all(
|
|
98
|
+
statements.map((s) => this.prepareStatementExecution(s, fRepo, sRepo))
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
for (const se of statementExecutions) {
|
|
102
|
+
g.addVertex(se);
|
|
103
|
+
}
|
|
77
104
|
|
|
78
105
|
let unresolved = this.makeEdges(g);
|
|
79
106
|
|
|
@@ -117,11 +144,6 @@ export class KIRuntime extends AbstractFunction {
|
|
|
117
144
|
console.log(`EID: ${inContext.getExecutionId()} ${eGraph?.toString()}`);
|
|
118
145
|
}
|
|
119
146
|
|
|
120
|
-
// if (logger.isDebugEnabled()) {
|
|
121
|
-
// logger.debug(StringFormatter.format("Executing : $.$", this.fd.getNamespace(), this.fd.getName()));
|
|
122
|
-
// logger.debug(eGraph.toString());
|
|
123
|
-
// }
|
|
124
|
-
|
|
125
147
|
let messages: string[] = eGraph
|
|
126
148
|
.getVerticesData()
|
|
127
149
|
.filter((e) => e.getMessages().length)
|
|
@@ -157,13 +179,19 @@ export class KIRuntime extends AbstractFunction {
|
|
|
157
179
|
(!executionQue.isEmpty() || !branchQue.isEmpty()) &&
|
|
158
180
|
!inContext.getEvents()?.has(Event.OUTPUT)
|
|
159
181
|
) {
|
|
182
|
+
const prevExecQueSize = executionQue.length;
|
|
183
|
+
const prevBranchQueSize = branchQue.length;
|
|
184
|
+
|
|
160
185
|
await this.processBranchQue(inContext, executionQue, branchQue);
|
|
161
186
|
await this.processExecutionQue(inContext, executionQue, branchQue);
|
|
162
187
|
|
|
163
|
-
|
|
188
|
+
// Only increment count when actual work was done
|
|
189
|
+
if (prevExecQueSize !== executionQue.length || prevBranchQueSize !== branchQue.length) {
|
|
190
|
+
inContext.setCount(inContext.getCount() + 1);
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
|
|
192
|
+
if (inContext.getCount() == KIRuntime.MAX_EXECUTION_ITERATIONS)
|
|
193
|
+
throw new KIRuntimeException('Execution locked in an infinite loop');
|
|
194
|
+
}
|
|
167
195
|
}
|
|
168
196
|
|
|
169
197
|
if (!eGraph.isSubGraph() && !inContext.getEvents()?.size) {
|
|
@@ -192,19 +220,44 @@ export class KIRuntime extends AbstractFunction {
|
|
|
192
220
|
>
|
|
193
221
|
>,
|
|
194
222
|
) {
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
223
|
+
if (executionQue.isEmpty()) return;
|
|
224
|
+
|
|
225
|
+
// Collect all vertices from the queue
|
|
226
|
+
const allVertices: GraphVertex<string, StatementExecution>[] = [];
|
|
227
|
+
while (!executionQue.isEmpty()) {
|
|
228
|
+
allVertices.push(executionQue.pop());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Separate ready and not-ready vertices
|
|
232
|
+
const readyVertices: GraphVertex<string, StatementExecution>[] = [];
|
|
233
|
+
const notReadyVertices: GraphVertex<string, StatementExecution>[] = [];
|
|
234
|
+
|
|
235
|
+
for (const vertex of allVertices) {
|
|
236
|
+
if (this.allDependenciesResolvedVertex(vertex, inContext.getSteps()!)) {
|
|
237
|
+
readyVertices.push(vertex);
|
|
238
|
+
} else {
|
|
239
|
+
notReadyVertices.push(vertex);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Add not-ready vertices back to the queue
|
|
244
|
+
for (const vertex of notReadyVertices) {
|
|
245
|
+
executionQue.add(vertex);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Execute all ready vertices in parallel
|
|
249
|
+
if (readyVertices.length > 0) {
|
|
250
|
+
await Promise.all(
|
|
251
|
+
readyVertices.map((vertex) =>
|
|
252
|
+
this.executeVertex(
|
|
253
|
+
vertex,
|
|
254
|
+
inContext,
|
|
255
|
+
branchQue,
|
|
256
|
+
executionQue,
|
|
257
|
+
inContext.getFunctionRepository(),
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
);
|
|
208
261
|
}
|
|
209
262
|
}
|
|
210
263
|
|
|
@@ -220,17 +273,40 @@ export class KIRuntime extends AbstractFunction {
|
|
|
220
273
|
>
|
|
221
274
|
>,
|
|
222
275
|
) {
|
|
223
|
-
if (branchQue.length)
|
|
224
|
-
let branch: Tuple4<
|
|
225
|
-
ExecutionGraph<string, StatementExecution>,
|
|
226
|
-
Tuple2<string, string>[],
|
|
227
|
-
FunctionOutput,
|
|
228
|
-
GraphVertex<string, StatementExecution>
|
|
229
|
-
> = branchQue.pop();
|
|
276
|
+
if (!branchQue.length) return;
|
|
230
277
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
278
|
+
// Collect all branches from the queue
|
|
279
|
+
const allBranches: Tuple4<
|
|
280
|
+
ExecutionGraph<string, StatementExecution>,
|
|
281
|
+
Tuple2<string, string>[],
|
|
282
|
+
FunctionOutput,
|
|
283
|
+
GraphVertex<string, StatementExecution>
|
|
284
|
+
>[] = [];
|
|
285
|
+
|
|
286
|
+
while (branchQue.length) {
|
|
287
|
+
allBranches.push(branchQue.pop());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Separate ready and not-ready branches
|
|
291
|
+
const readyBranches: typeof allBranches = [];
|
|
292
|
+
const notReadyBranches: typeof allBranches = [];
|
|
293
|
+
|
|
294
|
+
for (const branch of allBranches) {
|
|
295
|
+
if (this.allDependenciesResolvedTuples(branch.getT2(), inContext.getSteps()!)) {
|
|
296
|
+
readyBranches.push(branch);
|
|
297
|
+
} else {
|
|
298
|
+
notReadyBranches.push(branch);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Add not-ready branches back to the queue
|
|
303
|
+
for (const branch of notReadyBranches) {
|
|
304
|
+
branchQue.add(branch);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Execute all ready branches (sequentially since they may have shared state)
|
|
308
|
+
for (const branch of readyBranches) {
|
|
309
|
+
await this.executeBranch(inContext, executionQue, branch);
|
|
234
310
|
}
|
|
235
311
|
}
|
|
236
312
|
|
|
@@ -246,13 +322,21 @@ export class KIRuntime extends AbstractFunction {
|
|
|
246
322
|
) {
|
|
247
323
|
let vertex: GraphVertex<string, StatementExecution> = branch.getT4();
|
|
248
324
|
let nextOutput: EventResult | undefined = undefined;
|
|
325
|
+
|
|
326
|
+
// Pre-compute statement names to delete - avoid recalculating each iteration
|
|
327
|
+
const statementsToDelete = branch
|
|
328
|
+
.getT1()
|
|
329
|
+
.getVerticesData()
|
|
330
|
+
.map((e) => e.getStatement().getStatementName());
|
|
249
331
|
|
|
250
332
|
do {
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
333
|
+
// Clear previous iteration's step outputs
|
|
334
|
+
const steps = inContext.getSteps();
|
|
335
|
+
if (steps) {
|
|
336
|
+
for (const statementName of statementsToDelete) {
|
|
337
|
+
steps.delete(statementName);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
256
340
|
|
|
257
341
|
await this.executeGraph(branch.getT1(), inContext);
|
|
258
342
|
nextOutput = branch.getT3().next();
|
|
@@ -285,10 +369,12 @@ export class KIRuntime extends AbstractFunction {
|
|
|
285
369
|
} while (nextOutput && nextOutput.getName() != Event.OUTPUT);
|
|
286
370
|
|
|
287
371
|
if (nextOutput?.getName() == Event.OUTPUT && vertex.getOutVertices().has(Event.OUTPUT)) {
|
|
288
|
-
(vertex?.getOutVertices()?.get(Event.OUTPUT) ?? [])
|
|
289
|
-
|
|
372
|
+
const outVertices = Array.from(vertex?.getOutVertices()?.get(Event.OUTPUT) ?? []);
|
|
373
|
+
for (const e of outVertices) {
|
|
374
|
+
if (this.allDependenciesResolvedVertex(e, inContext.getSteps()!)) {
|
|
290
375
|
executionQue.add(e);
|
|
291
|
-
|
|
376
|
+
}
|
|
377
|
+
}
|
|
292
378
|
}
|
|
293
379
|
}
|
|
294
380
|
|
|
@@ -317,10 +403,12 @@ export class KIRuntime extends AbstractFunction {
|
|
|
317
403
|
})
|
|
318
404
|
.every((e) => !isNullValue(e) && e !== false);
|
|
319
405
|
|
|
320
|
-
if (!allTrue)
|
|
406
|
+
if (!allTrue) {
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
321
409
|
}
|
|
322
410
|
|
|
323
|
-
let fun: Function | undefined = await
|
|
411
|
+
let fun: Function | undefined = await this.getCachedFunction(fRepo, s.getNamespace(), s.getName());
|
|
324
412
|
|
|
325
413
|
if (!fun) {
|
|
326
414
|
throw new KIRuntimeException(
|
|
@@ -413,17 +501,23 @@ export class KIRuntime extends AbstractFunction {
|
|
|
413
501
|
|
|
414
502
|
if (!isOutput) {
|
|
415
503
|
let subGraph = vertex.getSubGraphOfType(er.getName());
|
|
416
|
-
let unResolvedDependencies: Tuple2<string, string>[] =
|
|
504
|
+
let unResolvedDependencies: Tuple2<string, string>[] = [];
|
|
505
|
+
if (!subGraph.areEdgesBuilt()) {
|
|
506
|
+
unResolvedDependencies = this.makeEdges(subGraph).getT1();
|
|
507
|
+
subGraph.setEdgesBuilt(true);
|
|
508
|
+
}
|
|
417
509
|
branchQue.push(new Tuple4(subGraph, unResolvedDependencies, result, vertex));
|
|
418
510
|
} else {
|
|
419
511
|
let out: Set<GraphVertex<string, StatementExecution>> | undefined = vertex
|
|
420
512
|
.getOutVertices()
|
|
421
513
|
.get(Event.OUTPUT);
|
|
422
|
-
if (out)
|
|
423
|
-
|
|
424
|
-
if (
|
|
514
|
+
if (out) {
|
|
515
|
+
for (const e of Array.from(out)) {
|
|
516
|
+
if (this.allDependenciesResolvedVertex(e, inContext.getSteps()!)) {
|
|
425
517
|
executionQue.add(e);
|
|
426
|
-
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
427
521
|
}
|
|
428
522
|
}
|
|
429
523
|
|
|
@@ -433,22 +527,20 @@ export class KIRuntime extends AbstractFunction {
|
|
|
433
527
|
): Map<string, any> {
|
|
434
528
|
if (!result) return result;
|
|
435
529
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}, new Map());
|
|
530
|
+
const resolved = new Map<string, any>();
|
|
531
|
+
for (const [key, value] of result.entries()) {
|
|
532
|
+
resolved.set(key, this.resolveInternalExpression(value, inContext));
|
|
533
|
+
}
|
|
534
|
+
return resolved;
|
|
442
535
|
}
|
|
443
536
|
|
|
444
537
|
private resolveInternalExpression(value: any, inContext: FunctionExecutionParameters): any {
|
|
445
538
|
if (isNullValue(value) || typeof value != 'object') return value;
|
|
446
539
|
|
|
447
540
|
if (value instanceof JsonExpression) {
|
|
448
|
-
|
|
541
|
+
return new ExpressionEvaluator(
|
|
449
542
|
(value as JsonExpression).getExpression(),
|
|
450
|
-
);
|
|
451
|
-
return exp.evaluate(inContext.getValuesMap());
|
|
543
|
+
).evaluate(inContext.getValuesMap());
|
|
452
544
|
}
|
|
453
545
|
|
|
454
546
|
if (Array.isArray(value)) {
|
|
@@ -490,16 +582,18 @@ export class KIRuntime extends AbstractFunction {
|
|
|
490
582
|
vertex: GraphVertex<string, StatementExecution>,
|
|
491
583
|
output: Map<string, Map<string, Map<string, any>>>,
|
|
492
584
|
): boolean {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
585
|
+
const inVertices = vertex.getInVertices();
|
|
586
|
+
if (!inVertices.size) return true;
|
|
587
|
+
|
|
588
|
+
// Use for..of directly instead of Array.from + filter
|
|
589
|
+
for (const e of inVertices) {
|
|
590
|
+
const stepName = e.getT1().getData().getStatement().getStatementName();
|
|
591
|
+
const type = e.getT2();
|
|
592
|
+
if (!(output.has(stepName) && output.get(stepName)?.has(type))) {
|
|
593
|
+
return false;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return true;
|
|
503
597
|
}
|
|
504
598
|
|
|
505
599
|
private getArgumentsFromParametersMap(
|
|
@@ -507,35 +601,33 @@ export class KIRuntime extends AbstractFunction {
|
|
|
507
601
|
s: Statement,
|
|
508
602
|
paramSet: Map<string, Parameter>,
|
|
509
603
|
): Map<string, any> {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
604
|
+
const args = new Map<string, any>();
|
|
605
|
+
|
|
606
|
+
for (const [paramName, paramRefMap] of s.getParameterMap().entries()) {
|
|
607
|
+
const prList: ParameterReference[] = Array.from(paramRefMap?.values() ?? []);
|
|
608
|
+
|
|
609
|
+
if (!prList?.length) continue;
|
|
610
|
+
|
|
611
|
+
const pDef: Parameter | undefined = paramSet.get(paramName);
|
|
612
|
+
if (!pDef) continue;
|
|
613
|
+
|
|
614
|
+
let ret: any;
|
|
615
|
+
if (pDef.isVariableArgument()) {
|
|
616
|
+
ret = prList
|
|
617
|
+
.sort((a, b) => (a.getOrder() ?? 0) - (b.getOrder() ?? 0))
|
|
618
|
+
.filter((r) => !isNullValue(r))
|
|
619
|
+
.map((r) => this.parameterReferenceEvaluation(inContext, r))
|
|
620
|
+
.flatMap((r) => (Array.isArray(r) ? r : [r]));
|
|
621
|
+
} else {
|
|
622
|
+
ret = this.parameterReferenceEvaluation(inContext, prList[0]);
|
|
623
|
+
}
|
|
531
624
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
}, new Map());
|
|
625
|
+
if (!isNullValue(ret)) {
|
|
626
|
+
args.set(paramName, ret);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return args;
|
|
539
631
|
}
|
|
540
632
|
|
|
541
633
|
private parameterReferenceEvaluation(
|
|
@@ -550,8 +642,7 @@ export class KIRuntime extends AbstractFunction {
|
|
|
550
642
|
ref.getType() == ParameterReferenceType.EXPRESSION &&
|
|
551
643
|
!StringUtil.isNullOrBlank(ref.getExpression())
|
|
552
644
|
) {
|
|
553
|
-
|
|
554
|
-
ret = exp.evaluate(inContext.getValuesMap());
|
|
645
|
+
ret = new ExpressionEvaluator(ref.getExpression() ?? '').evaluate(inContext.getValuesMap());
|
|
555
646
|
}
|
|
556
647
|
return ret;
|
|
557
648
|
}
|
|
@@ -563,7 +654,7 @@ export class KIRuntime extends AbstractFunction {
|
|
|
563
654
|
): Promise<StatementExecution> {
|
|
564
655
|
let se: StatementExecution = new StatementExecution(s);
|
|
565
656
|
|
|
566
|
-
let fun: Function | undefined = await
|
|
657
|
+
let fun: Function | undefined = await this.getCachedFunction(fRepo, s.getNamespace(), s.getName());
|
|
567
658
|
|
|
568
659
|
if (!fun) {
|
|
569
660
|
se.addMessage(
|
|
@@ -13,6 +13,10 @@ export class Expression extends ExpressionToken {
|
|
|
13
13
|
private tokens: LinkedList<ExpressionToken> = new LinkedList();
|
|
14
14
|
// Data structure for storing operations
|
|
15
15
|
private ops: LinkedList<Operation> = new LinkedList();
|
|
16
|
+
|
|
17
|
+
// Cached arrays for fast evaluation (avoids LinkedList traversal)
|
|
18
|
+
private cachedTokensArray?: ExpressionToken[];
|
|
19
|
+
private cachedOpsArray?: Operation[];
|
|
16
20
|
|
|
17
21
|
public constructor(
|
|
18
22
|
expression?: string,
|
|
@@ -45,6 +49,21 @@ export class Expression extends ExpressionToken {
|
|
|
45
49
|
public getOperations(): LinkedList<Operation> {
|
|
46
50
|
return this.ops;
|
|
47
51
|
}
|
|
52
|
+
|
|
53
|
+
// Fast array access for evaluation (cached)
|
|
54
|
+
public getTokensArray(): ExpressionToken[] {
|
|
55
|
+
if (!this.cachedTokensArray) {
|
|
56
|
+
this.cachedTokensArray = this.tokens.toArray();
|
|
57
|
+
}
|
|
58
|
+
return this.cachedTokensArray;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
public getOperationsArray(): Operation[] {
|
|
62
|
+
if (!this.cachedOpsArray) {
|
|
63
|
+
this.cachedOpsArray = this.ops.toArray();
|
|
64
|
+
}
|
|
65
|
+
return this.cachedOpsArray;
|
|
66
|
+
}
|
|
48
67
|
|
|
49
68
|
private evaluate(): void {
|
|
50
69
|
const length: number = this.expression.length;
|
|
@@ -345,11 +364,8 @@ export class Expression extends ExpressionToken {
|
|
|
345
364
|
'Missing a closed parenthesis',
|
|
346
365
|
);
|
|
347
366
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
subExp.charAt(0) == '(' &&
|
|
351
|
-
subExp.charAt(subExp.length() - 1) == ')'
|
|
352
|
-
) {
|
|
367
|
+
// Only remove outer parentheses if they actually match
|
|
368
|
+
while (subExp.length() > 2 && this.hasMatchingOuterParentheses(subExp.toString())) {
|
|
353
369
|
subExp.deleteCharAt(0);
|
|
354
370
|
subExp.setLength(subExp.length() - 1);
|
|
355
371
|
}
|
|
@@ -421,6 +437,22 @@ export class Expression extends ExpressionToken {
|
|
|
421
437
|
return pre2 < pre1;
|
|
422
438
|
}
|
|
423
439
|
|
|
440
|
+
private hasMatchingOuterParentheses(str: string): boolean {
|
|
441
|
+
if (str.length < 2 || str.charAt(0) !== '(' || str.charAt(str.length - 1) !== ')') {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
// Check if the first '(' matches the last ')'
|
|
445
|
+
// by verifying that the nesting level never drops to 0 before the end
|
|
446
|
+
let level = 0;
|
|
447
|
+
for (let i = 0; i < str.length - 1; i++) {
|
|
448
|
+
const ch = str.charAt(i);
|
|
449
|
+
if (ch === '(') level++;
|
|
450
|
+
else if (ch === ')') level--;
|
|
451
|
+
if (level === 0) return false; // First paren closed before end
|
|
452
|
+
}
|
|
453
|
+
return level === 1; // Should be 1 just before the last ')'
|
|
454
|
+
}
|
|
455
|
+
|
|
424
456
|
public toString(): string {
|
|
425
457
|
if (this.ops.isEmpty()) {
|
|
426
458
|
if (this.tokens.size() == 1) return this.tokens.get(0).toString();
|