simple-graph-query 2.6.0 → 2.7.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/dist/ForgeExprStaticAnalyzer.d.ts +71 -0
- package/dist/index.d.ts +60 -1
- package/dist/simple-graph-query.bundle.js +1045 -2
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AbstractParseTreeVisitor } from "antlr4ts/tree/AbstractParseTreeVisitor";
|
|
2
|
+
import { ParseTree } from "antlr4ts/tree/ParseTree";
|
|
3
|
+
import { ForgeVisitor } from "./forge-antlr/ForgeVisitor";
|
|
4
|
+
import { Expr1Context, Expr1_5Context, Expr2Context, Expr3Context, Expr4Context, Expr5Context, Expr6Context, Expr7Context, Expr8Context, Expr9Context, Expr11Context, Expr12Context, Expr14Context, Expr15Context, Expr17Context, Expr18Context, ExprContext, NameContext, BlockContext } from "./forge-antlr/ForgeParser";
|
|
5
|
+
import { IForgeSchema } from "./types";
|
|
6
|
+
type Abstract = {
|
|
7
|
+
kind: "bool";
|
|
8
|
+
value: boolean;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "num";
|
|
11
|
+
value: number;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "empty";
|
|
14
|
+
} | {
|
|
15
|
+
kind: "typed";
|
|
16
|
+
arity: number;
|
|
17
|
+
columnTypes?: readonly string[];
|
|
18
|
+
} | {
|
|
19
|
+
kind: "ill-typed";
|
|
20
|
+
reason: string;
|
|
21
|
+
} | {
|
|
22
|
+
kind: "unknown";
|
|
23
|
+
};
|
|
24
|
+
export type StaticAnalysis = {
|
|
25
|
+
status: "unsat";
|
|
26
|
+
reason: string;
|
|
27
|
+
} | {
|
|
28
|
+
status: "tautology";
|
|
29
|
+
reason: string;
|
|
30
|
+
} | {
|
|
31
|
+
status: "empty";
|
|
32
|
+
reason: string;
|
|
33
|
+
} | {
|
|
34
|
+
status: "ill-typed";
|
|
35
|
+
reason: string;
|
|
36
|
+
} | {
|
|
37
|
+
status: "unknown";
|
|
38
|
+
};
|
|
39
|
+
export declare class ForgeExprStaticAnalyzer extends AbstractParseTreeVisitor<Abstract> implements ForgeVisitor<Abstract> {
|
|
40
|
+
private readonly schema?;
|
|
41
|
+
constructor(schema?: IForgeSchema);
|
|
42
|
+
private collectNamesFromList;
|
|
43
|
+
private collectBoundNames;
|
|
44
|
+
private illTypedIfBindsReservedName;
|
|
45
|
+
analyze(ctx: ParseTree): StaticAnalysis;
|
|
46
|
+
private static arityOf;
|
|
47
|
+
protected defaultResult(): Abstract;
|
|
48
|
+
private static bailIfIllTyped;
|
|
49
|
+
protected aggregateResult(aggregate: Abstract, nextResult: Abstract): Abstract;
|
|
50
|
+
visitExpr(ctx: ExprContext): Abstract;
|
|
51
|
+
private foldQuantifier;
|
|
52
|
+
visitBlock(ctx: BlockContext): Abstract;
|
|
53
|
+
visitExpr1(ctx: Expr1Context): Abstract;
|
|
54
|
+
visitExpr1_5(ctx: Expr1_5Context): Abstract;
|
|
55
|
+
visitExpr2(ctx: Expr2Context): Abstract;
|
|
56
|
+
visitExpr3(ctx: Expr3Context): Abstract;
|
|
57
|
+
visitExpr4(ctx: Expr4Context): Abstract;
|
|
58
|
+
visitExpr5(ctx: Expr5Context): Abstract;
|
|
59
|
+
visitExpr6(ctx: Expr6Context): Abstract;
|
|
60
|
+
visitExpr7(ctx: Expr7Context): Abstract;
|
|
61
|
+
visitExpr8(ctx: Expr8Context): Abstract;
|
|
62
|
+
visitExpr9(ctx: Expr9Context): Abstract;
|
|
63
|
+
visitExpr12(ctx: Expr12Context): Abstract;
|
|
64
|
+
visitExpr11(ctx: Expr11Context): Abstract;
|
|
65
|
+
visitExpr14(ctx: Expr14Context): Abstract;
|
|
66
|
+
visitExpr15(ctx: Expr15Context): Abstract;
|
|
67
|
+
visitExpr17(ctx: Expr17Context): Abstract;
|
|
68
|
+
visitExpr18(ctx: Expr18Context): Abstract;
|
|
69
|
+
visitName(ctx: NameContext): Abstract;
|
|
70
|
+
}
|
|
71
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,20 +1,79 @@
|
|
|
1
1
|
import { ForgeListenerImpl } from './forge-antlr/ForgeListenerImpl';
|
|
2
2
|
import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker';
|
|
3
3
|
import { EvalResult } from './ForgeExprEvaluator';
|
|
4
|
-
import {
|
|
4
|
+
import { ForgeExprStaticAnalyzer, StaticAnalysis } from './ForgeExprStaticAnalyzer';
|
|
5
|
+
import { IDataInstance, IForgeSchema } from './types';
|
|
5
6
|
export type ErrorResult = {
|
|
6
7
|
error: Error;
|
|
7
8
|
stackTrace?: string;
|
|
8
9
|
};
|
|
9
10
|
export type EvaluationResult = EvalResult | ErrorResult;
|
|
11
|
+
/**
|
|
12
|
+
* Evaluates Forge expressions against an `IDataInstance`.
|
|
13
|
+
*
|
|
14
|
+
* ## Caching contract (important for correctness)
|
|
15
|
+
*
|
|
16
|
+
* For performance, this class caches an inner evaluator (with its relation
|
|
17
|
+
* index and subexpression cache) across `evaluateExpression` calls. The
|
|
18
|
+
* cache is invalidated when the `datum` field is reassigned to a different
|
|
19
|
+
* reference, but **not** when the underlying `IDataInstance` is mutated in
|
|
20
|
+
* place (e.g. adding/removing atoms or tuples on the same object).
|
|
21
|
+
*
|
|
22
|
+
* If you mutate the underlying data in place, you **must** call
|
|
23
|
+
* {@link invalidate} (or reassign `datum` to a new object) before the next
|
|
24
|
+
* `evaluateExpression` call. Otherwise queries may return stale results.
|
|
25
|
+
*
|
|
26
|
+
* Recommended patterns:
|
|
27
|
+
* - Treat `IDataInstance` as immutable; create a new instance on data
|
|
28
|
+
* changes and construct a new `SimpleGraphQueryEvaluator` (or assign to
|
|
29
|
+
* `.datum`).
|
|
30
|
+
* - Or, if you mutate in place, call `invalidate()` after each mutation
|
|
31
|
+
* batch.
|
|
32
|
+
*/
|
|
10
33
|
export declare class SimpleGraphQueryEvaluator {
|
|
34
|
+
/**
|
|
35
|
+
* The data instance evaluated against. Reassigning this field is a
|
|
36
|
+
* supported invalidation signal — the inner evaluator cache is rebuilt
|
|
37
|
+
* on the next `evaluateExpression` call. **In-place mutation of the
|
|
38
|
+
* existing object is NOT detected**; call {@link invalidate} in that
|
|
39
|
+
* case.
|
|
40
|
+
*/
|
|
11
41
|
datum: IDataInstance;
|
|
12
42
|
forgeListener: ForgeListenerImpl;
|
|
13
43
|
walker: ParseTreeWalker;
|
|
14
44
|
private parseTreeCache;
|
|
45
|
+
private cachedEvaluator;
|
|
46
|
+
private cachedEvaluatorDatum;
|
|
15
47
|
constructor(datum: IDataInstance);
|
|
48
|
+
/**
|
|
49
|
+
* Discard the cached inner evaluator (and its relation index /
|
|
50
|
+
* subexpression cache). Call this after mutating the underlying
|
|
51
|
+
* `IDataInstance` in place, so the next `evaluateExpression` sees the
|
|
52
|
+
* updated data.
|
|
53
|
+
*
|
|
54
|
+
* Cheap: just nulls a couple of references. The caches rebuild lazily
|
|
55
|
+
* on the next query.
|
|
56
|
+
*/
|
|
57
|
+
invalidate(): void;
|
|
16
58
|
getExpressionParseTree(forgeExpr: string): import("./forge-antlr/ForgeParser").ParseExprContext;
|
|
17
59
|
evaluateExpression(forgeExpr: string): EvaluationResult;
|
|
18
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Run a static analysis on a Forge expression.
|
|
63
|
+
*
|
|
64
|
+
* Returns `unsat` when the expression provably reduces to `false`, `empty`
|
|
65
|
+
* when it provably reduces to the empty set, `tautology` when it provably
|
|
66
|
+
* reduces to `true`, `ill-typed` for static type errors (e.g. arity
|
|
67
|
+
* mismatch), and `unknown` otherwise (including parse errors).
|
|
68
|
+
*
|
|
69
|
+
* When `schema` is provided, the analyzer also uses the type lattice and
|
|
70
|
+
* relation declarations to detect type-disjoint intersections, subtype
|
|
71
|
+
* tautologies in `in`, join column-type mismatches, and arity errors.
|
|
72
|
+
* Disjointness uses a closed-world rule (A ∩ B = ∅ iff no type in the
|
|
73
|
+
* lattice has both A and B in its lineage).
|
|
74
|
+
*/
|
|
75
|
+
export declare function analyzeForgeExpression(forgeExpr: string, schema?: IForgeSchema): StaticAnalysis;
|
|
76
|
+
export { ForgeExprStaticAnalyzer, StaticAnalysis };
|
|
77
|
+
export type { IForgeSchema };
|
|
19
78
|
export { synthesizeSelector, synthesizeBinaryRelation, synthesizeBinaryRelationWithWhy, synthesizeSelectorWithWhy, AtomSelectionExample, BinaryRelationExample, SelectorSynthesisError, SynthesisWhy, SynthesisWhyExample, WhyNode, } from './SelectorSynthesizer';
|
|
20
79
|
export { getIdentifierName, quoteIfReserved, FORGE_RESERVED_KEYWORDS, } from './forge-antlr/utils';
|
|
@@ -50801,6 +50801,963 @@ class ForgeExprFreeVariableFinder extends AbstractParseTreeVisitor_1.AbstractPar
|
|
|
50801
50801
|
exports.ForgeExprFreeVariableFinder = ForgeExprFreeVariableFinder;
|
|
50802
50802
|
|
|
50803
50803
|
|
|
50804
|
+
/***/ }),
|
|
50805
|
+
|
|
50806
|
+
/***/ "./src/ForgeExprStaticAnalyzer.ts":
|
|
50807
|
+
/*!****************************************!*\
|
|
50808
|
+
!*** ./src/ForgeExprStaticAnalyzer.ts ***!
|
|
50809
|
+
\****************************************/
|
|
50810
|
+
/***/ ((__unused_webpack_module, exports, __webpack_require__) => {
|
|
50811
|
+
|
|
50812
|
+
"use strict";
|
|
50813
|
+
|
|
50814
|
+
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
50815
|
+
exports.ForgeExprStaticAnalyzer = void 0;
|
|
50816
|
+
const AbstractParseTreeVisitor_1 = __webpack_require__(/*! antlr4ts/tree/AbstractParseTreeVisitor */ "./node_modules/antlr4ts/tree/AbstractParseTreeVisitor.js");
|
|
50817
|
+
const ForgeParser_1 = __webpack_require__(/*! ./forge-antlr/ForgeParser */ "./src/forge-antlr/ForgeParser.ts");
|
|
50818
|
+
const utils_1 = __webpack_require__(/*! ./forge-antlr/utils */ "./src/forge-antlr/utils.ts");
|
|
50819
|
+
const UNKNOWN = { kind: "unknown" };
|
|
50820
|
+
// Helper that wraps the schema with O(1) lookups and lattice queries.
|
|
50821
|
+
class SchemaInfo {
|
|
50822
|
+
constructor(schema) {
|
|
50823
|
+
this.typesById = new Map(schema.getTypes().map((t) => [t.id, t]));
|
|
50824
|
+
this.relationsByName = new Map(schema.getRelations().map((r) => [r.name, r]));
|
|
50825
|
+
}
|
|
50826
|
+
getType(id) { return this.typesById.get(id); }
|
|
50827
|
+
getRelation(name) { return this.relationsByName.get(name); }
|
|
50828
|
+
// A ⊆ B when B is in A's lineage. `IType.types` is the lineage in ascending
|
|
50829
|
+
// order, conventionally starting with the type itself.
|
|
50830
|
+
isSubtypeOf(a, b) {
|
|
50831
|
+
if (a === b)
|
|
50832
|
+
return true;
|
|
50833
|
+
const t = this.typesById.get(a);
|
|
50834
|
+
if (!t)
|
|
50835
|
+
return false;
|
|
50836
|
+
return t.types.includes(b);
|
|
50837
|
+
}
|
|
50838
|
+
// Closed-world disjointness: A and B are disjoint iff no type in the
|
|
50839
|
+
// schema has both A and B in its lineage (i.e., no common subtype exists).
|
|
50840
|
+
// Sound only when the caller treats the supplied schema as complete.
|
|
50841
|
+
areDisjoint(a, b) {
|
|
50842
|
+
if (a === b)
|
|
50843
|
+
return false;
|
|
50844
|
+
for (const t of this.typesById.values()) {
|
|
50845
|
+
if (t.types.includes(a) && t.types.includes(b))
|
|
50846
|
+
return false;
|
|
50847
|
+
}
|
|
50848
|
+
return true;
|
|
50849
|
+
}
|
|
50850
|
+
}
|
|
50851
|
+
// ANTLR's .text concatenates child tokens without whitespace, which gives us
|
|
50852
|
+
// a usable canonical form for "syntactically the same subexpression".
|
|
50853
|
+
function sameSubtree(a, b) {
|
|
50854
|
+
if (!a || !b)
|
|
50855
|
+
return false;
|
|
50856
|
+
return a.text === b.text;
|
|
50857
|
+
}
|
|
50858
|
+
// True iff `ctx` is structurally a difference whose subtrahend (anywhere in a
|
|
50859
|
+
// left-associated minus chain) matches `targetText`. Strips parens and
|
|
50860
|
+
// pass-through wrappers. Examples that match for target="X":
|
|
50861
|
+
// X - X (Y - X) (Y - Z - X) ((Y - X) - Z)
|
|
50862
|
+
function subtractsTarget(ctx, targetText) {
|
|
50863
|
+
if (!ctx)
|
|
50864
|
+
return false;
|
|
50865
|
+
let cur = ctx;
|
|
50866
|
+
while (cur.childCount === 1)
|
|
50867
|
+
cur = cur.getChild(0);
|
|
50868
|
+
if (cur instanceof ForgeParser_1.Expr18Context && cur.LEFT_PAREN_TOK()) {
|
|
50869
|
+
return subtractsTarget(cur.expr(), targetText);
|
|
50870
|
+
}
|
|
50871
|
+
if (cur instanceof ForgeParser_1.Expr8Context && cur.MINUS_TOK()) {
|
|
50872
|
+
if (cur.expr10().text === targetText)
|
|
50873
|
+
return true;
|
|
50874
|
+
// Chain: `A - B - target` parses left-assoc as `((A - B) - target)`;
|
|
50875
|
+
// recurse on the left to catch deeper occurrences.
|
|
50876
|
+
return subtractsTarget(cur.expr8(), targetText);
|
|
50877
|
+
}
|
|
50878
|
+
return false;
|
|
50879
|
+
}
|
|
50880
|
+
// True iff `ctx` is structurally an intersection involving `targetText`
|
|
50881
|
+
// (so `ctx ⊆ target`). Walks through parens, pass-throughs, and chains of `&`.
|
|
50882
|
+
function intersectionInvolves(ctx, targetText) {
|
|
50883
|
+
if (!ctx)
|
|
50884
|
+
return false;
|
|
50885
|
+
let cur = ctx;
|
|
50886
|
+
while (cur.childCount === 1)
|
|
50887
|
+
cur = cur.getChild(0);
|
|
50888
|
+
if (cur instanceof ForgeParser_1.Expr18Context && cur.LEFT_PAREN_TOK()) {
|
|
50889
|
+
return intersectionInvolves(cur.expr(), targetText);
|
|
50890
|
+
}
|
|
50891
|
+
if (cur instanceof ForgeParser_1.Expr11Context && cur.AMP_TOK()) {
|
|
50892
|
+
if (cur.expr11().text === targetText)
|
|
50893
|
+
return true;
|
|
50894
|
+
if (cur.expr12().text === targetText)
|
|
50895
|
+
return true;
|
|
50896
|
+
return (intersectionInvolves(cur.expr11(), targetText) ||
|
|
50897
|
+
intersectionInvolves(cur.expr12(), targetText));
|
|
50898
|
+
}
|
|
50899
|
+
return false;
|
|
50900
|
+
}
|
|
50901
|
+
// True iff `ctx` is structurally a union containing `targetText`
|
|
50902
|
+
// (so `target ⊆ ctx`). Walks through parens, pass-throughs, and chains of `+`.
|
|
50903
|
+
function unionContains(ctx, targetText) {
|
|
50904
|
+
if (!ctx)
|
|
50905
|
+
return false;
|
|
50906
|
+
let cur = ctx;
|
|
50907
|
+
while (cur.childCount === 1)
|
|
50908
|
+
cur = cur.getChild(0);
|
|
50909
|
+
if (cur instanceof ForgeParser_1.Expr18Context && cur.LEFT_PAREN_TOK()) {
|
|
50910
|
+
return unionContains(cur.expr(), targetText);
|
|
50911
|
+
}
|
|
50912
|
+
if (cur instanceof ForgeParser_1.Expr8Context && cur.PLUS_TOK()) {
|
|
50913
|
+
if (cur.expr8().text === targetText)
|
|
50914
|
+
return true;
|
|
50915
|
+
if (cur.expr10().text === targetText)
|
|
50916
|
+
return true;
|
|
50917
|
+
return (unionContains(cur.expr8(), targetText) ||
|
|
50918
|
+
unionContains(cur.expr10(), targetText));
|
|
50919
|
+
}
|
|
50920
|
+
return false;
|
|
50921
|
+
}
|
|
50922
|
+
// Strip wrapping parens / pass-through expr layers to reveal an inner NEG_TOK
|
|
50923
|
+
// at the expr5 level. Returns the operand-text if `ctx` is structurally `not E`,
|
|
50924
|
+
// else undefined.
|
|
50925
|
+
function negationOperandText(ctx) {
|
|
50926
|
+
if (!ctx)
|
|
50927
|
+
return undefined;
|
|
50928
|
+
let cur = ctx;
|
|
50929
|
+
while (cur.childCount === 1) {
|
|
50930
|
+
cur = cur.getChild(0);
|
|
50931
|
+
}
|
|
50932
|
+
if (cur instanceof ForgeParser_1.Expr5Context && cur.NEG_TOK()) {
|
|
50933
|
+
const inner = cur.expr5();
|
|
50934
|
+
return inner ? inner.text : undefined;
|
|
50935
|
+
}
|
|
50936
|
+
if (cur instanceof ForgeParser_1.Expr18Context && cur.LEFT_PAREN_TOK()) {
|
|
50937
|
+
return negationOperandText(cur.expr());
|
|
50938
|
+
}
|
|
50939
|
+
return undefined;
|
|
50940
|
+
}
|
|
50941
|
+
class ForgeExprStaticAnalyzer extends AbstractParseTreeVisitor_1.AbstractParseTreeVisitor {
|
|
50942
|
+
constructor(schema) {
|
|
50943
|
+
super();
|
|
50944
|
+
if (schema)
|
|
50945
|
+
this.schema = new SchemaInfo(schema);
|
|
50946
|
+
}
|
|
50947
|
+
// Walk a nameList (`a, b, c`) and accumulate identifiers into `out`.
|
|
50948
|
+
collectNamesFromList(ctx, out) {
|
|
50949
|
+
out.add((0, utils_1.getIdentifierName)(ctx.name()));
|
|
50950
|
+
const tail = ctx.nameList();
|
|
50951
|
+
if (tail)
|
|
50952
|
+
this.collectNamesFromList(tail, out);
|
|
50953
|
+
}
|
|
50954
|
+
// Walk a quantDeclList (`a : S, b : T`) and accumulate all bound identifiers.
|
|
50955
|
+
collectBoundNames(ctx, out) {
|
|
50956
|
+
this.collectNamesFromList(ctx.quantDecl().nameList(), out);
|
|
50957
|
+
const tail = ctx.quantDeclList();
|
|
50958
|
+
if (tail)
|
|
50959
|
+
this.collectBoundNames(tail, out);
|
|
50960
|
+
}
|
|
50961
|
+
// Policy: when a schema is provided, type and relation names are reserved —
|
|
50962
|
+
// they may only refer to the schema entity. Returns an ill-typed Abstract if
|
|
50963
|
+
// any candidate name collides; undefined otherwise.
|
|
50964
|
+
illTypedIfBindsReservedName(names) {
|
|
50965
|
+
if (!this.schema)
|
|
50966
|
+
return undefined;
|
|
50967
|
+
for (const name of names) {
|
|
50968
|
+
if (this.schema.getType(name) || this.schema.getRelation(name)) {
|
|
50969
|
+
return {
|
|
50970
|
+
kind: "ill-typed",
|
|
50971
|
+
reason: `cannot bind variable named '${name}': it is a reserved schema ` +
|
|
50972
|
+
`${this.schema.getType(name) ? "type" : "relation"} name`,
|
|
50973
|
+
};
|
|
50974
|
+
}
|
|
50975
|
+
}
|
|
50976
|
+
return undefined;
|
|
50977
|
+
}
|
|
50978
|
+
// Public entry: convert internal lattice value to a verdict + reason.
|
|
50979
|
+
analyze(ctx) {
|
|
50980
|
+
const v = this.visit(ctx);
|
|
50981
|
+
if (v.kind === "bool") {
|
|
50982
|
+
return v.value
|
|
50983
|
+
? { status: "tautology", reason: "expression folds to literal true" }
|
|
50984
|
+
: { status: "unsat", reason: "expression folds to literal false" };
|
|
50985
|
+
}
|
|
50986
|
+
if (v.kind === "empty") {
|
|
50987
|
+
return { status: "empty", reason: "expression is provably the empty set" };
|
|
50988
|
+
}
|
|
50989
|
+
if (v.kind === "ill-typed") {
|
|
50990
|
+
return { status: "ill-typed", reason: v.reason };
|
|
50991
|
+
}
|
|
50992
|
+
return { status: "unknown" };
|
|
50993
|
+
}
|
|
50994
|
+
// Arity of an Abstract when known; -1 means "not determinable".
|
|
50995
|
+
static arityOf(v) {
|
|
50996
|
+
if (v.kind === "typed")
|
|
50997
|
+
return v.arity;
|
|
50998
|
+
if (v.kind === "bool" || v.kind === "num")
|
|
50999
|
+
return 1; // singleton sets
|
|
51000
|
+
if (v.kind === "empty")
|
|
51001
|
+
return -1; // empty has no committed arity
|
|
51002
|
+
return -1;
|
|
51003
|
+
}
|
|
51004
|
+
defaultResult() {
|
|
51005
|
+
return UNKNOWN;
|
|
51006
|
+
}
|
|
51007
|
+
// Return the first ill-typed value among the args, or undefined. Used by
|
|
51008
|
+
// every operator handler to surface a statically malformed sub-expression
|
|
51009
|
+
// before any fold (including short-circuits like `false AND ?` and `true OR
|
|
51010
|
+
// ?`) could mask it.
|
|
51011
|
+
static bailIfIllTyped(...vals) {
|
|
51012
|
+
for (const v of vals) {
|
|
51013
|
+
if (v.kind === "ill-typed")
|
|
51014
|
+
return v;
|
|
51015
|
+
}
|
|
51016
|
+
return undefined;
|
|
51017
|
+
}
|
|
51018
|
+
// Prefer the more specific child result when aggregating across siblings.
|
|
51019
|
+
// For pass-through expr layers there's exactly one meaningful child; for
|
|
51020
|
+
// wrapper nodes like `( expr )` the terminals contribute UNKNOWN and the
|
|
51021
|
+
// real value comes from the single non-terminal child. Ill-typed always
|
|
51022
|
+
// wins — if any subtree is statically malformed, the parent is too.
|
|
51023
|
+
aggregateResult(aggregate, nextResult) {
|
|
51024
|
+
if (aggregate.kind === "ill-typed")
|
|
51025
|
+
return aggregate;
|
|
51026
|
+
if (nextResult.kind === "ill-typed")
|
|
51027
|
+
return nextResult;
|
|
51028
|
+
if (aggregate.kind === "unknown")
|
|
51029
|
+
return nextResult;
|
|
51030
|
+
return aggregate;
|
|
51031
|
+
}
|
|
51032
|
+
// Let/bind introduce data-dependent bindings; we don't fold through them.
|
|
51033
|
+
// Quantifiers we *do* fold in the cases where the verdict is independent of
|
|
51034
|
+
// the binding: an empty quantified domain, or a body that's a literal.
|
|
51035
|
+
visitExpr(ctx) {
|
|
51036
|
+
if (ctx.LET_TOK() || ctx.BIND_TOK())
|
|
51037
|
+
return UNKNOWN;
|
|
51038
|
+
const quant = ctx.quant();
|
|
51039
|
+
if (quant)
|
|
51040
|
+
return this.foldQuantifier(ctx, quant);
|
|
51041
|
+
return this.visitChildren(ctx);
|
|
51042
|
+
}
|
|
51043
|
+
foldQuantifier(ctx, quant) {
|
|
51044
|
+
const blockOrBar = ctx.blockOrBar();
|
|
51045
|
+
if (!blockOrBar)
|
|
51046
|
+
return UNKNOWN;
|
|
51047
|
+
// Reserved-name check at the binder: under a schema, type/relation names
|
|
51048
|
+
// cannot be reused as quantified variables.
|
|
51049
|
+
const declListTop = ctx.quantDeclList();
|
|
51050
|
+
if (declListTop) {
|
|
51051
|
+
const boundHere = new Set();
|
|
51052
|
+
this.collectBoundNames(declListTop, boundHere);
|
|
51053
|
+
const reservedError = this.illTypedIfBindsReservedName(boundHere);
|
|
51054
|
+
if (reservedError)
|
|
51055
|
+
return reservedError;
|
|
51056
|
+
}
|
|
51057
|
+
// Determine if any quantified domain is statically empty.
|
|
51058
|
+
let domainEmpty = false;
|
|
51059
|
+
let declList = ctx.quantDeclList();
|
|
51060
|
+
while (declList) {
|
|
51061
|
+
const setExpr = declList.quantDecl().expr();
|
|
51062
|
+
const v = this.visit(setExpr);
|
|
51063
|
+
if (v.kind === "ill-typed")
|
|
51064
|
+
return v;
|
|
51065
|
+
if (v.kind === "empty") {
|
|
51066
|
+
domainEmpty = true;
|
|
51067
|
+
break;
|
|
51068
|
+
}
|
|
51069
|
+
const next = declList.quantDeclList();
|
|
51070
|
+
if (!next)
|
|
51071
|
+
break;
|
|
51072
|
+
declList = next;
|
|
51073
|
+
}
|
|
51074
|
+
// Body analysis (only attempt the `| expr` form; blocks are conjunctions
|
|
51075
|
+
// and would need the binding to be meaningful).
|
|
51076
|
+
let body = UNKNOWN;
|
|
51077
|
+
if (blockOrBar.BAR_TOK() && blockOrBar.expr()) {
|
|
51078
|
+
body = this.visit(blockOrBar.expr());
|
|
51079
|
+
}
|
|
51080
|
+
const mult = quant.mult();
|
|
51081
|
+
const isAll = quant.ALL_TOK() !== undefined;
|
|
51082
|
+
const isNo = quant.NO_TOK() !== undefined;
|
|
51083
|
+
const isSome = mult?.SOME_TOK() !== undefined;
|
|
51084
|
+
const isOne = mult?.ONE_TOK() !== undefined;
|
|
51085
|
+
const isTwo = mult?.TWO_TOK() !== undefined;
|
|
51086
|
+
const isLone = mult?.LONE_TOK() !== undefined;
|
|
51087
|
+
if (domainEmpty) {
|
|
51088
|
+
// ∀ over empty is vacuously true; ∃ over empty is false.
|
|
51089
|
+
if (isAll || isNo || isLone)
|
|
51090
|
+
return { kind: "bool", value: true };
|
|
51091
|
+
if (isSome || isOne || isTwo)
|
|
51092
|
+
return { kind: "bool", value: false };
|
|
51093
|
+
}
|
|
51094
|
+
if (body.kind === "bool") {
|
|
51095
|
+
if (!body.value) {
|
|
51096
|
+
// body always false: no x satisfies, regardless of domain
|
|
51097
|
+
if (isNo || isLone)
|
|
51098
|
+
return { kind: "bool", value: true };
|
|
51099
|
+
if (isSome || isOne || isTwo)
|
|
51100
|
+
return { kind: "bool", value: false };
|
|
51101
|
+
// all x | false → data-dependent (vacuous if domain empty, else false)
|
|
51102
|
+
}
|
|
51103
|
+
else {
|
|
51104
|
+
// body always true: every x satisfies
|
|
51105
|
+
if (isAll)
|
|
51106
|
+
return { kind: "bool", value: true };
|
|
51107
|
+
// some/one/no etc. depend on domain size — data-dependent
|
|
51108
|
+
}
|
|
51109
|
+
}
|
|
51110
|
+
return UNKNOWN;
|
|
51111
|
+
}
|
|
51112
|
+
// A block { e1; e2; ...; en } is a conjunction of its expressions.
|
|
51113
|
+
visitBlock(ctx) {
|
|
51114
|
+
let result = { kind: "bool", value: true };
|
|
51115
|
+
for (const e of ctx.expr()) {
|
|
51116
|
+
const v = this.visit(e);
|
|
51117
|
+
if (v.kind === "ill-typed")
|
|
51118
|
+
return v; // ill-typed wins
|
|
51119
|
+
if (v.kind === "bool" && !v.value)
|
|
51120
|
+
return v; // any false → false
|
|
51121
|
+
if (v.kind !== "bool")
|
|
51122
|
+
result = UNKNOWN; // forget the running true
|
|
51123
|
+
}
|
|
51124
|
+
return result;
|
|
51125
|
+
}
|
|
51126
|
+
visitExpr1(ctx) {
|
|
51127
|
+
if (ctx.OR_TOK()) {
|
|
51128
|
+
const leftCtx = ctx.expr1();
|
|
51129
|
+
const rightCtx = ctx.expr1_5();
|
|
51130
|
+
const l = this.visit(leftCtx);
|
|
51131
|
+
const r = this.visit(rightCtx);
|
|
51132
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51133
|
+
if (bail)
|
|
51134
|
+
return bail;
|
|
51135
|
+
// X or X → X
|
|
51136
|
+
if (sameSubtree(leftCtx, rightCtx))
|
|
51137
|
+
return l;
|
|
51138
|
+
if (l.kind === "bool" && l.value)
|
|
51139
|
+
return l;
|
|
51140
|
+
if (r.kind === "bool" && r.value)
|
|
51141
|
+
return r;
|
|
51142
|
+
if (l.kind === "bool" && r.kind === "bool") {
|
|
51143
|
+
return { kind: "bool", value: l.value || r.value };
|
|
51144
|
+
}
|
|
51145
|
+
// X or not X / not X or X → true (classical tautology)
|
|
51146
|
+
const lNeg = negationOperandText(leftCtx);
|
|
51147
|
+
const rNeg = negationOperandText(rightCtx);
|
|
51148
|
+
if (lNeg !== undefined && lNeg === rightCtx.text)
|
|
51149
|
+
return { kind: "bool", value: true };
|
|
51150
|
+
if (rNeg !== undefined && rNeg === leftCtx.text)
|
|
51151
|
+
return { kind: "bool", value: true };
|
|
51152
|
+
return UNKNOWN;
|
|
51153
|
+
}
|
|
51154
|
+
return this.visitChildren(ctx);
|
|
51155
|
+
}
|
|
51156
|
+
visitExpr1_5(ctx) {
|
|
51157
|
+
if (ctx.XOR_TOK()) {
|
|
51158
|
+
const leftCtx = ctx.expr1_5();
|
|
51159
|
+
const rightCtx = ctx.expr2();
|
|
51160
|
+
const l = this.visit(leftCtx);
|
|
51161
|
+
const r = this.visit(rightCtx);
|
|
51162
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51163
|
+
if (bail)
|
|
51164
|
+
return bail;
|
|
51165
|
+
// X xor X → false
|
|
51166
|
+
if (sameSubtree(leftCtx, rightCtx))
|
|
51167
|
+
return { kind: "bool", value: false };
|
|
51168
|
+
if (l.kind === "bool" && r.kind === "bool") {
|
|
51169
|
+
return { kind: "bool", value: l.value !== r.value };
|
|
51170
|
+
}
|
|
51171
|
+
return UNKNOWN;
|
|
51172
|
+
}
|
|
51173
|
+
return this.visitChildren(ctx);
|
|
51174
|
+
}
|
|
51175
|
+
visitExpr2(ctx) {
|
|
51176
|
+
if (ctx.IFF_TOK()) {
|
|
51177
|
+
const leftCtx = ctx.expr2();
|
|
51178
|
+
const rightCtx = ctx.expr3();
|
|
51179
|
+
const l = this.visit(leftCtx);
|
|
51180
|
+
const r = this.visit(rightCtx);
|
|
51181
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51182
|
+
if (bail)
|
|
51183
|
+
return bail;
|
|
51184
|
+
// X iff X → true
|
|
51185
|
+
if (sameSubtree(leftCtx, rightCtx))
|
|
51186
|
+
return { kind: "bool", value: true };
|
|
51187
|
+
// X iff not X / not X iff X → false
|
|
51188
|
+
const lNeg = negationOperandText(leftCtx);
|
|
51189
|
+
const rNeg = negationOperandText(rightCtx);
|
|
51190
|
+
if (lNeg !== undefined && lNeg === rightCtx.text)
|
|
51191
|
+
return { kind: "bool", value: false };
|
|
51192
|
+
if (rNeg !== undefined && rNeg === leftCtx.text)
|
|
51193
|
+
return { kind: "bool", value: false };
|
|
51194
|
+
if (l.kind === "bool" && r.kind === "bool") {
|
|
51195
|
+
return { kind: "bool", value: l.value === r.value };
|
|
51196
|
+
}
|
|
51197
|
+
return UNKNOWN;
|
|
51198
|
+
}
|
|
51199
|
+
return this.visitChildren(ctx);
|
|
51200
|
+
}
|
|
51201
|
+
visitExpr3(ctx) {
|
|
51202
|
+
if (ctx.IMP_TOK()) {
|
|
51203
|
+
const ant = this.visit(ctx.expr4());
|
|
51204
|
+
const exprs3 = ctx.expr3();
|
|
51205
|
+
// Implication or implication-with-else. Visit all branches up front so
|
|
51206
|
+
// an ill-typed sub-expression is reported even when a short-circuit
|
|
51207
|
+
// (false antecedent, etc.) would otherwise fold the verdict.
|
|
51208
|
+
if (ctx.ELSE_TOK()) {
|
|
51209
|
+
const thenBranch = this.visit(exprs3[0]);
|
|
51210
|
+
const elseBranch = this.visit(exprs3[1]);
|
|
51211
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(ant, thenBranch, elseBranch);
|
|
51212
|
+
if (bail)
|
|
51213
|
+
return bail;
|
|
51214
|
+
if (ant.kind === "bool")
|
|
51215
|
+
return ant.value ? thenBranch : elseBranch;
|
|
51216
|
+
return UNKNOWN;
|
|
51217
|
+
}
|
|
51218
|
+
const conseqCtx = exprs3[0];
|
|
51219
|
+
const con = this.visit(conseqCtx);
|
|
51220
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(ant, con);
|
|
51221
|
+
if (bail)
|
|
51222
|
+
return bail;
|
|
51223
|
+
// antecedent false → vacuously true
|
|
51224
|
+
if (ant.kind === "bool" && !ant.value)
|
|
51225
|
+
return { kind: "bool", value: true };
|
|
51226
|
+
// true => X ≡ X
|
|
51227
|
+
if (ant.kind === "bool" && ant.value)
|
|
51228
|
+
return con;
|
|
51229
|
+
// _ => true ≡ true
|
|
51230
|
+
if (con.kind === "bool" && con.value)
|
|
51231
|
+
return con;
|
|
51232
|
+
return UNKNOWN;
|
|
51233
|
+
}
|
|
51234
|
+
return this.visitChildren(ctx);
|
|
51235
|
+
}
|
|
51236
|
+
visitExpr4(ctx) {
|
|
51237
|
+
if (ctx.AND_TOK()) {
|
|
51238
|
+
const leftCtx = ctx.expr4();
|
|
51239
|
+
const rightCtx = ctx.expr4_5();
|
|
51240
|
+
const l = this.visit(leftCtx);
|
|
51241
|
+
const r = this.visit(rightCtx);
|
|
51242
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51243
|
+
if (bail)
|
|
51244
|
+
return bail;
|
|
51245
|
+
// X and X → X
|
|
51246
|
+
if (sameSubtree(leftCtx, rightCtx))
|
|
51247
|
+
return l;
|
|
51248
|
+
if (l.kind === "bool" && !l.value)
|
|
51249
|
+
return l;
|
|
51250
|
+
if (r.kind === "bool" && !r.value)
|
|
51251
|
+
return r;
|
|
51252
|
+
if (l.kind === "bool" && r.kind === "bool") {
|
|
51253
|
+
return { kind: "bool", value: l.value && r.value };
|
|
51254
|
+
}
|
|
51255
|
+
// X and not X / not X and X → false
|
|
51256
|
+
const lNeg = negationOperandText(leftCtx);
|
|
51257
|
+
const rNeg = negationOperandText(rightCtx);
|
|
51258
|
+
if (lNeg !== undefined && lNeg === rightCtx.text)
|
|
51259
|
+
return { kind: "bool", value: false };
|
|
51260
|
+
if (rNeg !== undefined && rNeg === leftCtx.text)
|
|
51261
|
+
return { kind: "bool", value: false };
|
|
51262
|
+
return UNKNOWN;
|
|
51263
|
+
}
|
|
51264
|
+
return this.visitChildren(ctx);
|
|
51265
|
+
}
|
|
51266
|
+
visitExpr5(ctx) {
|
|
51267
|
+
if (ctx.NEG_TOK()) {
|
|
51268
|
+
const inner = this.visit(ctx.expr5());
|
|
51269
|
+
if (inner.kind === "ill-typed")
|
|
51270
|
+
return inner;
|
|
51271
|
+
if (inner.kind === "bool")
|
|
51272
|
+
return { kind: "bool", value: !inner.value };
|
|
51273
|
+
return UNKNOWN;
|
|
51274
|
+
}
|
|
51275
|
+
// Temporal operators are not implemented; bail out conservatively.
|
|
51276
|
+
if (ctx.ALWAYS_TOK() ||
|
|
51277
|
+
ctx.EVENTUALLY_TOK() ||
|
|
51278
|
+
ctx.AFTER_TOK() ||
|
|
51279
|
+
ctx.BEFORE_TOK() ||
|
|
51280
|
+
ctx.ONCE_TOK() ||
|
|
51281
|
+
ctx.HISTORICALLY_TOK()) {
|
|
51282
|
+
return UNKNOWN;
|
|
51283
|
+
}
|
|
51284
|
+
return this.visitChildren(ctx);
|
|
51285
|
+
}
|
|
51286
|
+
visitExpr6(ctx) {
|
|
51287
|
+
const op = ctx.compareOp();
|
|
51288
|
+
if (!op)
|
|
51289
|
+
return this.visitChildren(ctx);
|
|
51290
|
+
const leftCtx = ctx.expr6();
|
|
51291
|
+
const rightCtx = ctx.expr7();
|
|
51292
|
+
const l = this.visit(leftCtx);
|
|
51293
|
+
const r = this.visit(rightCtx);
|
|
51294
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51295
|
+
if (bail)
|
|
51296
|
+
return bail;
|
|
51297
|
+
const negate = ctx.NEG_TOK() !== undefined;
|
|
51298
|
+
const opText = op.text;
|
|
51299
|
+
// `ni` is non-membership (the codebase's evaluator implements it as
|
|
51300
|
+
// !(in)). We fold it by computing the corresponding `in` verdict and
|
|
51301
|
+
// letting `finalize` invert. This keeps the static verdict in agreement
|
|
51302
|
+
// with runtime semantics for expressions like `X ni X` (false) and
|
|
51303
|
+
// `none ni 1` (true).
|
|
51304
|
+
const isNi = opText === "ni";
|
|
51305
|
+
const opForLogic = isNi ? "in" : opText;
|
|
51306
|
+
const finalize = (value) => {
|
|
51307
|
+
let v = value;
|
|
51308
|
+
if (isNi)
|
|
51309
|
+
v = !v;
|
|
51310
|
+
if (negate)
|
|
51311
|
+
v = !v;
|
|
51312
|
+
return { kind: "bool", value: v };
|
|
51313
|
+
};
|
|
51314
|
+
// Same-subtree shortcuts hold regardless of unknown values.
|
|
51315
|
+
if (sameSubtree(leftCtx, rightCtx)) {
|
|
51316
|
+
switch (opForLogic) {
|
|
51317
|
+
case "=":
|
|
51318
|
+
case "<=":
|
|
51319
|
+
case ">=":
|
|
51320
|
+
case "in":
|
|
51321
|
+
return finalize(true);
|
|
51322
|
+
case "<":
|
|
51323
|
+
case ">":
|
|
51324
|
+
return finalize(false);
|
|
51325
|
+
}
|
|
51326
|
+
}
|
|
51327
|
+
// Numeric literal folding.
|
|
51328
|
+
if (l.kind === "num" && r.kind === "num") {
|
|
51329
|
+
switch (opForLogic) {
|
|
51330
|
+
case "=":
|
|
51331
|
+
return finalize(l.value === r.value);
|
|
51332
|
+
case "<":
|
|
51333
|
+
return finalize(l.value < r.value);
|
|
51334
|
+
case ">":
|
|
51335
|
+
return finalize(l.value > r.value);
|
|
51336
|
+
case "<=":
|
|
51337
|
+
return finalize(l.value <= r.value);
|
|
51338
|
+
case ">=":
|
|
51339
|
+
return finalize(l.value >= r.value);
|
|
51340
|
+
}
|
|
51341
|
+
}
|
|
51342
|
+
// Boolean literal equality.
|
|
51343
|
+
if (l.kind === "bool" && r.kind === "bool" && opForLogic === "=") {
|
|
51344
|
+
return finalize(l.value === r.value);
|
|
51345
|
+
}
|
|
51346
|
+
// Empty / singleton comparisons.
|
|
51347
|
+
const lIsKnownSingleton = l.kind === "num" || l.kind === "bool";
|
|
51348
|
+
const rIsKnownSingleton = r.kind === "num" || r.kind === "bool";
|
|
51349
|
+
if (opForLogic === "=") {
|
|
51350
|
+
if (l.kind === "empty" && r.kind === "empty")
|
|
51351
|
+
return finalize(true);
|
|
51352
|
+
if (l.kind === "empty" && rIsKnownSingleton)
|
|
51353
|
+
return finalize(false);
|
|
51354
|
+
if (lIsKnownSingleton && r.kind === "empty")
|
|
51355
|
+
return finalize(false);
|
|
51356
|
+
}
|
|
51357
|
+
if (opForLogic === "in") {
|
|
51358
|
+
// empty set is a subset of every set
|
|
51359
|
+
if (l.kind === "empty")
|
|
51360
|
+
return finalize(true);
|
|
51361
|
+
// a non-empty singleton cannot be contained in the empty set
|
|
51362
|
+
if (lIsKnownSingleton && r.kind === "empty")
|
|
51363
|
+
return finalize(false);
|
|
51364
|
+
}
|
|
51365
|
+
// Arity mismatch on arity-sensitive comparisons → ill-typed.
|
|
51366
|
+
if (opText === "=" || opText === "in" || opText === "ni") {
|
|
51367
|
+
const la = ForgeExprStaticAnalyzer.arityOf(l);
|
|
51368
|
+
const ra = ForgeExprStaticAnalyzer.arityOf(r);
|
|
51369
|
+
if (la > 0 && ra > 0 && la !== ra) {
|
|
51370
|
+
return {
|
|
51371
|
+
kind: "ill-typed",
|
|
51372
|
+
reason: `arity mismatch in '${opText}': left has arity ${la}, right has arity ${ra}`,
|
|
51373
|
+
};
|
|
51374
|
+
}
|
|
51375
|
+
}
|
|
51376
|
+
// Schema-driven: subtype tautologies for `in` (and `ni` via finalize
|
|
51377
|
+
// inversion when A ⊆ B → A in B is true → A ni B is false). We
|
|
51378
|
+
// deliberately do NOT positively fold `ni` from the lattice — concluding
|
|
51379
|
+
// "not (A ⊆ B)" requires disjointness reasoning we don't perform here.
|
|
51380
|
+
if (this.schema && l.kind === "typed" && r.kind === "typed") {
|
|
51381
|
+
const lCols = l.columnTypes;
|
|
51382
|
+
const rCols = r.columnTypes;
|
|
51383
|
+
if (lCols && rCols && lCols.length === rCols.length) {
|
|
51384
|
+
const colWiseSubtype = lCols.every((lc, i) => this.schema.isSubtypeOf(lc, rCols[i]));
|
|
51385
|
+
if (opForLogic === "in" && colWiseSubtype)
|
|
51386
|
+
return finalize(true);
|
|
51387
|
+
}
|
|
51388
|
+
}
|
|
51389
|
+
return UNKNOWN;
|
|
51390
|
+
}
|
|
51391
|
+
visitExpr7(ctx) {
|
|
51392
|
+
const inner = this.visit(ctx.expr8());
|
|
51393
|
+
if (inner.kind === "ill-typed")
|
|
51394
|
+
return inner;
|
|
51395
|
+
if (ctx.SET_TOK())
|
|
51396
|
+
return inner;
|
|
51397
|
+
if (inner.kind === "empty") {
|
|
51398
|
+
if (ctx.NO_TOK())
|
|
51399
|
+
return { kind: "bool", value: true };
|
|
51400
|
+
if (ctx.LONE_TOK())
|
|
51401
|
+
return { kind: "bool", value: true };
|
|
51402
|
+
if (ctx.SOME_TOK())
|
|
51403
|
+
return { kind: "bool", value: false };
|
|
51404
|
+
if (ctx.ONE_TOK())
|
|
51405
|
+
return { kind: "bool", value: false };
|
|
51406
|
+
if (ctx.TWO_TOK())
|
|
51407
|
+
return { kind: "bool", value: false };
|
|
51408
|
+
}
|
|
51409
|
+
// If we can't say anything specific about the multiplicity, the whole
|
|
51410
|
+
// expression is still a boolean — but its value is unknown.
|
|
51411
|
+
if (ctx.NO_TOK() ||
|
|
51412
|
+
ctx.LONE_TOK() ||
|
|
51413
|
+
ctx.SOME_TOK() ||
|
|
51414
|
+
ctx.ONE_TOK() ||
|
|
51415
|
+
ctx.TWO_TOK()) {
|
|
51416
|
+
return UNKNOWN;
|
|
51417
|
+
}
|
|
51418
|
+
return inner;
|
|
51419
|
+
}
|
|
51420
|
+
visitExpr8(ctx) {
|
|
51421
|
+
if (ctx.MINUS_TOK()) {
|
|
51422
|
+
const leftCtx = ctx.expr8();
|
|
51423
|
+
const rightCtx = ctx.expr10();
|
|
51424
|
+
const l = this.visit(leftCtx);
|
|
51425
|
+
const r = this.visit(rightCtx);
|
|
51426
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51427
|
+
if (bail)
|
|
51428
|
+
return bail;
|
|
51429
|
+
// X - X → empty (set difference) or 0 (numeric)
|
|
51430
|
+
if (sameSubtree(leftCtx, rightCtx)) {
|
|
51431
|
+
if (l.kind === "num")
|
|
51432
|
+
return { kind: "num", value: 0 };
|
|
51433
|
+
return { kind: "empty" };
|
|
51434
|
+
}
|
|
51435
|
+
// Numeric folding for literal subtraction.
|
|
51436
|
+
if (l.kind === "num" && r.kind === "num") {
|
|
51437
|
+
return { kind: "num", value: l.value - r.value };
|
|
51438
|
+
}
|
|
51439
|
+
// empty - X → empty; X - empty → X
|
|
51440
|
+
if (l.kind === "empty")
|
|
51441
|
+
return { kind: "empty" };
|
|
51442
|
+
if (r.kind === "empty")
|
|
51443
|
+
return l;
|
|
51444
|
+
// Subset patterns: L ⊆ R ⇒ L - R = empty.
|
|
51445
|
+
// (R & ?) - R or (? & R) - R (intersection is a subset)
|
|
51446
|
+
if (intersectionInvolves(leftCtx, rightCtx.text))
|
|
51447
|
+
return { kind: "empty" };
|
|
51448
|
+
// L - (L + ?) or L - (? + L) (L is a subset of the union)
|
|
51449
|
+
if (unionContains(rightCtx, leftCtx.text))
|
|
51450
|
+
return { kind: "empty" };
|
|
51451
|
+
// Arity mismatch is a static type error.
|
|
51452
|
+
const lArity = ForgeExprStaticAnalyzer.arityOf(l);
|
|
51453
|
+
const rArity = ForgeExprStaticAnalyzer.arityOf(r);
|
|
51454
|
+
if (lArity > 0 && rArity > 0 && lArity !== rArity) {
|
|
51455
|
+
return {
|
|
51456
|
+
kind: "ill-typed",
|
|
51457
|
+
reason: `arity mismatch in '-': left has arity ${lArity}, right has arity ${rArity}`,
|
|
51458
|
+
};
|
|
51459
|
+
}
|
|
51460
|
+
return UNKNOWN;
|
|
51461
|
+
}
|
|
51462
|
+
if (ctx.PLUS_TOK()) {
|
|
51463
|
+
const l = this.visit(ctx.expr8());
|
|
51464
|
+
const r = this.visit(ctx.expr10());
|
|
51465
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51466
|
+
if (bail)
|
|
51467
|
+
return bail;
|
|
51468
|
+
// Numeric literal addition.
|
|
51469
|
+
if (l.kind === "num" && r.kind === "num") {
|
|
51470
|
+
return { kind: "num", value: l.value + r.value };
|
|
51471
|
+
}
|
|
51472
|
+
// Empty is the identity of set union: X + none ≡ X, none + X ≡ X.
|
|
51473
|
+
if (l.kind === "empty" && r.kind === "empty")
|
|
51474
|
+
return { kind: "empty" };
|
|
51475
|
+
if (l.kind === "empty")
|
|
51476
|
+
return r;
|
|
51477
|
+
if (r.kind === "empty")
|
|
51478
|
+
return l;
|
|
51479
|
+
// Arity mismatch is a static type error.
|
|
51480
|
+
const lArity = ForgeExprStaticAnalyzer.arityOf(l);
|
|
51481
|
+
const rArity = ForgeExprStaticAnalyzer.arityOf(r);
|
|
51482
|
+
if (lArity > 0 && rArity > 0 && lArity !== rArity) {
|
|
51483
|
+
return {
|
|
51484
|
+
kind: "ill-typed",
|
|
51485
|
+
reason: `arity mismatch in '+': left has arity ${lArity}, right has arity ${rArity}`,
|
|
51486
|
+
};
|
|
51487
|
+
}
|
|
51488
|
+
if (lArity > 0 && rArity > 0) {
|
|
51489
|
+
return { kind: "typed", arity: lArity };
|
|
51490
|
+
}
|
|
51491
|
+
return UNKNOWN;
|
|
51492
|
+
}
|
|
51493
|
+
return this.visitChildren(ctx);
|
|
51494
|
+
}
|
|
51495
|
+
visitExpr9(ctx) {
|
|
51496
|
+
if (ctx.CARD_TOK()) {
|
|
51497
|
+
const inner = this.visit(ctx.expr9());
|
|
51498
|
+
if (inner.kind === "ill-typed")
|
|
51499
|
+
return inner;
|
|
51500
|
+
if (inner.kind === "empty")
|
|
51501
|
+
return { kind: "num", value: 0 };
|
|
51502
|
+
return UNKNOWN;
|
|
51503
|
+
}
|
|
51504
|
+
return this.visitChildren(ctx);
|
|
51505
|
+
}
|
|
51506
|
+
visitExpr12(ctx) {
|
|
51507
|
+
if (ctx.arrowOp()) {
|
|
51508
|
+
// Cartesian product: if either side is empty, the product is empty.
|
|
51509
|
+
const l = this.visit(ctx.expr12());
|
|
51510
|
+
const r = this.visit(ctx.expr13());
|
|
51511
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51512
|
+
if (bail)
|
|
51513
|
+
return bail;
|
|
51514
|
+
if (l.kind === "empty" || r.kind === "empty")
|
|
51515
|
+
return { kind: "empty" };
|
|
51516
|
+
// Propagate combined arity / column types.
|
|
51517
|
+
if (l.kind === "typed" && r.kind === "typed") {
|
|
51518
|
+
const cols = l.columnTypes && r.columnTypes
|
|
51519
|
+
? [...l.columnTypes, ...r.columnTypes]
|
|
51520
|
+
: undefined;
|
|
51521
|
+
return { kind: "typed", arity: l.arity + r.arity, columnTypes: cols };
|
|
51522
|
+
}
|
|
51523
|
+
const la = ForgeExprStaticAnalyzer.arityOf(l);
|
|
51524
|
+
const ra = ForgeExprStaticAnalyzer.arityOf(r);
|
|
51525
|
+
if (la > 0 && ra > 0)
|
|
51526
|
+
return { kind: "typed", arity: la + ra };
|
|
51527
|
+
return UNKNOWN;
|
|
51528
|
+
}
|
|
51529
|
+
return this.visitChildren(ctx);
|
|
51530
|
+
}
|
|
51531
|
+
visitExpr11(ctx) {
|
|
51532
|
+
if (ctx.AMP_TOK()) {
|
|
51533
|
+
const leftCtx = ctx.expr11();
|
|
51534
|
+
const rightCtx = ctx.expr12();
|
|
51535
|
+
const l = this.visit(leftCtx);
|
|
51536
|
+
const r = this.visit(rightCtx);
|
|
51537
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51538
|
+
if (bail)
|
|
51539
|
+
return bail;
|
|
51540
|
+
// X & X → X (preserve "empty" if we know it; otherwise unknown).
|
|
51541
|
+
if (sameSubtree(leftCtx, rightCtx))
|
|
51542
|
+
return l;
|
|
51543
|
+
// empty & anything → empty
|
|
51544
|
+
if (l.kind === "empty" || r.kind === "empty")
|
|
51545
|
+
return { kind: "empty" };
|
|
51546
|
+
// Distinct numeric singletons are disjoint sets.
|
|
51547
|
+
if (l.kind === "num" && r.kind === "num" && l.value !== r.value) {
|
|
51548
|
+
return { kind: "empty" };
|
|
51549
|
+
}
|
|
51550
|
+
// X & (... - X) → empty (subtrahend strips X out).
|
|
51551
|
+
if (subtractsTarget(rightCtx, leftCtx.text))
|
|
51552
|
+
return { kind: "empty" };
|
|
51553
|
+
if (subtractsTarget(leftCtx, rightCtx.text))
|
|
51554
|
+
return { kind: "empty" };
|
|
51555
|
+
// Arity mismatch → ill-typed.
|
|
51556
|
+
const la = ForgeExprStaticAnalyzer.arityOf(l);
|
|
51557
|
+
const ra = ForgeExprStaticAnalyzer.arityOf(r);
|
|
51558
|
+
if (la > 0 && ra > 0 && la !== ra) {
|
|
51559
|
+
return {
|
|
51560
|
+
kind: "ill-typed",
|
|
51561
|
+
reason: `arity mismatch in '&': left has arity ${la}, right has arity ${ra}`,
|
|
51562
|
+
};
|
|
51563
|
+
}
|
|
51564
|
+
// Schema-driven column-wise disjointness.
|
|
51565
|
+
if (this.schema && l.kind === "typed" && r.kind === "typed" && l.columnTypes && r.columnTypes) {
|
|
51566
|
+
if (l.columnTypes.length === r.columnTypes.length) {
|
|
51567
|
+
const anyDisjoint = l.columnTypes.some((lc, i) => this.schema.areDisjoint(lc, r.columnTypes[i]));
|
|
51568
|
+
if (anyDisjoint)
|
|
51569
|
+
return { kind: "empty" };
|
|
51570
|
+
}
|
|
51571
|
+
}
|
|
51572
|
+
return UNKNOWN;
|
|
51573
|
+
}
|
|
51574
|
+
return this.visitChildren(ctx);
|
|
51575
|
+
}
|
|
51576
|
+
visitExpr14(ctx) {
|
|
51577
|
+
if (ctx.LEFT_SQUARE_TOK()) {
|
|
51578
|
+
// Box join f[a, b, ...] ≡ ...b.a.f
|
|
51579
|
+
const fn = this.visit(ctx.expr14());
|
|
51580
|
+
if (fn.kind === "ill-typed")
|
|
51581
|
+
return fn;
|
|
51582
|
+
if (fn.kind === "empty")
|
|
51583
|
+
return { kind: "empty" };
|
|
51584
|
+
// walk the comma-separated argument list
|
|
51585
|
+
let list = ctx.exprList();
|
|
51586
|
+
while (list) {
|
|
51587
|
+
const arg = this.visit(list.expr());
|
|
51588
|
+
if (arg.kind === "ill-typed")
|
|
51589
|
+
return arg;
|
|
51590
|
+
if (arg.kind === "empty")
|
|
51591
|
+
return { kind: "empty" };
|
|
51592
|
+
list = list.exprList();
|
|
51593
|
+
}
|
|
51594
|
+
return UNKNOWN;
|
|
51595
|
+
}
|
|
51596
|
+
return this.visitChildren(ctx);
|
|
51597
|
+
}
|
|
51598
|
+
visitExpr15(ctx) {
|
|
51599
|
+
if (ctx.DOT_TOK()) {
|
|
51600
|
+
const l = this.visit(ctx.expr15());
|
|
51601
|
+
const r = this.visit(ctx.expr16());
|
|
51602
|
+
const bail = ForgeExprStaticAnalyzer.bailIfIllTyped(l, r);
|
|
51603
|
+
if (bail)
|
|
51604
|
+
return bail;
|
|
51605
|
+
if (l.kind === "empty" || r.kind === "empty")
|
|
51606
|
+
return { kind: "empty" };
|
|
51607
|
+
// Joining two singletons / unary expressions yields arity 0 — ill-typed.
|
|
51608
|
+
const la = ForgeExprStaticAnalyzer.arityOf(l);
|
|
51609
|
+
const ra = ForgeExprStaticAnalyzer.arityOf(r);
|
|
51610
|
+
if (la === 1 && ra === 1) {
|
|
51611
|
+
return {
|
|
51612
|
+
kind: "ill-typed",
|
|
51613
|
+
reason: "dot join of two unary expressions produces arity 0",
|
|
51614
|
+
};
|
|
51615
|
+
}
|
|
51616
|
+
// Schema-aware column-type check on the join boundary.
|
|
51617
|
+
if (this.schema &&
|
|
51618
|
+
l.kind === "typed" && r.kind === "typed" &&
|
|
51619
|
+
l.columnTypes && r.columnTypes &&
|
|
51620
|
+
l.columnTypes.length > 0 && r.columnTypes.length > 0) {
|
|
51621
|
+
const leftLast = l.columnTypes[l.columnTypes.length - 1];
|
|
51622
|
+
const rightFirst = r.columnTypes[0];
|
|
51623
|
+
if (this.schema.areDisjoint(leftLast, rightFirst)) {
|
|
51624
|
+
return { kind: "empty" };
|
|
51625
|
+
}
|
|
51626
|
+
// Propagate typed-ness through the join when possible.
|
|
51627
|
+
const newCols = [
|
|
51628
|
+
...l.columnTypes.slice(0, -1),
|
|
51629
|
+
...r.columnTypes.slice(1),
|
|
51630
|
+
];
|
|
51631
|
+
if (newCols.length > 0) {
|
|
51632
|
+
return { kind: "typed", arity: newCols.length, columnTypes: newCols };
|
|
51633
|
+
}
|
|
51634
|
+
}
|
|
51635
|
+
// Otherwise propagate arity if we have it on both sides.
|
|
51636
|
+
if (la > 0 && ra > 0) {
|
|
51637
|
+
return { kind: "typed", arity: la + ra - 2 };
|
|
51638
|
+
}
|
|
51639
|
+
return UNKNOWN;
|
|
51640
|
+
}
|
|
51641
|
+
// name LEFT_SQUARE_TOK exprList RIGHT_SQUARE_TOK is a named pred call —
|
|
51642
|
+
// body unknown, so we can't fold even if args are empty.
|
|
51643
|
+
if (ctx.LEFT_SQUARE_TOK())
|
|
51644
|
+
return UNKNOWN;
|
|
51645
|
+
return this.visitChildren(ctx);
|
|
51646
|
+
}
|
|
51647
|
+
visitExpr17(ctx) {
|
|
51648
|
+
if (ctx.TILDE_TOK() || ctx.EXP_TOK()) {
|
|
51649
|
+
// transpose ~X and transitive closure ^X both preserve emptiness
|
|
51650
|
+
const inner = this.visit(ctx.expr17());
|
|
51651
|
+
if (inner.kind === "ill-typed")
|
|
51652
|
+
return inner;
|
|
51653
|
+
if (inner.kind === "empty")
|
|
51654
|
+
return { kind: "empty" };
|
|
51655
|
+
return UNKNOWN;
|
|
51656
|
+
}
|
|
51657
|
+
if (ctx.STAR_TOK()) {
|
|
51658
|
+
// reflexive transitive closure *X = iden ∪ ^X — *none = iden, which is
|
|
51659
|
+
// generally non-empty, so we cannot conclude empty here.
|
|
51660
|
+
return UNKNOWN;
|
|
51661
|
+
}
|
|
51662
|
+
if (ctx.GET_LABEL_TOK() ||
|
|
51663
|
+
ctx.GET_LABEL_STR_TOK() ||
|
|
51664
|
+
ctx.GET_LABEL_BOOL_TOK() ||
|
|
51665
|
+
ctx.GET_LABEL_NUM_TOK()) {
|
|
51666
|
+
// Label lookups depend on per-atom labels in the data instance.
|
|
51667
|
+
return UNKNOWN;
|
|
51668
|
+
}
|
|
51669
|
+
return this.visitChildren(ctx);
|
|
51670
|
+
}
|
|
51671
|
+
visitExpr18(ctx) {
|
|
51672
|
+
if (ctx.LEFT_PAREN_TOK()) {
|
|
51673
|
+
return this.visit(ctx.expr());
|
|
51674
|
+
}
|
|
51675
|
+
if (ctx.const()) {
|
|
51676
|
+
const c = ctx.const();
|
|
51677
|
+
if (c.NONE_TOK())
|
|
51678
|
+
return { kind: "empty" };
|
|
51679
|
+
if (c.number()) {
|
|
51680
|
+
const n = Number(c.number().text);
|
|
51681
|
+
const value = c.MINUS_TOK() ? -n : n;
|
|
51682
|
+
return { kind: "num", value };
|
|
51683
|
+
}
|
|
51684
|
+
// iden, univ are data-dependent
|
|
51685
|
+
return UNKNOWN;
|
|
51686
|
+
}
|
|
51687
|
+
if (ctx.qualName()) {
|
|
51688
|
+
// qualName resolves to either INT_TOK / SUM_TOK (data-dependent) or a
|
|
51689
|
+
// plain name — descend so visitName can fold `true` / `false`.
|
|
51690
|
+
return this.visit(ctx.qualName());
|
|
51691
|
+
}
|
|
51692
|
+
if (ctx.LEFT_CURLY_TOK()) {
|
|
51693
|
+
// Set comprehension `{x : S | body}` is empty if any quantified set is
|
|
51694
|
+
// empty, or if the body is statically false. We also enforce the
|
|
51695
|
+
// reserved-name policy here: under a schema, type/relation names cannot
|
|
51696
|
+
// be reused as binder variables.
|
|
51697
|
+
const declList = ctx.quantDeclList();
|
|
51698
|
+
const boundHere = new Set();
|
|
51699
|
+
if (declList) {
|
|
51700
|
+
let cur = declList;
|
|
51701
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
51702
|
+
while (cur) {
|
|
51703
|
+
const setExpr = cur.quantDecl().expr();
|
|
51704
|
+
const v = this.visit(setExpr);
|
|
51705
|
+
if (v.kind === "empty")
|
|
51706
|
+
return { kind: "empty" };
|
|
51707
|
+
if (v.kind === "ill-typed")
|
|
51708
|
+
return v;
|
|
51709
|
+
this.collectNamesFromList(cur.quantDecl().nameList(), boundHere);
|
|
51710
|
+
const next = cur.quantDeclList();
|
|
51711
|
+
if (!next)
|
|
51712
|
+
break;
|
|
51713
|
+
cur = next;
|
|
51714
|
+
}
|
|
51715
|
+
}
|
|
51716
|
+
const reservedError = this.illTypedIfBindsReservedName(boundHere);
|
|
51717
|
+
if (reservedError)
|
|
51718
|
+
return reservedError;
|
|
51719
|
+
const blockOrBar = ctx.blockOrBar();
|
|
51720
|
+
if (blockOrBar && blockOrBar.BAR_TOK() && blockOrBar.expr()) {
|
|
51721
|
+
const body = this.visit(blockOrBar.expr());
|
|
51722
|
+
if (body.kind === "ill-typed")
|
|
51723
|
+
return body;
|
|
51724
|
+
if (body.kind === "bool" && !body.value)
|
|
51725
|
+
return { kind: "empty" };
|
|
51726
|
+
}
|
|
51727
|
+
return UNKNOWN;
|
|
51728
|
+
}
|
|
51729
|
+
if (ctx.block()) {
|
|
51730
|
+
return this.visit(ctx.block());
|
|
51731
|
+
}
|
|
51732
|
+
// sexpr, AT_TOK, BACKQUOTE_TOK, THIS_TOK — unsupported.
|
|
51733
|
+
return UNKNOWN;
|
|
51734
|
+
}
|
|
51735
|
+
// `true` / `false` are parsed as names rather than `const` literals — the
|
|
51736
|
+
// evaluator handles them in visitName, so we do the same. When a schema is
|
|
51737
|
+
// available, type and relation names resolve to `typed` with arity and
|
|
51738
|
+
// column types from the schema.
|
|
51739
|
+
visitName(ctx) {
|
|
51740
|
+
const id = (0, utils_1.getIdentifierName)(ctx);
|
|
51741
|
+
if (id === "true")
|
|
51742
|
+
return { kind: "bool", value: true };
|
|
51743
|
+
if (id === "false")
|
|
51744
|
+
return { kind: "bool", value: false };
|
|
51745
|
+
if (this.schema) {
|
|
51746
|
+
const t = this.schema.getType(id);
|
|
51747
|
+
if (t)
|
|
51748
|
+
return { kind: "typed", arity: 1, columnTypes: [t.id] };
|
|
51749
|
+
const r = this.schema.getRelation(id);
|
|
51750
|
+
if (r) {
|
|
51751
|
+
const cols = r.types;
|
|
51752
|
+
return { kind: "typed", arity: cols.length, columnTypes: cols };
|
|
51753
|
+
}
|
|
51754
|
+
}
|
|
51755
|
+
return UNKNOWN;
|
|
51756
|
+
}
|
|
51757
|
+
}
|
|
51758
|
+
exports.ForgeExprStaticAnalyzer = ForgeExprStaticAnalyzer;
|
|
51759
|
+
|
|
51760
|
+
|
|
50804
51761
|
/***/ }),
|
|
50805
51762
|
|
|
50806
51763
|
/***/ "./src/NumericConstraintOptimizer.ts":
|
|
@@ -62246,13 +63203,16 @@ exports.FORGE_RESERVED_KEYWORDS = new Set([
|
|
|
62246
63203
|
"use strict";
|
|
62247
63204
|
|
|
62248
63205
|
Object.defineProperty(exports, "__esModule", ({ value: true }));
|
|
62249
|
-
exports.FORGE_RESERVED_KEYWORDS = exports.quoteIfReserved = exports.getIdentifierName = exports.SelectorSynthesisError = exports.synthesizeSelectorWithWhy = exports.synthesizeBinaryRelationWithWhy = exports.synthesizeBinaryRelation = exports.synthesizeSelector = exports.SimpleGraphQueryEvaluator = void 0;
|
|
63206
|
+
exports.FORGE_RESERVED_KEYWORDS = exports.quoteIfReserved = exports.getIdentifierName = exports.SelectorSynthesisError = exports.synthesizeSelectorWithWhy = exports.synthesizeBinaryRelationWithWhy = exports.synthesizeBinaryRelation = exports.synthesizeSelector = exports.ForgeExprStaticAnalyzer = exports.SimpleGraphQueryEvaluator = void 0;
|
|
63207
|
+
exports.analyzeForgeExpression = analyzeForgeExpression;
|
|
62250
63208
|
const antlr4ts_1 = __webpack_require__(/*! antlr4ts */ "./node_modules/antlr4ts/index.js");
|
|
62251
63209
|
const ForgeParser_1 = __webpack_require__(/*! ./forge-antlr/ForgeParser */ "./src/forge-antlr/ForgeParser.ts");
|
|
62252
63210
|
const ForgeLexer_1 = __webpack_require__(/*! ./forge-antlr/ForgeLexer */ "./src/forge-antlr/ForgeLexer.ts");
|
|
62253
63211
|
const ForgeListenerImpl_1 = __webpack_require__(/*! ./forge-antlr/ForgeListenerImpl */ "./src/forge-antlr/ForgeListenerImpl.ts");
|
|
62254
63212
|
const ParseTreeWalker_1 = __webpack_require__(/*! antlr4ts/tree/ParseTreeWalker */ "./node_modules/antlr4ts/tree/ParseTreeWalker.js");
|
|
62255
63213
|
const ForgeExprEvaluator_1 = __webpack_require__(/*! ./ForgeExprEvaluator */ "./src/ForgeExprEvaluator.ts");
|
|
63214
|
+
const ForgeExprStaticAnalyzer_1 = __webpack_require__(/*! ./ForgeExprStaticAnalyzer */ "./src/ForgeExprStaticAnalyzer.ts");
|
|
63215
|
+
Object.defineProperty(exports, "ForgeExprStaticAnalyzer", ({ enumerable: true, get: function () { return ForgeExprStaticAnalyzer_1.ForgeExprStaticAnalyzer; } }));
|
|
62256
63216
|
const errorListener_1 = __webpack_require__(/*! ./errorListener */ "./src/errorListener.ts");
|
|
62257
63217
|
function createForgeParser(input) {
|
|
62258
63218
|
const inputStream = antlr4ts_1.CharStreams.fromString(input);
|
|
@@ -62264,14 +63224,56 @@ function createForgeParser(input) {
|
|
|
62264
63224
|
parser.addErrorListener(new errorListener_1.ParseErrorListener());
|
|
62265
63225
|
return parser;
|
|
62266
63226
|
}
|
|
63227
|
+
/**
|
|
63228
|
+
* Evaluates Forge expressions against an `IDataInstance`.
|
|
63229
|
+
*
|
|
63230
|
+
* ## Caching contract (important for correctness)
|
|
63231
|
+
*
|
|
63232
|
+
* For performance, this class caches an inner evaluator (with its relation
|
|
63233
|
+
* index and subexpression cache) across `evaluateExpression` calls. The
|
|
63234
|
+
* cache is invalidated when the `datum` field is reassigned to a different
|
|
63235
|
+
* reference, but **not** when the underlying `IDataInstance` is mutated in
|
|
63236
|
+
* place (e.g. adding/removing atoms or tuples on the same object).
|
|
63237
|
+
*
|
|
63238
|
+
* If you mutate the underlying data in place, you **must** call
|
|
63239
|
+
* {@link invalidate} (or reassign `datum` to a new object) before the next
|
|
63240
|
+
* `evaluateExpression` call. Otherwise queries may return stale results.
|
|
63241
|
+
*
|
|
63242
|
+
* Recommended patterns:
|
|
63243
|
+
* - Treat `IDataInstance` as immutable; create a new instance on data
|
|
63244
|
+
* changes and construct a new `SimpleGraphQueryEvaluator` (or assign to
|
|
63245
|
+
* `.datum`).
|
|
63246
|
+
* - Or, if you mutate in place, call `invalidate()` after each mutation
|
|
63247
|
+
* batch.
|
|
63248
|
+
*/
|
|
62267
63249
|
class SimpleGraphQueryEvaluator {
|
|
62268
63250
|
constructor(datum) {
|
|
62269
63251
|
this.forgeListener = new ForgeListenerImpl_1.ForgeListenerImpl();
|
|
62270
63252
|
this.walker = new ParseTreeWalker_1.ParseTreeWalker();
|
|
62271
63253
|
// Cache for parsed expressions to avoid re-parsing the same expression
|
|
62272
63254
|
this.parseTreeCache = new Map();
|
|
63255
|
+
// Cached inner evaluator. Reused across evaluateExpression calls so its
|
|
63256
|
+
// relation cache / relation index / subexpression cache survive between
|
|
63257
|
+
// calls. Invalidated when the `datum` reference changes, or explicitly
|
|
63258
|
+
// via `invalidate()`. NOT invalidated by in-place mutation of the
|
|
63259
|
+
// underlying IDataInstance -- callers must signal that themselves.
|
|
63260
|
+
this.cachedEvaluator = null;
|
|
63261
|
+
this.cachedEvaluatorDatum = null;
|
|
62273
63262
|
this.datum = datum;
|
|
62274
63263
|
}
|
|
63264
|
+
/**
|
|
63265
|
+
* Discard the cached inner evaluator (and its relation index /
|
|
63266
|
+
* subexpression cache). Call this after mutating the underlying
|
|
63267
|
+
* `IDataInstance` in place, so the next `evaluateExpression` sees the
|
|
63268
|
+
* updated data.
|
|
63269
|
+
*
|
|
63270
|
+
* Cheap: just nulls a couple of references. The caches rebuild lazily
|
|
63271
|
+
* on the next query.
|
|
63272
|
+
*/
|
|
63273
|
+
invalidate() {
|
|
63274
|
+
this.cachedEvaluator = null;
|
|
63275
|
+
this.cachedEvaluatorDatum = null;
|
|
63276
|
+
}
|
|
62275
63277
|
getExpressionParseTree(forgeExpr) {
|
|
62276
63278
|
const parser = createForgeParser(forgeExpr);
|
|
62277
63279
|
const tree = parser.parseExpr();
|
|
@@ -62302,13 +63304,27 @@ class SimpleGraphQueryEvaluator {
|
|
|
62302
63304
|
};
|
|
62303
63305
|
}
|
|
62304
63306
|
}
|
|
62305
|
-
|
|
63307
|
+
// Reuse the inner evaluator across calls. Rebuild only when the datum
|
|
63308
|
+
// reference has changed (the contract: callers signal "data changed"
|
|
63309
|
+
// by swapping the datum reference, not by mutating it in place).
|
|
63310
|
+
if (this.cachedEvaluator === null || this.cachedEvaluatorDatum !== this.datum) {
|
|
63311
|
+
this.cachedEvaluator = new ForgeExprEvaluator_1.ForgeExprEvaluator(this.datum);
|
|
63312
|
+
this.cachedEvaluatorDatum = this.datum;
|
|
63313
|
+
}
|
|
63314
|
+
const evaluator = this.cachedEvaluator;
|
|
62306
63315
|
try {
|
|
62307
63316
|
let result = evaluator.visit(tree);
|
|
62308
63317
|
// ensure we're visiting an ExprContext
|
|
62309
63318
|
return result;
|
|
62310
63319
|
}
|
|
62311
63320
|
catch (error) {
|
|
63321
|
+
// The visit threw mid-traversal, so the evaluator's transient state
|
|
63322
|
+
// (environment stack) may be inconsistent. Discard the cached
|
|
63323
|
+
// evaluator so the next call rebuilds from a clean slate. The
|
|
63324
|
+
// relation cache will be rebuilt lazily on first use, which is the
|
|
63325
|
+
// same as the pre-change behavior.
|
|
63326
|
+
this.cachedEvaluator = null;
|
|
63327
|
+
this.cachedEvaluatorDatum = null;
|
|
62312
63328
|
if (error instanceof ForgeExprEvaluator_1.NameNotFoundError) {
|
|
62313
63329
|
// Return an empty EvalResult for undefined names
|
|
62314
63330
|
let emptyResult = [];
|
|
@@ -62329,6 +63345,33 @@ class SimpleGraphQueryEvaluator {
|
|
|
62329
63345
|
}
|
|
62330
63346
|
}
|
|
62331
63347
|
exports.SimpleGraphQueryEvaluator = SimpleGraphQueryEvaluator;
|
|
63348
|
+
/**
|
|
63349
|
+
* Run a static analysis on a Forge expression.
|
|
63350
|
+
*
|
|
63351
|
+
* Returns `unsat` when the expression provably reduces to `false`, `empty`
|
|
63352
|
+
* when it provably reduces to the empty set, `tautology` when it provably
|
|
63353
|
+
* reduces to `true`, `ill-typed` for static type errors (e.g. arity
|
|
63354
|
+
* mismatch), and `unknown` otherwise (including parse errors).
|
|
63355
|
+
*
|
|
63356
|
+
* When `schema` is provided, the analyzer also uses the type lattice and
|
|
63357
|
+
* relation declarations to detect type-disjoint intersections, subtype
|
|
63358
|
+
* tautologies in `in`, join column-type mismatches, and arity errors.
|
|
63359
|
+
* Disjointness uses a closed-world rule (A ∩ B = ∅ iff no type in the
|
|
63360
|
+
* lattice has both A and B in its lineage).
|
|
63361
|
+
*/
|
|
63362
|
+
function analyzeForgeExpression(forgeExpr, schema) {
|
|
63363
|
+
try {
|
|
63364
|
+
const parser = createForgeParser(forgeExpr);
|
|
63365
|
+
const tree = parser.parseExpr();
|
|
63366
|
+
if (!tree || tree.childCount === 0) {
|
|
63367
|
+
return { status: "unknown" };
|
|
63368
|
+
}
|
|
63369
|
+
return new ForgeExprStaticAnalyzer_1.ForgeExprStaticAnalyzer(schema).analyze(tree);
|
|
63370
|
+
}
|
|
63371
|
+
catch {
|
|
63372
|
+
return { status: "unknown" };
|
|
63373
|
+
}
|
|
63374
|
+
}
|
|
62332
63375
|
var SelectorSynthesizer_1 = __webpack_require__(/*! ./SelectorSynthesizer */ "./src/SelectorSynthesizer.ts");
|
|
62333
63376
|
Object.defineProperty(exports, "synthesizeSelector", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.synthesizeSelector; } }));
|
|
62334
63377
|
Object.defineProperty(exports, "synthesizeBinaryRelation", ({ enumerable: true, get: function () { return SelectorSynthesizer_1.synthesizeBinaryRelation; } }));
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED