simple-graph-query 2.1.4 → 2.2.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.
@@ -17,13 +17,17 @@ export declare class ForgeExprEvaluator extends AbstractParseTreeVisitor<EvalRes
17
17
  private freeVariables;
18
18
  private cachedResults;
19
19
  private instanceData;
20
+ private relationCache;
21
+ private relationIndexCache;
20
22
  constructor(datum: IDataInstance);
23
+ private buildRelationCache;
21
24
  private updateFreeVariables;
22
25
  private constructFreeVariableKey;
23
26
  private getLabelForValue;
24
27
  private getLabelAsString;
25
28
  private getLabelAsBoolean;
26
29
  private getLabelAsNumber;
30
+ private dotJoin;
27
31
  private cacheResult;
28
32
  private getIden;
29
33
  protected aggregateResult(aggregate: EvalResult, nextResult: EvalResult): EvalResult;
@@ -0,0 +1,39 @@
1
+ import { ExprContext } from "./forge-antlr/ForgeParser";
2
+ import { Tuple } from "./ForgeExprEvaluator";
3
+ /**
4
+ * Analyzes set comprehension constraints to detect simple numeric comparison patterns
5
+ * that can be optimized by generating only valid combinations instead of filtering.
6
+ */
7
+ export interface NumericConstraintPattern {
8
+ type: 'less_than' | 'greater_than' | 'less_equal' | 'greater_equal' | 'not_equal' | 'none';
9
+ leftVar: string;
10
+ rightVar: string;
11
+ }
12
+ /**
13
+ * Detects if a constraint expression is a simple numeric comparison between two variables.
14
+ * Currently detects patterns like: a < b, a > b, a <= b, a >= b, a != b
15
+ *
16
+ * Note: Uses string matching on expression text which is simple but may miss expressions
17
+ * with extra whitespace or parentheses. This is intentionally conservative - we prefer
18
+ * to fall back to the standard approach rather than risk incorrect optimization.
19
+ *
20
+ * @param constraintExpr The constraint expression to analyze
21
+ * @param varNames The variable names in scope
22
+ * @returns Pattern info if detected, null otherwise
23
+ */
24
+ export declare function detectNumericComparisonPattern(constraintExpr: ExprContext, varNames: string[]): NumericConstraintPattern | null;
25
+ /**
26
+ * Checks if all values in the sets are numbers (or tuples containing single numbers).
27
+ * This is required for numeric comparison optimization to be applicable.
28
+ */
29
+ export declare function areAllNumericSets(sets: Tuple[][]): boolean;
30
+ /**
31
+ * Generates optimized combinations for numeric comparison constraints.
32
+ * Instead of generating all combinations and filtering, generates only valid ones.
33
+ *
34
+ * @param varNames Variable names in order
35
+ * @param quantifiedSets The sets each variable ranges over
36
+ * @param pattern The detected numeric comparison pattern
37
+ * @returns Only the tuples that satisfy the constraint
38
+ */
39
+ export declare function generateOptimizedNumericCombinations(varNames: string[], quantifiedSets: Tuple[][], pattern: NumericConstraintPattern): Tuple[];
@@ -48441,6 +48441,7 @@ exports.areTupleArraysEqual = areTupleArraysEqual;
48441
48441
  const AbstractParseTreeVisitor_1 = __webpack_require__(/*! antlr4ts/tree/AbstractParseTreeVisitor */ "./node_modules/antlr4ts/tree/AbstractParseTreeVisitor.js");
48442
48442
  const lodash_1 = __webpack_require__(/*! lodash */ "./node_modules/lodash/lodash.js");
48443
48443
  const ForgeExprFreeVariableFinder_1 = __webpack_require__(/*! ./ForgeExprFreeVariableFinder */ "./src/ForgeExprFreeVariableFinder.ts");
48444
+ const NumericConstraintOptimizer_1 = __webpack_require__(/*! ./NumericConstraintOptimizer */ "./src/NumericConstraintOptimizer.ts");
48444
48445
  ///// HELPER FUNCTIONS /////
