simple-graph-query 2.8.1 → 3.0.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,6 +6,91 @@ A TypeScript library for evaluating relational + some more expressions with a br
6
6
 
7
7
  MIT
8
8
 
9
+ ## Strings
10
+
11
+ Strings are written as double-quoted literals:
12
+
13
+ ```
14
+ {y : Color | @:y = "Black"}
15
+ ```
16
+
17
+ A literal always denotes a string, whatever the instance happens to contain. It
18
+ may hold any characters — spaces, punctuation, reserved keywords — and supports
19
+ the escapes `\"`, `\\`, `\n`, `\t`, `\r`, and `\0`; any other escaped character
20
+ stands for itself.
21
+
22
+ ```
23
+ "dark blue" "#FF0000" "2 of clubs" "set" "say \"hi\""
24
+ ```
25
+
26
+ There is no implicit conversion: `"12" = 12` and `"true" = true` are both false.
27
+ Use `@num:` / `@bool:` to convert explicitly.
28
+
29
+ ### Embedding in YAML
30
+
31
+ `@` is a reserved indicator in YAML, so an expression beginning with `@:` cannot
32
+ be a plain scalar — it has to be quoted whether or not it contains a string
33
+ literal. Single-quoted and block scalars pass `"` through untouched and are the
34
+ recommended styles:
35
+
36
+ ```yaml
37
+ selector: '@:y = "Black"'
38
+
39
+ selector: |-
40
+ @:y = "Black"
41
+ ```
42
+
43
+ A double-quoted YAML scalar also works, but requires escaping: `"@:y = \"Black\""`.
44
+
45
+ ### Prior to 3.0
46
+
47
+ Any identifier that failed to resolve was silently reinterpreted as a string, so
48
+ `@:y = Black` worked only as long as nothing in the instance was named `Black`.
49
+ Adding a sig by that name silently changed what the query meant, labels
50
+ containing spaces or punctuation were inexpressible, and typos evaluated to
51
+ themselves instead of being reported. Bare names are no longer strings; quote
52
+ them.
53
+
54
+ ## Unresolved names
55
+
56
+ A name that matches nothing in the instance evaluates to the **empty relation**
57
+ and raises a diagnostic. It is not an error:
58
+
59
+ ```ts
60
+ const { value, diagnostics } = evaluator.evaluateExpressionWithDiagnostics("Playr");
61
+ // value -> []
62
+ // diagnostics -> [{
63
+ // kind: "unresolved-name",
64
+ // severity: "warning",
65
+ // name: "Playr",
66
+ // suggestion: "Player",
67
+ // message: "'Playr' does not name a type, relation, or atom in this instance; ..."
68
+ // }]
69
+ ```
70
+
71
+ An instance carries only *populated* types and relations, so a sig with no atoms
72
+ is absent from it entirely — and a sig can empty out between frames of one
73
+ trace. A missing name is therefore indistinguishable from a typo at evaluation
74
+ time, which is why this is a warning for the consumer to surface rather than an
75
+ error. `evaluateExpression` is unchanged and returns the value alone.
76
+
77
+ Note that empty means predicates pass vacuously: `no Playr` is `true`. This is
78
+ correct for an empty set, and the diagnostic is the only thing distinguishing it
79
+ from a real result — so consumers should show these warnings, not drop them.
80
+
81
+ ### Static analysis
82
+
83
+ `analyzeForgeExpression(expr, schema)` reports the same names in
84
+ `unresolvedNames`, alongside (not instead of) its status. Given a *schema* it
85
+ is conclusive, because a schema declares every entity whether populated or not:
86
+
87
+ ```ts
88
+ analyzeForgeExpression("none & Playr", schema);
89
+ // { status: "empty", reason: "...", unresolvedNames: ["Playr"] }
90
+ ```
91
+
92
+ Without a schema the field is omitted — there is nothing to check names against.
93
+
9
94
  ## Selector synthesis overview
10
95
 
