simple-graph-query 2.2.0 → 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.
@@ -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" ||
@@ -48959,7 +48960,26 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48959
48960
  varNames.push(varName);
48960
48961
  quantifiedSets.push(varQuantifiedSets[varName]);
48961
48962
  }
48962
- 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
+ }
48963
48983
  const result = [];
48964
48984
  let foundTrue = false;
48965
48985
  let foundFalse = false;
@@ -48990,10 +49010,19 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48990
49010
  for (let j = 0; j < varNames.length; j++) {
48991
49011
  quantDeclEnv.env[varNames[j]] = tuple[j];
48992
49012
  }
48993
- // now, we want to evaluate the barExpr
48994
- const barExprValue = this.visit(barExpr);
48995
- if (!isBoolean(barExprValue)) {
48996
- 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;
48997
49026
  }
48998
49027
  if (barExprValue) {
48999
49028
  result.push(tuple);
@@ -49973,7 +50002,28 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49973
50002
  varNames.push(varName);
49974
50003
  quantifiedSets.push(varQuantifiedSets[varName]);
49975
50004
  }
49976
- 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
+ }
49977
50027
  const result = [];
49978
50028
  // Optimize: create environment once and reuse it
49979
50029
  const quantDeclEnv = {
@@ -49987,10 +50037,19 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49987
50037
  for (let j = 0; j < varNames.length; j++) {
49988
50038
  quantDeclEnv.env[varNames[j]] = tuple[j];
49989
50039
  }
49990
- // now, we want to evaluate the barExpr
49991
- const barExprValue = this.visit(barExpr);
49992
- if (!isBoolean(barExprValue)) {
49993
- 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;
49994
50053
  }
49995
50054
  if (barExprValue) {
49996
50055
  // will error if not boolean val, which we want
@@ -50657,6 +50716,200 @@ class ForgeExprFreeVariableFinder extends AbstractParseTreeVisitor_1.AbstractPar
50657
50716
  exports.ForgeExprFreeVariableFinder = ForgeExprFreeVariableFinder;
50658
50717
 
50659
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
+
50660
50913
  /***/ }),
50661
50914
 
50662
50915
  /***/ "./src/errorListener.ts":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-graph-query",
3
- "version": "2.2.0",
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",