simple-graph-query 2.2.1 → 2.4.0

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/README.md CHANGED
@@ -6,3 +6,31 @@ A TypeScript library for evaluating relational + some more expressions with a br
6
6
 
7
7
  MIT
8
8
 
9
+ ## Selector synthesis overview
10
+
11
+ The selector synthesizer infers a relational expression that returns exactly the atoms supplied in a set of training examples (pairs of `Set<IAtom>` and `IDataInstance`). The current implementation uses a bounded, enumerative search over a compact expression grammar (identifiers, unions/intersections/differences, joins, and transitive closure) that mirrors a lightweight Alloy fragment. The search works breadth-first by depth so that the first solution found is the simplest expression within the bound, and it only explores identifiers shared by every example plus Alloy built-ins like `univ` and `iden`.
12
+
13
+ Although the implementation is enumerative, it follows the same spirit as CEGIS and FOIL-style learners: each candidate expression is validated against every provided example, immediately pruning failures before expanding the search frontier. This tight feedback loop keeps the search space tractable in bounded instances while still allowing compositional operators (e.g., joins composed under closure) to satisfy more relational targets. Raising the `maxDepth` parameter broadens the hypothesis space when more complex solutions are needed, while lower depths keep synthesis fast for simple selectors.
14
+
15
+ ### Synthesis "why" explanations
16
+
17
+ The `synthesizeSelectorWithWhy` and `synthesizeBinaryRelationWithWhy` helpers return a provenance tree alongside the synthesized expression. Each returned `examples[i].why` value mirrors the final expression: every node records the operator kind, the textual subexpression, and the evaluated result for that subexpression in the corresponding datum. Binary operators (union, intersection, difference, join) list two children, closures list one, and identifiers are leaves.
18
+
19
+ For example, synthesizing a selector for `{a1, a2}` and `{b1}` produces:
20
+
21
+ ```ts
22
+ const { expression, examples } = synthesizeSelectorWithWhy([
23
+ { atoms: new Set([a1, a2]), datum: datumA },
24
+ { atoms: new Set([b1]), datum: datumB },
25
+ ]);
26
+
27
+ // expression === "Thing"
28
+ // examples[0].why === {
29
+ // kind: "identifier",
30
+ // expression: "Thing",
31
+ // result: new Set(["a1", "a2"]),
32
+ // }
33
+ ```
34
+
35
+ Synthesizing a binary relation explanation yields the same structure, but `result` contains normalized tuple identifiers (e.g., `"n1\u0000n3"`) for each subexpression. For a two-hop reachability example, the root `why` node would have `kind: "join"` with two child nodes describing the `edge` relations on the left and right of the join.
36
+
@@ -27,6 +27,9 @@ export declare class ForgeExprEvaluator extends AbstractParseTreeVisitor<EvalRes
27
27
  private getLabelAsString;
28
28
  private getLabelAsBoolean;
29
29
  private getLabelAsNumber;
30
+ private isConvertibleToNumber;
31
+ private isConvertibleToBoolean;
32
+ private convertToBoolean;
30
33
  private dotJoin;
31
34
  private cacheResult;
32
35
  private getIden;