48445
48446
  function isSingleValue(value) {
48446
48447
  return (typeof value === "string" ||
@@ -48570,38 +48571,6 @@ function transitiveClosure(pairs) {
48570
48571
  // convert the result back to a Tuple[] and return
48571
48572
  return Array.from(transitiveClosureSet).map((pair) => JSON.parse(pair));
48572
48573
  }
48573
- function dotJoin(left, right) {
48574
- const leftExpr = isSingleValue(left) ? [[left]] : left;
48575
- const rightExpr = isSingleValue(right) ? [[right]] : right;
48576
- // Optimize join using a Map for O(n+m) instead of O(n*m) lookup
48577
- // Group right tuples by their first element for fast lookup
48578
- const rightIndex = new Map();
48579
- for (const rightTuple of rightExpr) {
48580
- const key = rightTuple[0];
48581
- if (!rightIndex.has(key)) {
48582
- rightIndex.set(key, []);
48583
- }
48584
- rightIndex.get(key).push(rightTuple);
48585
- }
48586
- const result = [];
48587
- for (const leftTuple of leftExpr) {
48588
- const joinKey = leftTuple[leftTuple.length - 1];
48589
- const matchingRightTuples = rightIndex.get(joinKey);
48590
- if (matchingRightTuples) {
48591
- for (const rightTuple of matchingRightTuples) {
48592
- result.push([
48593
- ...leftTuple.slice(0, leftTuple.length - 1),
48594
- ...rightTuple.slice(1),
48595
- ]);
48596
- }
48597
- }
48598
- }
48599
- if (result.some(tuple => tuple.length === 0)) {
48600
- throw new Error("Join would create a relation of arity 0");
48601
- }
48602
- // Deduplicate results to ensure set semantics
48603
- return deduplicateTuples(result);
48604
- }
48605
48574
  function bitwidthWraparound(value, bitwidth) {
48606
48575
  const modulus = Math.pow(2, bitwidth); // total number of Int values
48607
48576
  const halfValue = Math.pow(2, bitwidth - 1); // halfway point
@@ -48632,11 +48601,61 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48632
48601
  // NOTE: strings will be of the format "<var-name>=<value>|..." sorted in
48633
48602
  // increasing lexicographic order of variable names
48634
48603
  this.cachedResults = new Map();
48604
+ // Cache for relation lookups to avoid O(n) scans
48605
+ this.relationCache = null;
48606
+ // Index for relations: Map<relationName, Map<firstElement, Tuple[]>>
48607
+ // This allows O(1) lookup for patterns like atom.field
48608
+ this.relationIndexCache = null;
48635
48609
  this.instanceData = datum;
48636
48610
  this.environmentStack = [];
48637
48611
  this.freeVariableFinder = new ForgeExprFreeVariableFinder_1.ForgeExprFreeVariableFinder(datum);
48638
48612
  this.freeVariables = new Map();
48639
48613
  }
48614
+ // helper function to build relation cache and indexes
48615
+ buildRelationCache() {
48616
+ if (this.relationCache !== null) {
48617
+ return; // already built
48618
+ }
48619
+ this.relationCache = new Map();
48620
+ this.relationIndexCache = new Map();
48621
+ const relations = this.instanceData.getRelations();
48622
+ const isConvertibleToNumber = (value) => {
48623
+ return typeof value === "string" && !isNaN(Number(value));
48624
+ };
48625
+ const isConvertibleToBoolean = (value) => {
48626
+ if (typeof value === "boolean")
48627
+ return false; // already boolean
48628
+ return value === "true" || value === "#t" || value === "false" || value === "#f";
48629
+ };
48630
+ const convertToBoolean = (value) => {
48631
+ if (typeof value === "boolean")
48632
+ return value;
48633
+ if (value === "true" || value === "#t")
48634
+ return true;
48635
+ if (value === "false" || value === "#f")
48636
+ return false;
48637
+ throw new Error(`Cannot convert ${value} to boolean`);
48638
+ };
48639
+ for (const relation of relations) {
48640
+ let relationAtoms = relation.tuples.map((tuple) => tuple.atoms);
48641
+ // Convert numeric and boolean strings to their actual types
48642
+ relationAtoms = relationAtoms.map((tuple) => tuple.map((value) => isConvertibleToNumber(value) ? Number(value) : value));
48643
+ relationAtoms = relationAtoms.map((tuple) => tuple.map((value) => isConvertibleToBoolean(value) ? convertToBoolean(value) : value));
48644
+ this.relationCache.set(relation.name, relationAtoms);
48645
+ // Build index for this relation: first element -> all tuples starting with that element
48646
+ const relationIndex = new Map();
48647
+ for (const tuple of relationAtoms) {
48648
+ if (tuple.length > 0) {
48649
+ const key = tuple[0];
48650
+ if (!relationIndex.has(key)) {
48651
+ relationIndex.set(key, []);
48652
+ }
48653
+ relationIndex.get(key).push(tuple);
48654
+ }
48655
+ }
48656
+ this.relationIndexCache.set(relation.name, relationIndex);
48657
+ }
48658
+ }
48640
48659
  //helper function
48641
48660
  updateFreeVariables(freeVars) {
48642
48661
  if (this.freeVariables.size === 0) {
@@ -48732,6 +48751,45 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48732
48751
  }
48733
48752
  return labelNum;
48734
48753
  }
48754
+ // Optimized dotJoin that can use pre-built relation indexes
48755
+ dotJoin(left, right, rightRelationName) {
48756
+ const leftExpr = isSingleValue(left) ? [[left]] : left;
48757
+ const rightExpr = isSingleValue(right) ? [[right]] : right;
48758
+ // Try to use pre-built index if available
48759
+ let rightIndex;
48760
+ if (rightRelationName && this.relationIndexCache) {
48761
+ rightIndex = this.relationIndexCache.get(rightRelationName);
48762
+ }
48763
+ // If no pre-built index, build one on the fly
48764
+ if (!rightIndex) {
48765
+ rightIndex = new Map();
48766
+ for (const rightTuple of rightExpr) {
48767
+ const key = rightTuple[0];
48768
+ if (!rightIndex.has(key)) {
48769
+ rightIndex.set(key, []);
48770
+ }
48771
+ rightIndex.get(key).push(rightTuple);
48772
+ }
48773
+ }
48774
+ const result = [];
48775
+ for (const leftTuple of leftExpr) {
48776
+ const joinKey = leftTuple[leftTuple.length - 1];
48777
+ const matchingRightTuples = rightIndex.get(joinKey);
48778
+ if (matchingRightTuples) {
48779
+ for (const rightTuple of matchingRightTuples) {
48780
+ result.push([
48781
+ ...leftTuple.slice(0, leftTuple.length - 1),
48782
+ ...rightTuple.slice(1),
48783
+ ]);
48784
+ }
48785
+ }
48786
+ }
48787
+ if (result.some(tuple => tuple.length === 0)) {
48788
+ throw new Error("Join would create a relation of arity 0");
48789
+ }
48790
+ // Deduplicate results to ensure set semantics
48791
+ return deduplicateTuples(result);
48792
+ }
48735
48793
  // helper function
48736
48794
  cacheResult(ctx, freeVarsKey, result) {
48737
48795
  if (!this.cachedResults.has(ctx)) {
@@ -48862,9 +48920,6 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48862
48920
  if (foundAllVars && this.cachedResults.has(ctx)) {
48863
48921
  if (this.cachedResults.get(ctx).has(freeVarsKey)) {
48864
48922
  // cache hit!
48865
- // console.log('cache hit for ctx:', ctx.text);
48866
- // console.log('freeVarsKey:', freeVarsKey);
48867
- // console.log('cachedResults:', this.cachedResults.get(ctx));
48868
48923
  return this.cachedResults.get(ctx).get(freeVarsKey);
48869
48924
  }
48870
48925
  }
@@ -48905,7 +48960,26 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48905
48960
  varNames.push(varName);
48906
48961
  quantifiedSets.push(varQuantifiedSets[varName]);
48907
48962
  }
48908
- const product = getCombinations(quantifiedSets);
48963
+ // Try to optimize numeric comparisons
48964
+ let product;
48965
+ let useOptimizedPath = false;
48966
+ if (!isDisjoint && varNames.length >= 2 && (0, NumericConstraintOptimizer_1.areAllNumericSets)(quantifiedSets)) {
48967
+ // Try to detect and optimize numeric comparison patterns
48968
+ const pattern = (0, NumericConstraintOptimizer_1.detectNumericComparisonPattern)(barExpr, varNames);
48969
+ if (pattern && pattern.type !== 'none') {
48970
+ // Use optimized combination generation
48971
+ product = (0, NumericConstraintOptimizer_1.generateOptimizedNumericCombinations)(varNames, quantifiedSets, pattern);
48972
+ useOptimizedPath = true;
48973
+ }
48974
+ else {
48975
+ // Fall back to standard cartesian product
48976
+ product = getCombinations(quantifiedSets);
48977
+ }
48978
+ }
48979
+ else {
48980
+ // Fall back to standard cartesian product
48981
+ product = getCombinations(quantifiedSets);
48982
+ }
48909
48983
  const result = [];
48910
48984
  let foundTrue = false;
48911
48985
  let foundFalse = false;
@@ -48936,10 +49010,19 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48936
49010
  for (let j = 0; j < varNames.length; j++) {
48937
49011
  quantDeclEnv.env[varNames[j]] = tuple[j];
48938
49012
  }
48939
- // now, we want to evaluate the barExpr
48940
- const barExprValue = this.visit(barExpr);
48941
- if (!isBoolean(barExprValue)) {
48942
- throw new Error("Expected the expression after the bar to be a boolean!");
49013
+ // If we used the optimized path, we can skip constraint evaluation
49014
+ // since the combinations already satisfy the constraint
49015
+ let barExprValue;
49016
+ if (useOptimizedPath) {
49017
+ barExprValue = true;
49018
+ }
49019
+ else {
49020
+ // now, we want to evaluate the barExpr
49021
+ const evalResult = this.visit(barExpr);
49022
+ if (!isBoolean(evalResult)) {
49023
+ throw new Error("Expected the expression after the bar to be a boolean!");
49024
+ }
49025
+ barExprValue = evalResult;
48943
49026
  }
48944
49027
  if (barExprValue) {
48945
49028
  result.push(tuple);
@@ -49634,7 +49717,7 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49634
49717
  }
49635
49718
  }
49636
49719
  // Box join: <expr-a>[<expr-b>] == <expr-b> . <expr-a>
49637
- return dotJoin(insideBracesExprs, beforeBracesExpr);
49720
+ return this.dotJoin(insideBracesExprs, beforeBracesExpr);
49638
49721
  }
49639
49722
  return this.visitChildren(ctx);
49640
49723
  }
@@ -49647,9 +49730,20 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49647
49730
  }
