Skip to content

Commit f0b09de

Browse files
committed
chore: improve evaluation of FilterExpression
* determine statically what filter behavior is needed * model all cases explicitly * tag function on their type Closes #140
1 parent 19a6d96 commit f0b09de

2 files changed

Lines changed: 105 additions & 71 deletions

File tree

src/interpreter.ts

Lines changed: 73 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
isDateTime,
1616
isType,
1717
isNumber,
18-
isContext
18+
isContext,
19+
isBoolean
1920
} from './types.js';
2021

2122
import {
@@ -402,15 +403,15 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
402403
case '!=': return (b) => (a) => !equals(a, b);
403404
}
404405

405-
}, Test('boolean'));
406+
}, 'test');
406407

407408
case 'BacktickIdentifier': return node.input.replace(/`/g, '');
408409

409410
case 'Wildcard': return (_context) => true;
410411

411-
case 'null': return (_context) => {
412+
case 'null': return tag((_context) => {
412413
return null;
413-
};
414+
}, 'nil');
414415

415416
case 'Disjunction': return tag((context) => {
416417

@@ -433,7 +434,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
433434
const b = typeof right === 'boolean' ? right : null;
434435

435436
return matrix.find(el => el[0] === a && el[1] === b)[2];
436-
}, Test('boolean'));
437+
}, 'test');
437438

438439
case 'Conjunction': return tag((context) => {
439440
const left = args[0](context);
@@ -455,7 +456,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
455456
const b = typeof right === 'boolean' ? right : null;
456457

457458
return matrix.find(el => el[0] === a && el[1] === b)[2];
458-
}, Test('boolean'));
459+
}, 'test');
459460

460461
case 'Context': return (context) => {
461462

@@ -518,7 +519,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
518519
// spaces into one (token)
519520
case 'Name': return node.input.replace(/\s{2,}/g, ' ');
520521

521-
case 'VariableName': return (context, local = false) => {
522+
case 'VariableName': return tag((context, local = false) => {
522523
const name = args.join(' ');
523524

524525
const contextValue = getFromContext(name, context);
@@ -558,7 +559,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
558559
}
559560

560561
return null;
561-
};
562+
}, 'any');
562563

563564
case 'QualifiedName': return (context) => {
564565
return args.reduce((context, arg) => arg(context), context);
@@ -668,7 +669,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
668669
const b = args[3](context);
669670

670671
return a instanceof b;
671-
}, Test('boolean'));
672+
}, 'test');
672673

673674
case 'every': return tag((context) => {
674675
return (_contexts, _condition) => {
@@ -681,7 +682,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
681682
return contexts.every(ctx => isTruthy(_condition(ctx)));
682683
};
683684

684-
}, Test('boolean'));
685+
}, 'test');
685686

686687
case 'some': return tag((context) => {
687688
return (_contexts, _condition) => {
@@ -693,7 +694,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
693694

694695
return contexts.some(ctx => isTruthy(_condition(ctx)));
695696
};
696-
}, Test('boolean'));
697+
}, 'test');
697698

698699
case 'NumericLiteral': return tag((_context) => node.input.includes('.') ? parseFloat(node.input) : parseInt(node.input), 'number');
699700

@@ -723,7 +724,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
723724
return getBuiltin(node.input, context);
724725
};
725726

726-
case 'DateTimeLiteral': return (context) => {
727+
case 'DateTimeLiteral': return tag((context) => {
727728

728729
// AtLiteral
729730
if (args.length === 1) {
@@ -765,9 +766,9 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
765766
return result;
766767
}
767768

768-
};
769+
}, 'date');
769770

770-
case 'AtLiteral': return (context) => {
771+
case 'AtLiteral': return tag((context) => {
771772

772773
const wrappedFn = wrapFunction(getBuiltin('@', context));
773774

@@ -781,9 +782,9 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
781782
}
782783

783784
return wrappedFn.invoke([ args[0](context) ]);
784-
};
785+
}, 'date');
785786

786-
case 'FunctionInvocation': return (context) => {
787+
case 'FunctionInvocation': return tag((context) => {
787788

788789
const target = args[0](context);
789790

@@ -817,7 +818,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
817818
}
818819

819820
return result;
820-
};
821+
}, 'any');
821822

822823
case 'IfExpression': return (function() {
823824

@@ -841,7 +842,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
841842

842843
case 'Parameters': return args.length === 3 ? args[1] : (_context) => [];
843844

844-
case 'Comparison': return (context) => {
845+
case 'Comparison': return tag((context) => {
845846

846847
const operator = args[1];
847848

@@ -872,7 +873,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
872873
const test = operator()(right);
873874

874875
return compareValue(test, left);
875-
};
876+
}, 'test');
876877

877878
case 'QuantifiedExpression': return (context) => {
878879

@@ -944,7 +945,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
944945

945946
case 'ParenthesizedExpression': return args[1];
946947

947-
case 'PathExpression': return (context) => {
948+
case 'PathExpression': return tag((context) => {
948949

949950
const pathTarget = args[0](context);
950951
const pathProp = args[1];
@@ -954,10 +955,10 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
954955
} else {
955956
return pathProp(pathTarget, true);
956957
}
957-
};
958+
}, 'any');
958959

959960
// expression !filter "[" expression "]"
960-
case 'FilterExpression': return (context) => {
961+
case 'FilterExpression': return tag((context) => {
961962

962963
const target = args[0](context);
963964

@@ -970,23 +971,27 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
970971
return null;
971972
}
972973

973-
// a[variable=number]
974-
if (typeof filterFn.type === 'undefined') {
975-
try {
976-
const value = filterFn(context);
974+
const type = filterFn.type;
977975

978-
if (isNumber(value)) {
979-
filterFn.type = 'number';
980-
}
981-
} catch (_err) {
976+
// a[1]
977+
// a[true]
978+
// a[b]
979+
// a[b()]
980+
// a[1 + 3]
981+
if ([ 'number', 'boolean', 'any' ].includes(type)) {
982+
const idx = filterFn(context);
982983

983-
// ignore
984+
if (isBoolean(idx)) {
985+
if (idx === true) {
986+
return target;
987+
} else {
988+
return [];
989+
}
984990
}
985-
}
986991

987-
// a[1]
988-
if (filterFn.type === 'number') {
989-
const idx = filterFn(context);
992+
if (!isNumber(idx)) {
993+
return [];
994+
}
990995

991996
const value = filterTarget[idx < 0 ? filterTarget.length + idx : idx - 1];
992997

@@ -997,49 +1002,50 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
9971002
}
9981003
}
9991004

1000-
// a[true]
1001-
if (filterFn.type === 'boolean') {
1002-
if (filterFn(context)) {
1003-
return filterTarget;
1004-
} else {
1005-
return [];
1006-
}
1007-
}
1008-
1009-
if (filterFn.type === 'string') {
1005+
// TODO(nikku): not covered by spec
1006+
// a["attr"]
1007+
if (type === 'string') {
10101008

10111009
const value = filterFn(context);
10121010

10131011
return filterTarget.filter(el => el === value);
10141012
}
10151013

1016-
// a[test]
1017-
return filterTarget.map(el => {
1014+
// a[b=c]
1015+
// a[>10]
1016+
if (type === 'test') {
10181017

1019-
const iterationContext = {
1020-
...context,
1021-
item: el,
1022-
...el
1023-
};
1018+
return filterTarget.map(el => {
10241019

1025-
let result = filterFn(iterationContext);
1020+
const iterationContext = {
1021+
...context,
1022+
item: el,
1023+
...el
1024+
};
10261025

1027-
// test is fn(val) => boolean SimpleUnaryTest
1028-
if (typeof result === 'function') {
1029-
result = result(el);
1030-
}
1026+
let result = filterFn(iterationContext);
10311027

1032-
if (result instanceof Range) {
1033-
result = result.includes(el);
1034-
}
1028+
// test is fn(val) => boolean SimpleUnaryTest
1029+
if (typeof result === 'function') {
1030+
result = result(el);
1031+
}
10351032

1036-
if (result === true) {
1037-
return el;
1038-
}
1033+
if (result instanceof Range) {
1034+
result = result.includes(el);
1035+
}
10391036

1040-
return result;
1041-
}).filter(isTruthy);
1042-
};
1037+
if (result === true) {
1038+
return el;
1039+
}
1040+
1041+
return result;
1042+
}).filter(isTruthy);
1043+
}
1044+
1045+
// a[null]
1046+
// a[@"2026-12-12"]
1047+
return null;
1048+
}, 'any');
10431049

10441050
case 'SimplePositiveUnaryTest': return tag((context) => {
10451051

@@ -1065,7 +1071,7 @@ function evalNode(node: Node, args: any[], interpreterContext: InterpreterContex
10651071
const endIncluded = right !== null && args[3] === ']';
10661072

10671073
return createRange(left, right, startIncluded, endIncluded);
1068-
}, Test('boolean'));
1074+
}, 'test');
10691075

10701076
case 'PositiveUnaryTests':
10711077
case 'Expressions': return (context) => {
@@ -1432,10 +1438,6 @@ function isTruthy(obj) {
14321438
return obj !== false && obj !== null;
14331439
}
14341440

1435-
function Test(type: string): string {
1436-
return `Test<${type}>`;
1437-
}
1438-
14391441
/**
14401442
* @param {Function} fn
14411443
* @param {string[]} [parameterNames]

test/interpreter-spec.js

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,18 @@ describe('interpreter', function() {
663663

664664
expr('[1, 2, 3][4]', null);
665665

666+
expr('[1, 2, 3][1 + 2]', 3);
667+
668+
expr('[1, 2, 3][1 + a]', 2, { a: 1 });
669+
670+
expr('[1, 2, 3][a.b]', 2, { a: { b: 2 } });
671+
672+
expr('[1, 2, 3][a[1]]', 2, { a: [ 2 ] });
673+
674+
expr('[1, 2, 3][a()]', 2, {
675+
a() { return 2; }
676+
});
677+
666678
expr('[1,2,3][true]', [ 1, 2, 3 ]);
667679

668680
expr('[1,2,3][false]', []);
@@ -715,6 +727,9 @@ describe('interpreter', function() {
715727

716728
expr('a[1]', null);
717729

730+
expr('a[date("2002-04-02")]', null);
731+
expr('a[@"2002-04-02"]', null);
732+
718733
expr('[][a]', null, { a: 1 });
719734

720735
expr('[][a]', [], { a: '1' });
@@ -1680,6 +1695,23 @@ describe('interpreter', function() {
16801695
});
16811696

16821697

1698+
it('for filter expression', function() {
1699+
1700+
// when
1701+
const {
1702+
value,
1703+
warnings
1704+
} = evaluate('list[a=b]', {
1705+
list: [ { a: 1, b: 1 } ]
1706+
});
1707+
1708+
// then
1709+
expect(value).to.eql([ { a: 1, b: 1 } ]);
1710+
1711+
expect(warnings).to.eql([]);
1712+
});
1713+
1714+
16831715
it('valid expressions', function() {
16841716

16851717
// when

0 commit comments

Comments
 (0)