11
96
  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`.
@@ -7,6 +7,30 @@ export type Tuple = SingleValue[];
7
7
  export type EvalResult = SingleValue | Tuple[];
8
8
  export declare function areTupleArraysEqual(a: Tuple[], b: Tuple[]): boolean;
9
9
  export declare const SUPPORTED_BUILTINS: string[];
10
+ /**
11
+ * A non-fatal condition noticed while evaluating an expression.
12
+ *
13
+ * Diagnostics never change the value of an expression — they are advisory, and
14
+ * consumers are expected to surface them to whoever wrote the query. Severity
15
+ * is always `"warning"`: the evaluator sees a single instance, not a schema, so
16
+ * it is not in a position to declare an unresolved name an error.
17
+ */
18
+ export interface Diagnostic {
19
+ kind: "unresolved-name";
20
+ severity: "warning";
21
+ /** The name that could not be resolved. */
22
+ name: string;
23
+ /** Human-readable message suitable for showing to a query author. */
24
+ message: string;
25
+ /** Closest name in the instance, when one is close enough to be worth offering. */
26
+ suggestion?: string;
27
+ }
28
+ /**
29
+ * Strip the surrounding double quotes from a STRING_TOK and resolve its escape
30
+ * sequences. The lexer rule is `'"' (~["\\] | '\\' .)* '"'`, so a backslash
31
+ * always consumes exactly one following character.
32
+ */
33
+ export declare function unquoteStringLiteral(text: string): string;
10
34
  /**
11
35
  * A recursive evaluator for Forge expressions.
12
36
  * This visitor walks the parse tree and prints the type of operation encountered.
@@ -19,7 +43,24 @@ export declare class ForgeExprEvaluator extends AbstractParseTreeVisitor<EvalRes
19
43
  private instanceData;
20
44
  private relationCache;
21
45
  private relationIndexCache;
46
+ private diagnostics;
22
47
  constructor(datum: IDataInstance);
48
+ /** Diagnostics recorded since the last `resetDiagnostics()`. */
49
+ getDiagnostics(): Diagnostic[];
50
+ /** Clear diagnostics. Callers that reuse an evaluator must call this per evaluation. */
51
+ resetDiagnostics(): void;
52
+ /**
53
+ * Record a name that could not be resolved against this instance.
54
+ *
55
+ * This is deliberately a warning rather than an error. The instance format
56
+ * carries only populated types and relations, so a sig with no atoms in this
57
+ * instance is indistinguishable from a typo — and a sig can empty out between
58
+ * frames of the same trace. Failing hard would break working selectors at
59
+ * exactly the frames where a set happens to be empty.
60
+ */
61
+ private reportUnresolvedName;
62
+ /** Closest name in this instance within a small edit distance, if any. */
63
+ private suggestName;
23
64
  private buildRelationCache;
24
65
  private updateFreeVariables;
25
66
  private constructFreeVariableKey;
@@ -21,7 +21,19 @@ type Abstract = {
21
21
  } | {
22
22
  kind: "unknown";
23
23
  };
24
- export type StaticAnalysis = {
24
+ /**
25
+ * Names the expression references that the schema does not declare.
26
+ *
27
+ * This is reported alongside — not instead of — the status, because an
28
+ * expression can both name something undeclared and be provably empty. It is
29
+ * populated only when a schema is supplied: without one there is nothing to
30
+ * check a name against.
31
+ *
32
+ * Unlike the evaluator's `unresolved-name` diagnostic, this IS conclusive. A
33
+ * schema declares every entity whether or not it is populated, so a name absent
34
+ * from the schema cannot be explained away as a sig that happens to be empty.
35
+ */
36
+ export type StaticAnalysis = ({
25
37
  status: "unsat";
26
38
  reason: string;
27
39
  } | {
@@ -35,10 +47,17 @@ export type StaticAnalysis = {
35
47
  reason: string;
36
48
  } | {
37
49
  status: "unknown";
50
+ }) & {
51
+ unresolvedNames?: string[];
38
52
  };
39
53
  export declare class ForgeExprStaticAnalyzer extends AbstractParseTreeVisitor<Abstract> implements ForgeVisitor<Abstract> {
40
54
  private readonly schema?;
55
+ private readonly boundScopes;
56
+ private readonly unresolved;
41
57
  constructor(schema?: IForgeSchema);
58
+ private isBound;
59
+ /** Run `body` with `names` treated as bound. */
60
+ private withBoundScope;
42
61
  private collectNamesFromList;
43
62
  private collectBoundNames;
44
63
  private illTypedIfBindsReservedName;
@@ -67,5 +86,10 @@ export declare class ForgeExprStaticAnalyzer extends AbstractParseTreeVisitor<Ab
67
86
  visitExpr17(ctx: Expr17Context): Abstract;
68
87
  visitExpr18(ctx: Expr18Context): Abstract;
69
88
  visitName(ctx: NameContext): Abstract;
89
+ /** Walk the whole tree, recording every name the schema does not declare. */
90
+ private collectUnresolvedNames;
91
+ /** The binder parts of a quantifier or set comprehension, if this node is one. */
92
+ private asBinder;
93
+ private recordIfUnresolved;
70
94
  }
71
95
  export {};
@@ -7,7 +7,7 @@ export declare class ForgeLexer extends Lexer {
7
7
  static readonly LEFT_SQUARE_TOK = 2;
8
8
  static readonly RIGHT_SQUARE_TOK = 3;
9
9
  static readonly AS_TOK = 4;
10
- static readonly FILE_PATH_TOK = 5;
10
+ static readonly STRING_TOK = 5;
11
11
  static readonly VAR_TOK = 6;
12
12
  static readonly ABSTRACT_TOK = 7;
13
13
  static readonly SIG_TOK = 8;
@@ -13,7 +13,7 @@ export declare class ForgeParser extends Parser {
13
13
  static readonly LEFT_SQUARE_TOK = 2;
14
14
  static readonly RIGHT_SQUARE_TOK = 3;
15
15
  static readonly AS_TOK = 4;
16
- static readonly FILE_PATH_TOK = 5;
16
+ static readonly STRING_TOK = 5;
17
17
  static readonly VAR_TOK = 6;
18
18
  static readonly ABSTRACT_TOK = 7;
19
19
  static readonly SIG_TOK = 8;
@@ -385,7 +385,7 @@ export declare class ImportDeclContext extends ParserRuleContext {
385
385
  RIGHT_SQUARE_TOK(): TerminalNode | undefined;
386
386
  AS_TOK(): TerminalNode | undefined;
387
387
  name(): NameContext | undefined;
388
- FILE_PATH_TOK(): TerminalNode | undefined;
388
+ STRING_TOK(): TerminalNode | undefined;
389
389
  constructor(parent: ParserRuleContext | undefined, invokingState: number);
390
390
  get ruleIndex(): number;
391
391
  enterRule(listener: ForgeListener): void;
@@ -651,6 +651,7 @@ export declare class ConstContext extends ParserRuleContext {
651
651
  IDEN_TOK(): TerminalNode | undefined;
652
652
  number(): NumberContext | undefined;
653
653
  MINUS_TOK(): TerminalNode | undefined;
654
+ STRING_TOK(): TerminalNode | undefined;
654
655
  constructor(parent: ParserRuleContext | undefined, invokingState: number);
655
656
  get ruleIndex(): number;
656
657
  enterRule(listener: ForgeListener): void;
@@ -846,7 +847,7 @@ export declare class OptionDeclContext extends ParserRuleContext {
846
847
  OPTION_TOK(): TerminalNode;
847
848
  qualName(): QualNameContext[];
848
849
  qualName(i: number): QualNameContext;
849
- FILE_PATH_TOK(): TerminalNode | undefined;
850
+ STRING_TOK(): TerminalNode | undefined;
850
851
  number(): NumberContext | undefined;
851
852
  MINUS_TOK(): TerminalNode | undefined;
852
853
  constructor(parent: ParserRuleContext | undefined, invokingState: number);
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { ForgeListenerImpl } from './forge-antlr/ForgeListenerImpl';
2
2
  import { ParseTreeWalker } from 'antlr4ts/tree/ParseTreeWalker';
3
- import { EvalResult } from './ForgeExprEvaluator';
3
+ import { Diagnostic, EvalResult } from './ForgeExprEvaluator';
4
4
  import { ForgeExprStaticAnalyzer, StaticAnalysis } from './ForgeExprStaticAnalyzer';
5
5
  import { IDataInstance, IForgeSchema } from './types';
6
6
  export type ErrorResult = {
@@ -44,6 +44,7 @@ export declare class SimpleGraphQueryEvaluator {
44
44
  private parseTreeCache;
45
45
  private cachedEvaluator;
46
46
  private cachedEvaluatorDatum;
47
+ private lastDiagnostics;
47
48
  constructor(datum: IDataInstance);
48
49
  /**
49
50
  * Discard the cached inner evaluator (and its relation index /
@@ -56,6 +57,24 @@ export declare class SimpleGraphQueryEvaluator {
56
57
  */
57
58
  invalidate(): void;
58
59
  getExpressionParseTree(forgeExpr: string): import("./forge-antlr/ForgeParser").ParseExprContext;
60
+ /**
61
+ * Evaluate `forgeExpr` and return both its value and any diagnostics raised
62
+ * along the way.
63
+ *
64
+ * Diagnostics are advisory and never change the value — today the only kind
65
+ * is an unresolved name, which evaluates to the empty set. Consumers should
66
+ * surface them to whoever authored the query; see {@link Diagnostic}.
67
+ *
68
+ * Note that an unresolved name is a warning rather than an error on purpose.
69
+ * An instance carries only populated types and relations, so a sig that is
70
+ * empty here looks exactly like a typo, and a sig can empty out between
71
+ * frames of one trace. Deciding which it is needs context this library does
72
+ * not have.
73
+ */
74
+ evaluateExpressionWithDiagnostics(forgeExpr: string): {
75
+ value: EvaluationResult;
76
+ diagnostics: Diagnostic[];
77
+ };
59
78
  evaluateExpression(forgeExpr: string): EvaluationResult;
60
79
  }
61
80
  /**
@@ -75,5 +94,6 @@ export declare class SimpleGraphQueryEvaluator {
75
94
  export declare function analyzeForgeExpression(forgeExpr: string, schema?: IForgeSchema): StaticAnalysis;
76
95
  export { ForgeExprStaticAnalyzer, StaticAnalysis };
77
96
  export type { IForgeSchema };
97
+ export type { Diagnostic };
78
98
  export { synthesizeSelector, synthesizeBinaryRelation, synthesizeBinaryRelationWithWhy, synthesizeSelectorWithWhy, AtomSelectionExample, BinaryRelationExample, SelectorSynthesisError, SynthesisWhy, SynthesisWhyExample, WhyNode, } from './SelectorSynthesizer';
79
99
  export { getIdentifierName, quoteIfReserved, FORGE_RESERVED_KEYWORDS, } from './forge-antlr/utils';