49648
49731
  const beforeDotExpr = this.visit(ctx.expr15());
49649
49732
  const afterDotExpr = this.visit(ctx.expr16());
49650
- // console.log('beforeExpr:', beforeDotExpr);
49651
- // console.log('afterExpr:', afterDotExpr);
49652
- return dotJoin(beforeDotExpr, afterDotExpr);
49733
+ // Try to extract the relation name if the right side is a simple identifier/relation name
49734
+ let rightRelationName;
49735
+ // Simple heuristic: check if it's a tuple array (likely a relation)
49736
+ // and if so, try to find which relation it matches
49737
+ if (isTupleArray(afterDotExpr) && this.relationCache) {
49738
+ // Check if this matches any cached relation
49739
+ for (const [relName, relTuples] of this.relationCache.entries()) {
49740
+ if (relTuples === afterDotExpr) {
49741
+ rightRelationName = relName;
49742
+ break;
49743
+ }
49744
+ }
49745
+ }
49746
+ return this.dotJoin(beforeDotExpr, afterDotExpr, rightRelationName);
49653
49747
  }
49654
49748
  if (ctx.LEFT_SQUARE_TOK()) {
49655
49749
  const beforeBracesName = this.visit(ctx.name());
@@ -49908,7 +50002,28 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49908
50002
  varNames.push(varName);
49909
50003
  quantifiedSets.push(varQuantifiedSets[varName]);
49910
50004
  }