@@ -0,0 +1,48 @@
1
+ import { IAtom, IDataInstance } from "./types";
2
+ export type AtomSelectionExample = {
3
+ atoms: Set<IAtom>;
4
+ datum: IDataInstance;
5
+ };
6
+ export type AtomPair = readonly [IAtom, IAtom];
7
+ export type BinaryRelationExample = {
8
+ pairs: Set<AtomPair>;
9
+ datum: IDataInstance;
10
+ };
11
+ type ExpressionNode = {
12
+ kind: "identifier";
13
+ name: string;
14
+ } | {
15
+ kind: "union" | "intersection" | "difference";
16
+ left: ExpressionNode;
17
+ right: ExpressionNode;
18
+ } | {
19
+ kind: "join";
20
+ left: ExpressionNode;
21
+ right: ExpressionNode;
22
+ } | {
23
+ kind: "closure";
24
+ child: ExpressionNode;
25
+ };
26
+ export type WhyNode = {
27
+ kind: ExpressionNode["kind"];
28
+ expression: string;
29
+ result: Set<string> | null;
30
+ children?: WhyNode[];
31
+ };
32
+ export declare class SelectorSynthesisError extends Error {
33
+ }
34
+ export declare function synthesizeSelector(examples: AtomSelectionExample[], maxDepth?: number): string;
35
+ export declare function synthesizeBinaryRelation(examples: BinaryRelationExample[], maxDepth?: number): string;
36
+ export type SynthesisWhyExample = {
37
+ datum: IDataInstance;
38
+ target: Set<string>;
39
+ result: Set<string> | null;
40
+ why: WhyNode;
41
+ };
42
+ export type SynthesisWhy = {
43
+ expression: string;
44
+ examples: SynthesisWhyExample[];
45
+ };
46
+ export declare function synthesizeSelectorWithWhy(examples: AtomSelectionExample[], maxDepth?: number): SynthesisWhy;
47
+ export declare function synthesizeBinaryRelationWithWhy(examples: BinaryRelationExample[], maxDepth?: number): SynthesisWhy;
48
+ export {};
package/dist/index.d.ts CHANGED
@@ -16,3 +16,4 @@ export declare class SimpleGraphQueryEvaluator {
16
16
  getExpressionParseTree(forgeExpr: string): import("./forge-antlr/ForgeParser").ParseExprContext;
17
17
  evaluateExpression(forgeExpr: string): EvaluationResult;
18
18
  }
19
+ export { synthesizeSelector, synthesizeBinaryRelation, synthesizeBinaryRelationWithWhy, synthesizeSelectorWithWhy, AtomSelectionExample, BinaryRelationExample, SelectorSynthesisError, SynthesisWhy, SynthesisWhyExample, WhyNode, } from './SelectorSynthesizer';
@@ -48619,28 +48619,11 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48619
48619
  this.relationCache = new Map();
48620
48620
  this.relationIndexCache = new Map();
48621
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
48622
  for (const relation of relations) {
48640
48623
  let relationAtoms = relation.tuples.map((tuple) => tuple.atoms);
48641
48624
  // 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));
48625
+ relationAtoms = relationAtoms.map((tuple) => tuple.map((value) => this.isConvertibleToNumber(value) ? Number(value) : value));
48626
+ relationAtoms = relationAtoms.map((tuple) => tuple.map((value) => this.isConvertibleToBoolean(value) ? this.convertToBoolean(value) : value));
48644
48627
  this.relationCache.set(relation.name, relationAtoms);
48645
48628
  // Build index for this relation: first element -> all tuples starting with that element
48646
48629
  const relationIndex = new Map();
@@ -48751,6 +48734,39 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
48751
48734
  }
48752
48735
  return labelNum;
48753
48736
  }
48737
+ // Helper function to check if a value can be converted to a number
48738
+ isConvertibleToNumber(value) {
48739
+ if (typeof value === "number") {
48740
+ return true;
48741
+ }
48742
+ if (typeof value === "string") {
48743
+ return !isNaN(Number(value));
48744
+ }
48745
+ return false;
48746
+ }
48747
+ // Helper function to check if a value can be converted to a boolean
48748
+ isConvertibleToBoolean(value) {
48749
+ if (typeof value === "boolean") {
48750
+ return true;
48751
+ }
48752
+ if (typeof value === "string") {
48753
+ return (value === "true" || value === "#t" || value === "false" || value === "#f");
48754
+ }
48755
+ return false;
48756
+ }
48757
+ // Helper function to convert a value to boolean
48758
+ convertToBoolean(value) {
48759
+ if (typeof value === "boolean") {
48760
+ return value;
48761
+ }
48762
+ if (value === "true" || value === "#t") {
48763
+ return true;
48764
+ }
48765
+ if (value === "false" || value === "#f") {
48766
+ return false;
48767
+ }
48768
+ throw new Error(`Cannot convert ${value} to boolean`);
48769
+ }
48754
48770
  // Optimized dotJoin that can use pre-built relation indexes
