simple-graph-query 2.5.0 → 2.5.2

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.
@@ -8,7 +8,7 @@ export type BinaryRelationExample = {
8
8
  pairs: Set<AtomPair>;
9
9
  datum: IDataInstance;
10
10
  };
11
- type ExpressionNode = {
11
+ export type ExpressionNode = {
12
12
  kind: "identifier";
13
13
  name: string;
14
14
  } | {
@@ -22,6 +22,41 @@ type ExpressionNode = {
22
22
  } | {
23
23
  kind: "closure";
24
24
  child: ExpressionNode;
25
+ } | {
26
+ kind: "reflexive-closure";
27
+ child: ExpressionNode;
28
+ } | {
29
+ kind: "transpose";
30
+ child: ExpressionNode;
31
+ } | {
32
+ kind: "comprehension";
33
+ varName: string;
34
+ domain: ExpressionNode;
35
+ body: ExpressionNode;
36
+ } | {
37
+ kind: "all" | "some" | "no" | "one" | "lone";
38
+ varName: string;
39
+ domain: ExpressionNode;
40
+ body: ExpressionNode;
41
+ } | {
42
+ kind: "and" | "or" | "implies" | "iff";
43
+ left: ExpressionNode;
44
+ right: ExpressionNode;
45
+ } | {
46
+ kind: "not";
47
+ child: ExpressionNode;
48
+ } | {
49
+ kind: "in" | "eq" | "neq";
50
+ left: ExpressionNode;
51
+ right: ExpressionNode;
52
+ } | {
53
+ kind: "lt" | "gt" | "lte" | "gte";
54
+ left: ExpressionNode;
55
+ right: ExpressionNode;
56
+ } | {
57
+ kind: "box-join";
58
+ base: ExpressionNode;
59
+ args: ExpressionNode[];
25
60
  };
26
61
  export type WhyNode = {
27
62
  kind: ExpressionNode["kind"];
@@ -45,4 +80,3 @@ export type SynthesisWhy = {
45
80
  };
46
81
  export declare function synthesizeSelectorWithWhy(examples: AtomSelectionExample[], maxDepth?: number): SynthesisWhy;
47
82
  export declare function synthesizeBinaryRelationWithWhy(examples: BinaryRelationExample[], maxDepth?: number): SynthesisWhy;
48
- export {};
@@ -49080,6 +49080,12 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49080
49080
  this.cacheResult(ctx, freeVarsKey, value);
49081
49081
  return value;
49082
49082
  }
49083
+ if (multExpr.TWO_TOK() && result.length > 2) {
49084
+ this.environmentStack.pop();
49085
+ const value = false;
49086
+ this.cacheResult(ctx, freeVarsKey, value);
49087
+ return value;
49088
+ }
49083
49089
  }
49084
49090
  }
49085
49091
  this.environmentStack.pop();
@@ -49111,7 +49117,9 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49111
49117
  return value;
49112
49118
  }
49113
49119
  else if (multExpr.TWO_TOK()) {
49114
- throw new Error("**NOT IMPLEMENTING FOR NOW** Two (`two`)");
49120
+ const value = result.length === 2;
49121
+ this.cacheResult(ctx, freeVarsKey, value);
49122
+ return value;
49115
49123
  }
49116
49124
  }
49117
49125
  // TODO: don't have support for SUM_TOK yet
@@ -49205,12 +49213,27 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49205
49213
  if (!isBoolean(leftChildValue)) {
49206
49214
  throw new Error("IMP operator expected 2 boolean operands!");
49207
49215
  }
49216
+ const expr3Values = ctx.expr3() ?? [];
49217
+ const thenExpr = expr3Values[0];
49218
+ const elseExpr = expr3Values[1];
49219
+ if (ctx.ELSE_TOK()) {
49220
+ if (!thenExpr || !elseExpr) {
49221
+ throw new Error("Expected the ELSE operator to have 2 operands!");
49222
+ }
49223
+ const branchValue = this.visit(leftChildValue ? thenExpr : elseExpr);
49224
+ if (!isBoolean(branchValue)) {
49225
+ throw new Error("IMP operator expected 2 boolean operands!");
49226
+ }
49227
+ return branchValue;
49228
+ }
49208
49229
  if (!leftChildValue) {
49209
49230
  // short circuit if the antecedent is false
49210
49231
  return true;
49211
49232
  }
49212
- const rightChildValue = this.visit(ctx.expr3()[0]);
49213
- // TODO: add support for ELSE_TOK over here
49233
+ if (!thenExpr) {
49234
+ throw new Error("Expected the IMP operator to have a consequent expression!");
49235
+ }
49236
+ const rightChildValue = this.visit(thenExpr);
49214
49237
  if (!isBoolean(rightChildValue)) {
49215
49238
  throw new Error("IMP operator expected 2 boolean operands!");
49216
49239
  }
@@ -49426,37 +49449,31 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49426
49449
  results = leftNum >= rightNum;
49427
49450
  break;
49428
49451
  case "in":
49452
+ case "ni": {
49453
+ let membershipResult;
49429
49454
  // this should be true if the left value is equal to the right value,
49430
49455
  // or a subset of it
49431
49456
  if (isTupleArray(leftChildValue) && isTupleArray(rightChildValue)) {
49432
49457
  if (areTupleArraysEqual(leftChildValue, rightChildValue)) {
49433
- results = true;
49458
+ membershipResult = true;
49434
49459
  }
49435
49460
  else {
49436
49461
  // check if left is subset of right
49437
- results = isTupleArraySubset(leftChildValue, rightChildValue);
49462
+ membershipResult = isTupleArraySubset(leftChildValue, rightChildValue);
49438
49463
  }
49439
49464
  }
49440
49465
  else if (isTupleArray(rightChildValue)) {
49441
- results = rightChildValue.some((tuple) => tuple.length === 1 && tuple[0] === leftChildValue);
49466
+ membershipResult = rightChildValue.some((tuple) => tuple.length === 1 && tuple[0] === leftChildValue);
49442
49467
  }
49443
49468
  else {
49444
49469
  // left is a tuple array but right is a single value, so false
49445
- results = false;
49470
+ membershipResult = false;
49446
49471
  }
49472
+ results = ctx.compareOp()?.text === "ni" ? !membershipResult : membershipResult;
49447
49473
  break;
49474
+ }
49448
49475
  case "is":
49449
49476
  throw new Error("**NOT IMPLEMENTING FOR NOW** Type Check (`is`)");
49450
- case "ni":
49451
- results.push(["**UNIMPLEMENTED** Set Non-Membership (`ni`)"]);
49452
- // TODO: implement this using leftValue and rightValue
49453
- // for now, just returning over here. what we need to do instead
49454
- // is to implement this, set the value of results to what we get
49455
- // from this, and then call break (so that we can negate before
49456
- // returning the final value, if required)
49457
- return results;
49458
- // removed by dead control flow
49459
- {} // redundant, but it won't be once we implement the TODO above
49460
49477
  default:
49461
49478
  throw new Error(`Unexpected compare operator provided: ${ctx.compareOp()?.text}`);
49462
49479
  }
@@ -49479,13 +49496,13 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49479
49496
  const childrenResults = this.visit(ctx.expr8());
49480
49497
  //console.log('childrenResults:', childrenResults);
49481
49498
  if (ctx.SET_TOK()) {
49482
- throw new Error("**NOT IMPLEMENTING FOR NOW** Set (`set`)");
49499
+ return childrenResults;
49483
49500
  }
49484
49501
  if (ctx.ONE_TOK()) {
49485
49502
  return isTupleArray(childrenResults) && childrenResults.length === 1;
49486
49503
  }
49487
49504
  if (ctx.TWO_TOK()) {
49488
- throw new Error("**NOT IMPLEMENTING FOR NOW** Two (`two`)");
49505
+ return isTupleArray(childrenResults) && childrenResults.length === 2;
49489
49506
  }