49911
- const product = getCombinations(quantifiedSets);
50005
+ // Try to optimize numeric comparisons (same optimization as in quantifiers)
50006
+ // Note: Set comprehensions don't support the 'disj' keyword, so we don't check for it here.
50007
+ // Quantifiers do support 'disj', so they check !isDisjoint before optimizing.
50008
+ let product;
50009
+ let useOptimizedPath = false;
50010
+ if (varNames.length >= 2 && (0, NumericConstraintOptimizer_1.areAllNumericSets)(quantifiedSets)) {
50011
+ // Try to detect and optimize numeric comparison patterns
50012
+ const pattern = (0, NumericConstraintOptimizer_1.detectNumericComparisonPattern)(barExpr, varNames);
50013
+ if (pattern && pattern.type !== 'none') {
50014
+ // Use optimized combination generation
50015
+ product = (0, NumericConstraintOptimizer_1.generateOptimizedNumericCombinations)(varNames, quantifiedSets, pattern);
50016
+ useOptimizedPath = true;
50017
+ }
50018
+ else {
50019
+ // Fall back to standard cartesian product
50020
+ product = getCombinations(quantifiedSets);
50021
+ }
50022
+ }
50023
+ else {
50024
+ // Fall back to standard cartesian product
50025
+ product = getCombinations(quantifiedSets);
50026
+ }
49912
50027
  const result = [];