48755
48771
  dotJoin(left, right, rightRelationName) {
48756
48772
  const leftExpr = isSingleValue(left) ? [[left]] : left;
@@ -49953,6 +49969,42 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
49953
49969
  // }
49954
49970
  return value;
49955
49971
  }
49972
+ // Handle iden (identity relation)
49973
+ if (constant.IDEN_TOK() !== undefined) {
49974
+ // The identity relation contains tuples (x, x) for every atom x in the universe
49975
+ const atoms = this.instanceData.getAtoms();
49976
+ const idenRelation = [];
49977
+ for (const atom of atoms) {
49978
+ let atomValue = atom.id;
49979
+ // Convert numeric and boolean strings to their actual types
49980
+ if (this.isConvertibleToNumber(atomValue)) {
49981
+ atomValue = Number(atomValue);
49982
+ }
49983
+ else if (this.isConvertibleToBoolean(atomValue)) {
49984
+ atomValue = this.convertToBoolean(atomValue);
49985
+ }
49986
+ idenRelation.push([atomValue, atomValue]);
49987
+ }
49988
+ return idenRelation;
49989
+ }
49990
+ // Handle univ (universal relation - all atoms)
49991
+ if (constant.UNIV_TOK() !== undefined) {
49992
+ // The universal relation contains all atoms as unary tuples
49993
+ const atoms = this.instanceData.getAtoms();
49994
+ const univRelation = [];
49995
+ for (const atom of atoms) {
49996
+ let atomValue = atom.id;
49997
+ // Convert numeric and boolean strings to their actual types
49998
+ if (this.isConvertibleToNumber(atomValue)) {
49999
+ atomValue = Number(atomValue);
50000
+ }
50001
+ else if (this.isConvertibleToBoolean(atomValue)) {
50002
+ atomValue = this.convertToBoolean(atomValue);
50003
+ }
50004
+ univRelation.push([atomValue]);
50005
+ }
50006
+ return univRelation;
50007
+ }
49956
50008
  // Handle boolean constants
49957
50009
  if (constant.text === 'true') {
49958
50010
  return true;
@@ -50184,46 +50236,15 @@ class ForgeExprEvaluator extends AbstractParseTreeVisitor_1.AbstractParseTreeVis
50184
50236
  }
50185
50237
  }
50186
50238
  }
50187
- // defining 3 helper functions here; not for use elsewhere
50188
- const isConvertibleToNumber = (value) => {
50189
- if (typeof value === "number") {
50190
- return true;
50191
- }
50192
- if (typeof value === "string") {
50193
- return !isNaN(Number(value));
50194
- }
50195
- return false;
50196
- };
50197
- const isConvertibleToBoolean = (value) => {
50198
- if (typeof value === "boolean") {
50199
- return true;
50200
- }
50201
- if (typeof value === "string") {
50202
- return (value === "true" || value === "#t" || value === "false" || value === "#f");
50203
- }
50204
- return false;
50205
- };
50206
- const convertToBoolean = (value) => {
50207
- if (typeof value === "boolean") {
50208
- return value;
50209
- }
50210
- if (value === "true" || value === "#t") {
50211
- return true;
50212
- }
50213
- if (value === "false" || value === "#f") {
50214
- return false;
50215
- }
50216
- throw new Error(`Cannot convert ${value} to boolean`);
50217
- };
50218
- // end of 3 helper functions
50239
+ // end of type search
50219
50240
  // check if it is a relation - use cache for faster lookups
50220
50241
  this.buildRelationCache();
50221
50242
  if (this.relationCache.has(identifier)) {
50222
50243
  return this.relationCache.get(identifier);
50223
50244
  }
50224
50245
  if (result !== undefined) {
50225
- result = result.map((tuple) => tuple.map((value) => isConvertibleToNumber(value) ? Number(value) : value));
50226
- result = result.map((tuple) => tuple.map((value) => isConvertibleToBoolean(value) ? convertToBoolean(value) : value));
50246
+ result = result.map((tuple) => tuple.map((value) => this.isConvertibleToNumber(value) ? Number(value) : value));
50247
+ result = result.map((tuple) => tuple.map((value) => this.isConvertibleToBoolean(value) ? this.convertToBoolean(value) : value));
50227
50248
  return result;
50228
50249
  }