49490
49507
  if (ctx.NO_TOK()) {
49491
49508
  return isTupleArray(childrenResults) && childrenResults.length === 0;
@@ -50955,10 +50972,20 @@ function nodeToString(node) {
50955
50972
  switch (node.kind) {
50956
50973
  case "identifier":
50957
50974
  return node.name;
50975
+ // Unary relational operators
50958
50976
  case "closure": {
50959
50977
  const inner = nodeToString(node.child);
50960
50978
  return `^${wrapForPrefix(inner)}`;
50961
50979
  }
50980
+ case "reflexive-closure": {
50981
+ const inner = nodeToString(node.child);
50982
+ return `*${wrapForPrefix(inner)}`;
50983
+ }
50984
+ case "transpose": {
50985
+ const inner = nodeToString(node.child);
50986
+ return `~${wrapForPrefix(inner)}`;
50987
+ }
50988
+ // Binary relational operators
50962
50989
  case "join": {
50963
50990
  const left = wrapForJoin(node.left);
50964
50991
  const right = wrapForJoin(node.right);
@@ -50970,10 +50997,59 @@ function nodeToString(node) {
50970
50997
  return `(${nodeToString(node.left)} & ${nodeToString(node.right)})`;
50971
50998
  case "difference":
50972
50999
  return `(${nodeToString(node.left)} - ${nodeToString(node.right)})`;
51000
+ // Set comprehension
51001
+ case "comprehension":
51002
+ return `{${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)}}`;
51003
+ // Quantified expressions
51004
+ case "all":
51005
+ return `(all ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
51006
+ case "some":
51007
+ return `(some ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
51008
+ case "no":
51009
+ return `(no ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
51010
+ case "one":
51011
+ return `(one ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
51012
+ case "lone":
51013
+ return `(lone ${node.varName}: ${nodeToString(node.domain)} | ${nodeToString(node.body)})`;
51014
+ // Logical operators
51015
+ case "and":
51016
+ return `(${nodeToString(node.left)} and ${nodeToString(node.right)})`;
51017
+ case "or":
51018
+ return `(${nodeToString(node.left)} or ${nodeToString(node.right)})`;
51019
+ case "implies":
51020
+ return `(${nodeToString(node.left)} => ${nodeToString(node.right)})`;
51021
+ case "iff":
51022
+ return `(${nodeToString(node.left)} <=> ${nodeToString(node.right)})`;
51023
+ case "not":
51024
+ return `!${wrapForPrefix(nodeToString(node.child))}`;
51025
+ // Relational comparison
51026
+ case "in":
51027
+ return `(${nodeToString(node.left)} in ${nodeToString(node.right)})`;
51028
+ case "eq":
51029
+ return `(${nodeToString(node.left)} = ${nodeToString(node.right)})`;
51030
+ case "neq":
51031
+ return `(${nodeToString(node.left)} != ${nodeToString(node.right)})`;
51032
+ // Numeric comparison
51033
+ case "lt":
51034
+ return `(${nodeToString(node.left)} < ${nodeToString(node.right)})`;
51035
+ case "gt":
51036
+ return `(${nodeToString(node.left)} > ${nodeToString(node.right)})`;
51037
+ case "lte":
51038
+ return `(${nodeToString(node.left)} <= ${nodeToString(node.right)})`;
51039
+ case "gte":
51040
+ return `(${nodeToString(node.left)} >= ${nodeToString(node.right)})`;
51041
+ // Box join
51042
+ case "box-join":
51043
+ return `${nodeToString(node.base)}[${node.args.map(nodeToString).join(", ")}]`;
50973
51044
  }
50974
51045
  }
50975
51046
  function wrapForJoin(node) {
50976
- if (node.kind === "identifier" || node.kind === "closure") {
51047
+ // Identifiers and prefix unary operators don't need wrapping
51048
+ if (node.kind === "identifier" ||
51049
+ node.kind === "closure" ||
51050
+ node.kind === "reflexive-closure" ||
51051
+ node.kind === "transpose" ||
51052
+ node.kind === "box-join") {
50977
51053
  return nodeToString(node);
50978
51054
  }
50979
51055
  return `(${nodeToString(node)})`;
@@ -50997,10 +51073,16 @@ function normalizeUnaryResult(result) {
50997
51073
  return null;
50998
51074
  }
50999
51075
  const value = tuple[0];
51000
- if (typeof value !== "string") {
51076
+ // Accept strings or numbers (integers are often returned as numbers)
51077
+ if (typeof value === "string") {
51078
+ ids.add(value);
51079
+ }
51080
+ else if (typeof value === "number") {
51081
+ ids.add(String(value));
51082
+ }
51083
+ else {
51001
51084
  return null;
51002
51085
  }
51003
- ids.add(value);
51004
51086
  }
51005
51087
  return ids;
51006
51088
  }
@@ -51015,10 +51097,13 @@ function normalizeBinaryResult(result) {
51015
51097
  return null;
51016
51098
  }
51017
51099
  const [first, second] = tuple;
51018
- if (typeof first !== "string" || typeof second !== "string") {
51100
+ // Accept strings or numbers for both elements
51101
+ const firstStr = typeof first === "string" ? first : typeof first === "number" ? String(first) : null;
51102
+ const secondStr = typeof second === "string" ? second : typeof second === "number" ? String(second) : null;
51103
+ if (firstStr === null || secondStr === null) {
51019
51104
  return null;
51020
51105
  }
51021
- ids.add(`${first}\u0000${second}`);
51106
+ ids.add(`${firstStr}\u0000${secondStr}`);
51022
51107
  }
51023
51108
  return ids;
51024
51109
  }
@@ -51068,6 +51153,99 @@ function classifyIdentifier(name, datums) {
51068
51153
  }
51069
51154
  return "other";
51070
51155
  }
51156
+ // Build semantically meaningful join candidates based on type-relation compatibility.
51157
+ // For a relation with types [T1, T2, ...], we generate joins like T1.relation
51158
+ // which projects the relation to its range. This helps synthesize expressions
51159
+ // like "Node.key" when selecting integer values that are keys of nodes.
51160
+ function buildSemanticJoinCandidates(datums) {
51161
+ const candidates = [];
51162
+ const seenExpressions = new Set();
51163
+ // Get relations and types that exist across all datums
51164
+ const sharedRelationNames = new Set();
51165
+ const sharedTypeIds = new Set();
51166
+ if (datums.length === 0)
51167
+ return candidates;
51168
+ // Initialize with first datum
51169
+ const firstDatum = datums[0];
51170
+ firstDatum.getRelations().forEach((r) => sharedRelationNames.add(r.name));
51171
+ firstDatum.getTypes().forEach((t) => sharedTypeIds.add(t.id));
51172
+ // Intersect with remaining datums
51173
+ for (let i = 1; i < datums.length; i++) {
51174
+ const datum = datums[i];
51175
+ const datumRelations = new Set(datum.getRelations().map((r) => r.name));
51176
+ const datumTypes = new Set(datum.getTypes().map((t) => t.id));
51177
+ for (const name of sharedRelationNames) {
51178
+ if (!datumRelations.has(name))
51179
+ sharedRelationNames.delete(name);
51180
+ }
51181
+ for (const id of sharedTypeIds) {
51182
+ if (!datumTypes.has(id))
51183
+ sharedTypeIds.delete(id);
51184
+ }
51185
+ }
51186
+ // For each shared relation, check if its domain type is also shared
51187
+ // and create Type.relation join candidates
51188
+ for (const relationName of sharedRelationNames) {
51189
+ // Get the relation's type signature from the first datum (should be consistent)
51190
+ const relation = firstDatum.getRelations().find((r) => r.name === relationName);
51191
+ if (!relation || relation.types.length < 2)
51192
+ continue;
51193
+ const domainType = relation.types[0];
51194
+ // Check if the domain type (or a compatible type) is available
51195
+ // We look for types in the hierarchy that could be the domain
51196
+ for (const typeId of sharedTypeIds) {
51197
+ const typeObj = firstDatum.getTypes().find((t) => t.id === typeId);
51198
+ if (!typeObj)
51199
+ continue;
51200
+ // Check if this type is compatible with the relation's domain
51201
+ // Either the type itself matches, or it's in the type hierarchy
51202
+ const isCompatible = typeId === domainType ||
51203
+ typeObj.types.includes(domainType) ||
51204
+ // Also check if the domain type is a subtype of this type
51205
+ firstDatum.getTypes().find((t) => t.id === domainType)?.types.includes(typeId);
51206
+ if (isCompatible) {
51207
+ const joinNode = {
51208
+ kind: "join",
51209
+ left: { kind: "identifier", name: typeId },
51210
+ right: { kind: "identifier", name: relationName },
51211
+ };
51212
+ const exprString = nodeToString(joinNode);
51213
+ if (!seenExpressions.has(exprString)) {
51214
+ seenExpressions.add(exprString);
51215
+ candidates.push(joinNode);
51216
+ }
51217
+ }
51218
+ }
51219
+ }
51220
+ // Also add relation.Type joins for inverse projections (getting domain values)
51221
+ for (const relationName of sharedRelationNames) {
51222
+ const relation = firstDatum.getRelations().find((r) => r.name === relationName);
51223
+ if (!relation || relation.types.length < 2)
51224
+ continue;
51225
+ const rangeType = relation.types[relation.types.length - 1];
51226
+ for (const typeId of sharedTypeIds) {
51227
+ const typeObj = firstDatum.getTypes().find((t) => t.id === typeId);
51228
+ if (!typeObj)
51229
+ continue;
51230
+ const isCompatible = typeId === rangeType ||
51231
+ typeObj.types.includes(rangeType) ||
51232
+ firstDatum.getTypes().find((t) => t.id === rangeType)?.types.includes(typeId);
51233
+ if (isCompatible) {
51234
+ const joinNode = {
51235
+ kind: "join",
51236
+ left: { kind: "identifier", name: relationName },
51237
+ right: { kind: "identifier", name: typeId },
51238
+ };
51239
+ const exprString = nodeToString(joinNode);
51240
+ if (!seenExpressions.has(exprString)) {
51241
+ seenExpressions.add(exprString);
51242
+ candidates.push(joinNode);
51243
+ }
51244
+ }
51245
+ }
51246
+ }
51247
+ return candidates;
51248
+ }
51071
51249
  function buildBaseNodes(datums) {
51072
51250
  const baseNames = intersectNames(datums);
51073
51251
  // Always include standard top-level identifiers when present in the language
@@ -51125,11 +51303,20 @@ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51125
51303
  if (baseNodes.length === 0) {
51126
51304
  throw new SelectorSynthesisError("No shared identifiers available across provided data instances");
51127
51305
  }
51306
+ // Check base identifiers first
51128
51307
  for (const node of baseNodes) {
51129
51308
  if (matchesTargets(node, evaluatedExamples, normalizer)) {
51130
51309
  return node;
51131
51310
  }
51132
51311
  }
51312
+ // Check semantic join candidates early - these are type-aware joins like Node.key
51313
+ // that are likely to be what the user wants when selecting relation ranges/domains
51314
+ const semanticJoins = buildSemanticJoinCandidates(datums);
51315
+ for (const node of semanticJoins) {
51316
+ if (matchesTargets(node, evaluatedExamples, normalizer)) {
51317
+ return node;
51318
+ }
51319
+ }
51133
51320
  const queue = [];
51134
51321
  const queued = new Set();
51135
51322
  const visited = new Set();
@@ -51141,6 +51328,10 @@ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51141
51328
  queue.push({ node, depth });
51142
51329
  queued.add(key);
51143
51330
  };
51331
+ // Mark semantic joins as already visited so we don't re-check them
51332
+ for (const node of semanticJoins) {
51333
+ visited.add(nodeToString(node));
51334
+ }
51144
51335
  baseNodes.forEach((node) => enqueue(node, 0));
51145
51336
  while (queue.length > 0) {
51146
51337
  const current = queue.shift();
@@ -51156,18 +51347,69 @@ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51156
51347
  if (current.depth >= maxDepth) {
51157
51348
  continue;
51158
51349
  }
51159
- // Unary expansions
51350
+ // Unary relational expansions
51160
51351
  enqueue({ kind: "closure", child: current.node }, current.depth + 1);
51352
+ enqueue({ kind: "reflexive-closure", child: current.node }, current.depth + 1);
51353
+ enqueue({ kind: "transpose", child: current.node }, current.depth + 1);
51161
51354
  for (const other of combinationPool) {
51162
51355
  const leftKey = nodeToString(current.node);
51163
51356
  const rightKey = nodeToString(other);
51357
+ // Set operations (commutative, so canonicalize order)
51164
51358
  const [unionLeft, unionRight] = leftKey < rightKey ? [current.node, other] : [other, current.node];
51165
51359
  enqueue({ kind: "union", left: unionLeft, right: unionRight }, current.depth + 1);
51166
51360
  const [interLeft, interRight] = leftKey < rightKey ? [current.node, other] : [other, current.node];
51167
51361
  enqueue({ kind: "intersection", left: interLeft, right: interRight }, current.depth + 1);
51362
+ // Difference is not commutative
51363
+ enqueue({ kind: "difference", left: current.node, right: other }, current.depth + 1);
51364
+ if (leftKey !== rightKey) {
51365
+ enqueue({ kind: "difference", left: other, right: current.node }, current.depth + 1);
51366
+ }
51367
+ // Join is not commutative
51168
51368
  enqueue({ kind: "join", left: current.node, right: other }, current.depth + 1);
51169
51369
  enqueue({ kind: "join", left: other, right: current.node }, current.depth + 1);
51170
51370
  }
51371
+ // Generate set comprehensions: {v: domain | body}
51372
+ // For each type, try comprehensions with simple membership conditions
51373
+ // This allows synthesizing expressions like {n: Node | n.key in SomeSet}
51374
+ if (current.depth + 2 <= maxDepth) {
51375
+ for (const domainNode of combinationPool) {
51376
+ // Only use types as comprehension domains
51377
+ if (domainNode.kind !== "identifier")
51378
+ continue;
51379
+ const domainName = domainNode.name;
51380
+ const classification = classifyIdentifier(domainName, datums);
51381
+ if (classification !== "type")
51382
+ continue;
51383
+ const varName = "v"; // Use a simple variable name
51384
+ // Try: {v: Domain | v in current.node}
51385
+ const varNode = { kind: "identifier", name: varName };
51386
+ enqueue({
51387
+ kind: "comprehension",
51388
+ varName,
51389
+ domain: domainNode,
51390
+ body: { kind: "in", left: varNode, right: current.node },
51391
+ }, current.depth + 2);
51392
+ // Try: {v: Domain | v.relation in SomeSet} for relations
51393
+ for (const relNode of combinationPool) {
51394
+ if (relNode.kind !== "identifier")
51395
+ continue;
51396
+ if (classifyIdentifier(relNode.name, datums) !== "relation")
51397
+ continue;
51398
+ const joinExpr = {
51399
+ kind: "join",
51400
+ left: varNode,
51401
+ right: relNode,
51402
+ };
51403
+ // {v: Domain | v.relation in current.node}
51404
+ enqueue({
51405
+ kind: "comprehension",
51406
+ varName,
51407
+ domain: domainNode,
51408
+ body: { kind: "in", left: joinExpr, right: current.node },
51409
+ }, current.depth + 2);
51410
+ }
51411
+ }
51412
+ }
51171
51413
  }
51172
51414
  throw new SelectorSynthesisError("Unable to synthesize an expression matching all examples");
51173
51415
  }
@@ -51195,14 +51437,31 @@ function buildWhyNode(node, evaluator, normalizer) {
51195
51437
  result,
51196
51438
  };
51197
51439
  switch (node.kind) {
51440
+ // Leaves
51198
51441
  case "identifier":
51199
51442
  return base;
51443
+ // Unary operators
51200
51444
  case "closure":
51445
+ case "reflexive-closure":
51446
+ case "transpose":
51447
+ case "not":
51201
51448
  return { ...base, children: [buildWhyNode(node.child, evaluator, normalizer)] };
51449
+ // Binary operators
51202
51450
  case "join":
51203
51451
  case "union":
51204
51452
  case "intersection":
51205
51453
  case "difference":
51454
+ case "and":
51455
+ case "or":
51456
+ case "implies":
51457
+ case "iff":
51458
+ case "in":
51459
+ case "eq":
51460
+ case "neq":
51461
+ case "lt":
51462
+ case "gt":
51463
+ case "lte":
51464
+ case "gte":
51206
51465
  return {
51207
51466
  ...base,
51208
51467
  children: [
@@ -51210,6 +51469,29 @@ function buildWhyNode(node, evaluator, normalizer) {
51210
51469
  buildWhyNode(node.right, evaluator, normalizer),
51211
51470
  ],
51212
51471
  };
51472
+ // Quantified and comprehension expressions
51473
+ case "all":
51474
+ case "some":
51475
+ case "no":
51476
+ case "one":
51477
+ case "lone":
51478
+ case "comprehension":
51479
+ return {
51480
+ ...base,
51481
+ children: [
51482
+ buildWhyNode(node.domain, evaluator, normalizer),
51483
+ buildWhyNode(node.body, evaluator, normalizer),
51484
+ ],
51485
+ };
51486
+ // Box join
51487
+ case "box-join":
51488
+ return {
51489
+ ...base,
51490
+ children: [
51491
+ buildWhyNode(node.base, evaluator, normalizer),
51492
+ ...node.args.map((arg) => buildWhyNode(arg, evaluator, normalizer)),
51493
+ ],
51494
+ };
51213
51495
  default:
51214
51496
  return base;
51215
51497
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-graph-query",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
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",