49913
50028
  // Optimize: create environment once and reuse it
49914
50029
  const quantDeclEnv = {
@@ -49922,10 +50037,19 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49922
50037
  for (let j = 0; j < varNames.length; j++) {
49923
50038
  quantDeclEnv.env[varNames[j]] = tuple[j];
49924
50039
  }
49925
- // now, we want to evaluate the barExpr
49926
- const barExprValue = this.visit(barExpr);
49927
- if (!isBoolean(barExprValue)) {
49928
- throw new Error("Expected the expression after the bar to be a boolean value!");
50040
+ // If we used the optimized path, we can skip constraint evaluation
50041
+ // since the combinations already satisfy the constraint
50042
+ let barExprValue;
50043
+ if (useOptimizedPath) {
50044
+ barExprValue = true;
50045
+ }
50046
+ else {
50047
+ // now, we want to evaluate the barExpr
50048
+ const evalResult = this.visit(barExpr);
50049
+ if (!isBoolean(evalResult)) {
50050
+ throw new Error("Expected the expression after the bar to be a boolean value!");
50051
+ }
50052
+ barExprValue = evalResult;
49929
50053
  }
49930
50054
  if (barExprValue) {
49931
50055
  // will error if not boolean val, which we want
@@ -50092,15 +50216,10 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
50092
50216
  throw new Error(`Cannot convert ${value} to boolean`);
50093
50217
  };
50094
50218
  // end of 3 helper functions
50095
- // check if it is a relation
50096
- const relations = this.instanceData.getRelations();
50097
- for (const relation of relations) {
50098
- if (relation.name === identifier) {
50099
- let relationAtoms = relation.tuples.map((tuple) => tuple.atoms);
50100
- relationAtoms = relationAtoms.map((tuple) => tuple.map((value) => isConvertibleToNumber(value) ? Number(value) : value));
50101
- relationAtoms = relationAtoms.map((tuple) => tuple.map((value) => isConvertibleToBoolean(value) ? convertToBoolean(value) : value));
50102
- return relationAtoms;
50103
- }
50219
+ // check if it is a relation - use cache for faster lookups
50220
+ this.buildRelationCache();
50221
+ if (this.relationCache.has(identifier)) {
50222
+ return this.relationCache.get(identifier);
50104
50223
  }
50105
50224
  if (result !== undefined) {
50106
50225
  result = result.map((tuple) => tuple.map((value) => isConvertibleToNumber(value) ? Number(value) : value));
@@ -50386,6 +50505,9 @@ class ForgeExprFreeVariableFinder extends AbstractParseTreeVisitor_1.AbstractPar
50386
50505
  const quantDeclListVars = this.getQuantDeclListVarNames(ctx.quantDeclList());
50387
50506
  // we need to get all the vars referenced here other than the vars
50388
50507
  // bound by the quantifier (in quantDeclListVars)
50508
+ // First, get free variables from the quantDeclList itself
50509
+ // (e.g., in "some p2 : c.parent | ...", c is a free variable)
50510
+ const quantDeclListFreeVars = this.visit(ctx.quantDeclList());
50389
50511
  const blockOrBar = ctx.blockOrBar();
50390
50512
  if (blockOrBar === undefined) {
50391
50513
  throw new Error("expected to quantify over something!");
@@ -50394,13 +50516,15 @@ class ForgeExprFreeVariableFinder extends AbstractParseTreeVisitor_1.AbstractPar
50394
50516
  blockOrBar.expr() === undefined) {
50395
50517
  throw new Error("Expected the quantifier to have a bar followed by an expr!");
50396
50518
  }
50397
- let allFreeVars;
50519
+ let barExprFreeVars;
50398
50520
  if (blockOrBar.block() !== undefined) {
50399
- allFreeVars = this.visit(blockOrBar.block());
50521
+ barExprFreeVars = this.visit(blockOrBar.block());
50400
50522
  }
50401
50523
  else {
50402
- allFreeVars = this.visit(blockOrBar.expr());
50524
+ barExprFreeVars = this.visit(blockOrBar.expr());
50403
50525
  }
50526
+ // Merge free variables from quantDeclList and barExpr
50527
+ const allFreeVars = this.aggregateResult(barExprFreeVars, quantDeclListFreeVars);
50404
50528
  // the context node for the quantifier as a whole shouldn't have _all_ of these
50405
50529
  // free vars; specifically, it should not include the vars that are being
50406
50530
  // bound by the quantifier
@@ -50592,6 +50716,200 @@ class ForgeExprFreeVariableFinder extends AbstractParseTreeVisitor_1.AbstractPar
50592
50716
  exports.ForgeExprFreeVariableFinder = ForgeExprFreeVariableFinder;
50593
50717
 
50594
50718
 
50719
+ /***/ }),
50720
+
50721
+ /***/ "./src/NumericConstraintOptimizer.ts":
50722
+ /*!*******************************************!*\
50723
+ !*** ./src/NumericConstraintOptimizer.ts ***!
50724
+ \*******************************************/
50725
+ /***/ ((__unused_webpack_module, exports) => {
50726
+
50727
+ "use strict";
50728
+
50729
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
50730
+ exports.detectNumericComparisonPattern = detectNumericComparisonPattern;
50731
+ exports.areAllNumericSets = areAllNumericSets;
50732
+ exports.generateOptimizedNumericCombinations = generateOptimizedNumericCombinations;
50733
+ /**
50734
+ * Detects if a constraint expression is a simple numeric comparison between two variables.
50735
+ * Currently detects patterns like: a < b, a > b, a <= b, a >= b, a != b
50736
+ *
50737
+ * Note: Uses string matching on expression text which is simple but may miss expressions
50738
+ * with extra whitespace or parentheses. This is intentionally conservative - we prefer
50739
+ * to fall back to the standard approach rather than risk incorrect optimization.
50740
+ *
50741
+ * @param constraintExpr The constraint expression to analyze
50742
+ * @param varNames The variable names in scope
50743
+ * @returns Pattern info if detected, null otherwise
50744
+ */
50745
+ function detectNumericComparisonPattern(constraintExpr, varNames) {
50746
+ // Parse the expression text to look for simple comparison patterns
50747
+ const exprText = constraintExpr.text;
50748
+ // Check if this is a simple binary comparison between two variables
50749
+ // Pattern: <varName1><compareOp><varName2> (no whitespace in text representation)
50750
+ // Note: The AST text property concatenates tokens without whitespace
50751
+ // Try to extract comparison operator and operands
50752
+ for (const leftVar of varNames) {
50753
+ for (const rightVar of varNames) {
50754
+ if (leftVar === rightVar)
50755
+ continue;
50756
+ // Check for different comparison operators (whitespace already removed in text)
50757
+ if (exprText === `${leftVar}<${rightVar}`) {
50758
+ return { type: 'less_than', leftVar, rightVar };
50759
+ }
50760
+ if (exprText === `${leftVar}>${rightVar}`) {
50761
+ return { type: 'greater_than', leftVar, rightVar };
50762
+ }
50763
+ if (exprText === `${leftVar}<=${rightVar}`) {
50764
+ return { type: 'less_equal', leftVar, rightVar };
50765
+ }
50766
+ if (exprText === `${leftVar}>=${rightVar}`) {
50767
+ return { type: 'greater_equal', leftVar, rightVar };
50768
+ }
50769
+ if (exprText === `${leftVar}!=${rightVar}`) {
50770
+ return { type: 'not_equal', leftVar, rightVar };
50771
+ }
50772
+ // Also check for negated equality: not a = b
50773
+ if (exprText === `not${leftVar}=${rightVar}`) {
50774
+ return { type: 'not_equal', leftVar, rightVar };
50775
+ }
50776
+ }
50777
+ }
50778
+ return null;
50779
+ }
50780
+ /**
50781
+ * Checks if all values in the sets are numbers (or tuples containing single numbers).
50782
+ * This is required for numeric comparison optimization to be applicable.
50783
+ */
50784
+ function areAllNumericSets(sets) {
50785
+ for (const set of sets) {
50786
+ for (const tuple of set) {
50787
+ if (tuple.length !== 1)
50788
+ return false;
50789
+ if (typeof tuple[0] !== 'number')
50790
+ return false;
50791
+ }
50792
+ }
50793
+ return true;
50794
+ }
50795
+ /**
50796
+ * Extracts numbers from tuples that contain single numbers.
50797
+ * Precondition: This should only be called after areAllNumericSets validation.
50798
+ */
50799
+ function extractNumbers(tuples) {
50800
+ return tuples.map(t => t[0]);
50801
+ }
50802
+ /**
50803
+ * Generates optimized combinations for numeric comparison constraints.
50804
+ * Instead of generating all combinations and filtering, generates only valid ones.
50805
+ *
50806
+ * @param varNames Variable names in order
50807
+ * @param quantifiedSets The sets each variable ranges over
50808
+ * @param pattern The detected numeric comparison pattern
50809
+ * @returns Only the tuples that satisfy the constraint
50810
+ */
50811
+ function generateOptimizedNumericCombinations(varNames, quantifiedSets, pattern) {
50812
+ // Map variable names to their indices and value sets
50813
+ const varIndexMap = new Map();
50814
+ varNames.forEach((name, idx) => {
50815
+ varIndexMap.set(name, idx);
50816
+ });
50817
+ const leftIdx = varIndexMap.get(pattern.leftVar);
50818
+ const rightIdx = varIndexMap.get(pattern.rightVar);
50819
+ if (leftIdx === undefined || rightIdx === undefined) {
50820
+ // Pattern variables don't match our variable set - this shouldn't happen
50821
+ // if detectNumericComparisonPattern is working correctly, but throw an error
50822
+ // to prevent incorrect results (caller should have validated pattern matches variables)
50823
+ throw new Error(`Internal error: Pattern variables ${pattern.leftVar}, ${pattern.rightVar} not found in variable list`);
50824
+ }
50825
+ // Extract numeric values
50826
+ const leftNumbers = extractNumbers(quantifiedSets[leftIdx]);
50827
+ const rightNumbers = extractNumbers(quantifiedSets[rightIdx]);
50828
+ // For other variables, we still need all combinations
50829
+ const otherVars = [];
50830
+ const otherSets = [];
50831
+ for (let i = 0; i < varNames.length; i++) {
50832
+ if (i !== leftIdx && i !== rightIdx) {
50833
+ otherVars.push(i);
50834
+ otherSets.push(extractNumbers(quantifiedSets[i]));
50835
+ }
50836
+ }
50837
+ const result = [];
50838
+ // Generate combinations based on the comparison type
50839
+ const generatePairs = (compareFunc) => {
50840
+ for (const leftVal of leftNumbers) {
50841
+ for (const rightVal of rightNumbers) {
50842
+ if (compareFunc(leftVal, rightVal)) {
50843
+ // This pair satisfies the constraint
50844
+ // Now combine with all other variables if any
50845
+ if (otherVars.length === 0) {
50846
+ // Simple case: only two variables
50847
+ const tuple = new Array(varNames.length);
50848
+ tuple[leftIdx] = leftVal;
50849
+ tuple[rightIdx] = rightVal;
50850
+ result.push(tuple);
50851
+ }
50852
+ else {
50853
+ // Need to generate cartesian product with other variables
50854
+ const otherCombos = cartesianProduct(otherSets);
50855
+ for (const otherCombo of otherCombos) {
50856
+ const tuple = new Array(varNames.length);
50857
+ tuple[leftIdx] = leftVal;
50858
+ tuple[rightIdx] = rightVal;
50859
+ for (let i = 0; i < otherVars.length; i++) {
50860
+ tuple[otherVars[i]] = otherCombo[i];
50861
+ }
50862
+ result.push(tuple);
50863
+ }
50864
+ }
50865
+ }
50866
+ }
50867
+ }
50868
+ };
50869
+ switch (pattern.type) {
50870
+ case 'less_than':
50871
+ generatePairs((a, b) => a < b);
50872
+ break;
50873
+ case 'greater_than':
50874
+ generatePairs((a, b) => a > b);
50875
+ break;
50876
+ case 'less_equal':
50877
+ generatePairs((a, b) => a <= b);
50878
+ break;
50879
+ case 'greater_equal':
50880
+ generatePairs((a, b) => a >= b);
50881
+ break;
50882
+ case 'not_equal':
50883
+ generatePairs((a, b) => a !== b);
50884
+ break;
50885
+ default:
50886
+ // Unknown pattern, return empty (will fall back to standard approach)
50887
+ return [];
50888
+ }
50889
+ return result;
50890
+ }
50891
+ /**
50892
+ * Helper to generate cartesian product of number arrays
50893
+ */
50894
+ function cartesianProduct(arrays) {
50895
+ if (arrays.length === 0)
50896
+ return [[]];
50897
+ if (arrays.some(arr => arr.length === 0))
50898
+ return [];
50899
+ let result = [[]];
50900
+ for (const arr of arrays) {
50901
+ const newResult = [];
50902
+ for (const existing of result) {
50903
+ for (const value of arr) {
50904
+ newResult.push([...existing, value]);
50905
+ }
50906
+ }
50907
+ result = newResult;
50908
+ }
50909
+ return result;
50910
+ }
50911
+
50912
+
50595
50913
  /***/ }),
50596
50914
 
50597
50915
  /***/ "./src/errorListener.ts":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-graph-query",
3
- "version": "2.1.4",
3
+ "version": "2.2.1",
4
4
  "description": "TypeScript evaluator for Forge expressions with browser-compatible UMD bundle",
5
5
  "main": "dist/simple-graph-query.bundle.js",
6
6
  "types": "dist/index.d.ts",