50229
50250
  // return identifier;
@@ -50910,6 +50931,275 @@ function cartesianProduct(arrays) {
50910
50931
  }
50911
50932
 
50912
50933
 
50934
+ /***/ }),
50935
+
50936
+ /***/ "./src/SelectorSynthesizer.ts":
50937
+ /*!************************************!*\
50938
+ !*** ./src/SelectorSynthesizer.ts ***!
50939
+ \************************************/
50940
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
50941
+
50942
+ "use strict";
50943
+
50944
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
50945
+ exports.SelectorSynthesisError = void 0;
50946
+ exports.synthesizeSelector = synthesizeSelector;
50947
+ exports.synthesizeBinaryRelation = synthesizeBinaryRelation;
50948
+ exports.synthesizeSelectorWithWhy = synthesizeSelectorWithWhy;
50949
+ exports.synthesizeBinaryRelationWithWhy = synthesizeBinaryRelationWithWhy;
50950
+ const index_1 = __webpack_require__(/*! ./index */ "./src/index.ts");
50951
+ class SelectorSynthesisError extends Error {
50952
+ }
50953
+ exports.SelectorSynthesisError = SelectorSynthesisError;
50954
+ function nodeToString(node) {
50955
+ switch (node.kind) {
50956
+ case "identifier":
50957
+ return node.name;
50958
+ case "closure": {
50959
+ const inner = nodeToString(node.child);
50960
+ return `^${wrapForPrefix(inner)}`;
50961
+ }
50962
+ case "join": {
50963
+ const left = wrapForJoin(node.left);
50964
+ const right = wrapForJoin(node.right);
50965
+ return `${left}.${right}`;
50966
+ }
50967
+ case "union":
50968
+ return `(${nodeToString(node.left)} + ${nodeToString(node.right)})`;
50969
+ case "intersection":
50970
+ return `(${nodeToString(node.left)} & ${nodeToString(node.right)})`;
50971
+ case "difference":
50972
+ return `(${nodeToString(node.left)} - ${nodeToString(node.right)})`;
50973
+ }
50974
+ }
50975
+ function wrapForJoin(node) {
50976
+ if (node.kind === "identifier" || node.kind === "closure") {
50977
+ return nodeToString(node);
50978
+ }
50979
+ return `(${nodeToString(node)})`;
50980
+ }
50981
+ function wrapForPrefix(expr) {
50982
+ return expr.startsWith("(") && expr.endsWith(")") ? expr : `(${expr})`;
50983
+ }
50984
+ function normalizeUnaryResult(result) {
50985
+ // We only consider unary tuple results or single string values as valid atom selections
50986
+ if (typeof result === "string") {
50987
+ return new Set([result]);
50988
+ }
50989
+ if (!Array.isArray(result)) {
50990
+ return null;
50991
+ }
50992
+ // Expect an array of tuples
50993
+ const tuples = result;
50994
+ const ids = new Set();
50995
+ for (const tuple of tuples) {
50996
+ if (!Array.isArray(tuple) || tuple.length !== 1) {
50997
+ return null;
50998
+ }
50999
+ const value = tuple[0];
51000
+ if (typeof value !== "string") {
51001
+ return null;
51002
+ }
51003
+ ids.add(value);
51004
+ }
51005
+ return ids;
51006
+ }
51007
+ function normalizeBinaryResult(result) {
51008
+ if (!Array.isArray(result)) {
51009
+ return null;
51010
+ }
51011
+ const tuples = result;
51012
+ const ids = new Set();
51013
+ for (const tuple of tuples) {
51014
+ if (!Array.isArray(tuple) || tuple.length !== 2) {
51015
+ return null;
51016
+ }
51017
+ const [first, second] = tuple;
51018
+ if (typeof first !== "string" || typeof second !== "string") {
51019
+ return null;
51020
+ }
51021
+ ids.add(`${first}\u0000${second}`);
51022
+ }
51023
+ return ids;
51024
+ }
51025
+ function evaluateExpression(node, datum, normalizer) {
51026
+ const evaluator = new index_1.SimpleGraphQueryEvaluator(datum);
51027
+ const expression = nodeToString(node);
51028
+ const result = evaluator.evaluateExpression(expression);
51029
+ return normalizer(result);
51030
+ }
51031
+ function intersectNames(datums) {
51032
+ const identifierSets = datums.map((datum) => {
51033
+ const typeIds = datum.getTypes().map((t) => t.id);
51034
+ const relationNames = datum.getRelations().map((r) => r.name);
51035
+ return new Set([...typeIds, ...relationNames]);
51036
+ });
51037
+ if (identifierSets.length === 0) {
51038
+ return new Set();
51039
+ }
51040
+ const [first, ...rest] = identifierSets;
51041
+ const intersection = new Set();
51042
+ for (const id of first) {
51043
+ if (rest.every((set) => set.has(id))) {
51044
+ intersection.add(id);
51045
+ }
51046
+ }
51047
+ return intersection;
51048
+ }
51049
+ function setsEqual(a, b) {
51050
+ if (a.size !== b.size)
51051
+ return false;
51052
+ for (const item of a) {
51053
+ if (!b.has(item))
51054
+ return false;
51055
+ }
51056
+ return true;
51057
+ }
51058
+ function buildBaseNodes(datums) {
51059
+ const baseNames = intersectNames(datums);
51060
+ // Always include standard top-level identifiers when present in the language
51061
+ ["univ", "iden"].forEach((builtin) => baseNames.add(builtin));
51062
+ return Array.from(baseNames).map((name) => ({ kind: "identifier", name }));
51063
+ }
51064
+ function matchesTargets(node, examples, normalizer) {
51065
+ for (const example of examples) {
51066
+ const result = evaluateExpression(node, example.datum, normalizer);
51067
+ if (!result)
51068
+ return false;
51069
+ if (!setsEqual(result, example.target)) {
51070
+ return false;
51071
+ }
51072
+ }
51073
+ return true;
51074
+ }
51075
+ function synthesizeExpressionNode(examples, normalizer, maxDepth = 3) {
51076
+ // Enumerative, CEGIS-style search: we BFS over the expression grammar (identifiers, set ops, joins, closure),
51077
+ // checking each candidate against *all* examples before allowing it to generate children. Early rejection of
51078
+ // incorrect hypotheses keeps the frontier small (FOIL-esque pruning), while depth-bounding curbs blowup.
51079
+ if (examples.length === 0) {
51080
+ throw new SelectorSynthesisError("No examples provided for synthesis");
51081
+ }
51082
+ const datums = examples.map((example) => example.datum);
51083
+ const baseNodes = buildBaseNodes(datums);
51084
+ if (baseNodes.length === 0) {
51085
+ throw new SelectorSynthesisError("No shared identifiers available across provided data instances");
51086
+ }
51087
+ const queue = [];
51088
+ const queued = new Set();
51089
+ const visited = new Set();
51090
+ const combinationPool = [...baseNodes];
51091
+ const enqueue = (node, depth) => {
51092
+ const key = nodeToString(node);
51093
+ if (visited.has(key) || queued.has(key))
51094
+ return;
51095
+ queue.push({ node, depth });
51096
+ queued.add(key);
51097
+ };
51098
+ baseNodes.forEach((node) => enqueue(node, 0));
51099
+ while (queue.length > 0) {
51100
+ const current = queue.shift();
51101
+ const key = nodeToString(current.node);
51102
+ queued.delete(key);
51103
+ if (visited.has(key)) {
51104
+ continue;
51105
+ }
51106
+ visited.add(key);
51107
+ if (matchesTargets(current.node, examples, normalizer)) {
51108
+ return current.node;
51109
+ }
51110
+ if (current.depth >= maxDepth) {
51111
+ continue;
51112
+ }
51113
+ // Unary expansions
51114
+ enqueue({ kind: "closure", child: current.node }, current.depth + 1);
51115
+ for (const other of combinationPool) {
51116
+ const leftKey = nodeToString(current.node);
51117
+ const rightKey = nodeToString(other);
51118
+ const [unionLeft, unionRight] = leftKey < rightKey ? [current.node, other] : [other, current.node];
51119
+ enqueue({ kind: "union", left: unionLeft, right: unionRight }, current.depth + 1);
51120
+ const [interLeft, interRight] = leftKey < rightKey ? [current.node, other] : [other, current.node];
51121
+ enqueue({ kind: "intersection", left: interLeft, right: interRight }, current.depth + 1);
51122
+ enqueue({ kind: "join", left: current.node, right: other }, current.depth + 1);
51123
+ enqueue({ kind: "join", left: other, right: current.node }, current.depth + 1);
51124
+ }
51125
+ }
51126
+ throw new SelectorSynthesisError("Unable to synthesize an expression matching all examples");
51127
+ }
51128
+ function synthesizeSelector(examples, maxDepth = 3) {
51129
+ const synthesisExamples = examples.map((example) => ({
51130
+ datum: example.datum,
51131
+ target: new Set(Array.from(example.atoms).map((atom) => atom.id)),
51132
+ }));
51133
+ const node = synthesizeExpressionNode(synthesisExamples, normalizeUnaryResult, maxDepth);
51134
+ return nodeToString(node);
51135
+ }
51136
+ function synthesizeBinaryRelation(examples, maxDepth = 3) {
51137
+ const synthesisExamples = examples.map((example) => {
51138
+ const encodedPairs = new Set(Array.from(example.pairs).map(([left, right]) => `${left.id}\u0000${right.id}`));
51139
+ return { datum: example.datum, target: encodedPairs };
51140
+ });
51141
+ const node = synthesizeExpressionNode(synthesisExamples, normalizeBinaryResult, maxDepth);
51142
+ return nodeToString(node);
51143
+ }
51144
+ function buildWhyNode(node, datum, normalizer) {
51145
+ const result = evaluateExpression(node, datum, normalizer);
51146
+ const base = {
51147
+ kind: node.kind,
51148
+ expression: nodeToString(node),
51149
+ result,
51150
+ };
51151
+ switch (node.kind) {
51152
+ case "identifier":
51153
+ return base;
51154
+ case "closure":
51155
+ return { ...base, children: [buildWhyNode(node.child, datum, normalizer)] };
51156
+ case "join":
51157
+ case "union":
51158
+ case "intersection":
51159
+ case "difference":
51160
+ return {
51161
+ ...base,
51162
+ children: [
51163
+ buildWhyNode(node.left, datum, normalizer),
51164
+ buildWhyNode(node.right, datum, normalizer),
51165
+ ],
51166
+ };
51167
+ default:
51168
+ return base;
51169
+ }
51170
+ }
51171
+ function synthesizeSelectorWithWhy(examples, maxDepth = 3) {
51172
+ const synthesisExamples = examples.map((example) => ({
51173
+ datum: example.datum,
51174
+ target: new Set(Array.from(example.atoms).map((atom) => atom.id)),
51175
+ }));
51176
+ const node = synthesizeExpressionNode(synthesisExamples, normalizeUnaryResult, maxDepth);
51177
+ const expression = nodeToString(node);
51178
+ const explanationExamples = synthesisExamples.map((example) => ({
51179
+ datum: example.datum,
51180
+ target: example.target,
51181
+ result: evaluateExpression(node, example.datum, normalizeUnaryResult),
51182
+ why: buildWhyNode(node, example.datum, normalizeUnaryResult),
51183
+ }));
51184
+ return { expression, examples: explanationExamples };
51185
+ }
51186
+ function synthesizeBinaryRelationWithWhy(examples, maxDepth = 3) {
51187
+ const synthesisExamples = examples.map((example) => {
51188
+ const encodedPairs = new Set(Array.from(example.pairs).map(([left, right]) => `${left.id}\u0000${right.id}`));
51189
+ return { datum: example.datum, target: encodedPairs };
51190
+ });
51191
+ const node = synthesizeExpressionNode(synthesisExamples, normalizeBinaryResult, maxDepth);
51192
+ const expression = nodeToString(node);
51193
+ const explanationExamples = synthesisExamples.map((example) => ({
51194
+ datum: example.datum,
51195
+ target: example.target,
51196
+ result: evaluateExpression(node, example.datum, normalizeBinaryResult),
51197
+ why: buildWhyNode(node, example.datum, normalizeBinaryResult),
51198
+ }));
51199
+ return { expression, examples: explanationExamples };
51200
+ }
51201
+
51202
+
50913
51203
  /***/ }),
50914
51204
 
50915
51205
  /***/ "./src/errorListener.ts":
@@ -61451,71 +61741,18 @@ class ConsistencyAssertionTest extends SyntaxNode {
61451
61741
  exports.ConsistencyAssertionTest = ConsistencyAssertionTest;
61452
61742
 
61453
61743
 
61454
- /***/ })
61744
+ /***/ }),
61455
61745
 
61456
- /******/ });
61457
- /************************************************************************/
61458
- /******/ // The module cache
61459
- /******/ var __webpack_module_cache__ = {};
61460
- /******/
61461
- /******/ // The require function
61462
- /******/ function __webpack_require__(moduleId) {
61463
- /******/ // Check if module is in cache
61464
- /******/ var cachedModule = __webpack_module_cache__[moduleId];
61465
- /******/ if (cachedModule !== undefined) {
61466
- /******/ return cachedModule.exports;
61467
- /******/ }
61468
- /******/ // Create a new module (and put it into the cache)
61469
- /******/ var module = __webpack_module_cache__[moduleId] = {
61470
- /******/ id: moduleId,
61471
- /******/ loaded: false,
61472
- /******/ exports: {}
61473
- /******/ };
61474
- /******/
61475
- /******/ // Execute the module function
61476
- /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
61477
- /******/
61478
- /******/ // Flag the module as loaded
61479
- /******/ module.loaded = true;
61480
- /******/
61481
- /******/ // Return the exports of the module
61482
- /******/ return module.exports;
61483
- /******/ }
61484
- /******/
61485
- /************************************************************************/
61486
- /******/ /* webpack/runtime/global */
61487
- /******/ (() => {
61488
- /******/ __webpack_require__.g = (function() {
61489
- /******/ if (typeof globalThis === 'object') return globalThis;
61490
- /******/ try {
61491
- /******/ return this || new Function('return this')();
61492
- /******/ } catch (e) {
61493
- /******/ if (typeof window === 'object') return window;
61494
- /******/ }
61495
- /******/ })();
61496
- /******/ })();
61497
- /******/
61498
- /******/ /* webpack/runtime/node module decorator */
61499
- /******/ (() => {
61500
- /******/ __webpack_require__.nmd = (module) => {
61501
- /******/ module.paths = [];
61502
- /******/ if (!module.children) module.children = [];
61503
- /******/ return module;
61504
- /******/ };
61505
- /******/ })();
61506
- /******/
61507
- /************************************************************************/
61508
- var __webpack_exports__ = {};
61509
- // This entry needs to be wrapped in an IIFE because it needs to be in strict mode.
61510
- (() => {
61511
- "use strict";
61512
- var exports = __webpack_exports__;
61746
+ /***/ "./src/index.ts":
61513
61747
  /*!**********************!*\
61514
61748
  !*** ./src/index.ts ***!
61515
61749
  \**********************/
61750
+ /***/ ((__unused_webpack_module, exports, __webpack_require__) => {
61751
+
61752
+ "use strict";
61516
61753
 
61517
61754
  Object.defineProperty(exports, "__esModule", ({ value: true }));
61518
- exports.SimpleGraphQueryEvaluator = void 0;
61755
+ exports.SelectorSynthesisError = exports.synthesizeSelectorWithWhy = exports.synthesizeBinaryRelationWithWhy = exports.synthesizeBinaryRelation = exports.synthesizeSelector = exports.SimpleGraphQueryEvaluator = void 0;
61519
61756
  const antlr4ts_1 = __webpack_require__(/*! antlr4ts */ "./node_modules/antlr4ts/index.js");
61520
61757
  const ForgeParser_1 = __webpack_require__(/*! ./forge-antlr/ForgeParser */ "./src/forge-antlr/ForgeParser.ts");
61521
61758
  const ForgeLexer_1 = __webpack_require__(/*! ./forge-antlr/ForgeLexer */ "./src/forge-antlr/ForgeLexer.ts");
@@ -61598,9 +61835,74 @@ class SimpleGraphQueryEvaluator {
61598
61835
  }
61599
61836
  }
61600
61837
  exports.SimpleGraphQueryEvaluator = SimpleGraphQueryEvaluator;
61838
+ var SelectorSynthesizer_1 = __webpack_require__(/*! ./SelectorSynthesizer */ "./src/SelectorSynthesizer.ts");
61839
+ Object.defineProperty(exports, "synthesizeSelector", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.synthesizeSelector; } }));
61840
+ Object.defineProperty(exports, "synthesizeBinaryRelation", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.synthesizeBinaryRelation; } }));
61841
+ Object.defineProperty(exports, "synthesizeBinaryRelationWithWhy", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.synthesizeBinaryRelationWithWhy; } }));
61842
+ Object.defineProperty(exports, "synthesizeSelectorWithWhy", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.synthesizeSelectorWithWhy; } }));
61843
+ Object.defineProperty(exports, "SelectorSynthesisError", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.SelectorSynthesisError; } }));
61601
61844
 
61602
- })();
61603
61845
 
61846
+ /***/ })
61847
+
61848
+ /******/ });
61849
+ /************************************************************************/
61850
+ /******/ // The module cache
61851
+ /******/ var __webpack_module_cache__ = {};
61852
+ /******/
61853
+ /******/ // The require function
61854
+ /******/ function __webpack_require__(moduleId) {
61855
+ /******/ // Check if module is in cache
61856
+ /******/ var cachedModule = __webpack_module_cache__[moduleId];
61857
+ /******/ if (cachedModule !== undefined) {
61858
+ /******/ return cachedModule.exports;
61859
+ /******/ }
61860
+ /******/ // Create a new module (and put it into the cache)
61861
+ /******/ var module = __webpack_module_cache__[moduleId] = {
61862
+ /******/ id: moduleId,
61863
+ /******/ loaded: false,
61864
+ /******/ exports: {}
61865
+ /******/ };
61866
+ /******/
61867
+ /******/ // Execute the module function
61868
+ /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__);
61869
+ /******/
61870
+ /******/ // Flag the module as loaded
61871
+ /******/ module.loaded = true;
61872
+ /******/
61873
+ /******/ // Return the exports of the module
61874
+ /******/ return module.exports;
61875
+ /******/ }
61876
+ /******/
61877
+ /************************************************************************/
61878
+ /******/ /* webpack/runtime/global */
61879
+ /******/ (() => {
61880
+ /******/ __webpack_require__.g = (function() {
61881
+ /******/ if (typeof globalThis === 'object') return globalThis;
61882
+ /******/ try {
61883
+ /******/ return this || new Function('return this')();
61884
+ /******/ } catch (e) {
61885
+ /******/ if (typeof window === 'object') return window;
61886
+ /******/ }
61887
+ /******/ })();
61888
+ /******/ })();
61889
+ /******/
61890
+ /******/ /* webpack/runtime/node module decorator */
61891
+ /******/ (() => {
61892
+ /******/ __webpack_require__.nmd = (module) => {
61893
+ /******/ module.paths = [];
61894
+ /******/ if (!module.children) module.children = [];
61895
+ /******/ return module;
61896
+ /******/ };
61897
+ /******/ })();
61898
+ /******/
61899
+ /************************************************************************/
61900
+ /******/
61901
+ /******/ // startup
61902
+ /******/ // Load entry module and return exports
61903
+ /******/ // This entry module is referenced by other modules so it can't be inlined
61904
+ /******/ var __webpack_exports__ = __webpack_require__("./src/index.ts");
61905
+ /******/
61604
61906
  /******/ return __webpack_exports__;
61605
61907
  /******/ })()
61606
61908
  ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simple-graph-query",
3
- "version": "2.2.1",
3
+ "version": "2.4.0",
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",