eyelang 0.1.4 → 0.1.6
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 +2 -22
- package/docs/guide.md +14 -6
- package/docs/language-reference.md +1 -3
- package/index.d.ts +167 -29
- package/package.json +1 -1
- package/test/run-regression.mjs +168 -58
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Eyelang
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/eyelang)
|
|
4
|
+
[](https://doi.org/10.5281/zenodo.20761726)
|
|
5
5
|
|
|
6
6
|
Eyelang is a small logic programming language for rules, goals, answers, and proofs.
|
|
7
7
|
Its source syntax is a deliberately small subset of ordinary Prolog term and Horn-clause syntax.
|
|
@@ -34,26 +34,6 @@ console.log(result.stdout);
|
|
|
34
34
|
- [Guide](docs/guide.md)
|
|
35
35
|
- [Language reference](docs/language-reference.md)
|
|
36
36
|
|
|
37
|
-
### Eyelang built-ins
|
|
38
|
-
|
|
39
|
-
The Eyelang engine has its own built-in registry under `src/builtins/`. Built-ins are called as ordinary Eyelang predicates. See the [Eyelang language reference](docs/language-reference.md#9-standard-built-in-predicates) for the portable profile. The bundled implementation currently registers 80 name/arity entries across 78 predicate names:
|
|
40
|
-
|
|
41
|
-
| Family | Count | Built-ins |
|
|
42
|
-
|---|---:|---|
|
|
43
|
-
| Core and host | 4 | `eq/2`, `neq/2`, `local_time/1`, `difference/3` |
|
|
44
|
-
| Arithmetic, comparison, and generators | 29 | `neg/2`, `abs/2`, `sin/2`, `cos/2`, `tan/2`, `asin/2`, `acos/2`, `sqrt/2`, `floor/2`, `ceiling/2`, `trunc/2`, `rounded/2`, `exp/2`, `log/2`, `add/3`, `sub/3`, `mul/3`, `div/3`, `mod/3`, `min/3`, `max/3`, `pow/3`, `atan2/3`, `lt/2`, `gt/2`, `le/2`, `ge/2`, `between/3`, `smallest_divisor_from/3` |
|
|
45
|
-
| Strings and conversions | 15 | `str_concat/3`, `contains/2`, `matches/2`, `matches/3`, `not_matches/2`, `split/3`, `join/3`, `substring/4`, `replace/4`, `lowercase/2`, `uppercase/2`, `trim/2`, `number_string/2`, `atom_string/2`, `term_string/2` |
|
|
46
|
-
| Lists | 19 | `append/3`, `nth0/3`, `set_nth0/4`, `head/2`, `rest/2`, `last/2`, `take/3`, `drop/3`, `slice/4`, `member/2`, `select/3`, `not_member/2`, `reverse/2`, `length/2`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `sort/2` |
|
|
47
|
-
| Aggregation | 5 | `findall/3`, `countall/2`, `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
48
|
-
| Control | 3 | `not/1`, `once/1`, `forall/2` |
|
|
49
|
-
| Context and terms | 5 | `holds/2`, `holds/3`, `functor/3`, `arg/3`, `compound_name_arguments/3` |
|
|
50
|
-
| **Total** | **80** | |
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## Custom built-ins
|
|
54
|
-
|
|
55
|
-
Custom built-ins can be supplied by creating a `BuiltinRegistry` and passing it to the solver or `run` API.
|
|
56
|
-
|
|
57
37
|
## Tests
|
|
58
38
|
|
|
59
39
|
```bash
|
package/docs/guide.md
CHANGED
|
@@ -238,9 +238,19 @@ The playground has matching `--stats` and `--proof` checkboxes, so browser runs
|
|
|
238
238
|
|
|
239
239
|
### Builtins
|
|
240
240
|
|
|
241
|
-
Eyelang builtins are registered by name and arity in small modules under [`src/builtins`](../src/builtins). This keeps the runtime portable to Node.js and the browser while giving each builtin family a clear boundary.
|
|
241
|
+
Eyelang builtins are registered by name and arity in small modules under [`src/builtins`](../src/builtins). This keeps the runtime portable to Node.js and the browser while giving each builtin family a clear boundary. Built-ins are called as ordinary Eyelang predicates. See the [Eyelang language reference](language-reference.md#9-standard-built-in-predicates) for the portable profile. The bundled implementation currently registers 80 name/arity entries across 78 predicate names:
|
|
242
|
+
|
|
243
|
+
| Family | Count | Built-ins |
|
|
244
|
+
|---|---:|---|
|
|
245
|
+
| Core and host | 4 | `eq/2`, `neq/2`, `local_time/1`, `difference/3` |
|
|
246
|
+
| Arithmetic, comparison, and generators | 29 | `neg/2`, `abs/2`, `sin/2`, `cos/2`, `tan/2`, `asin/2`, `acos/2`, `sqrt/2`, `floor/2`, `ceiling/2`, `trunc/2`, `rounded/2`, `exp/2`, `log/2`, `add/3`, `sub/3`, `mul/3`, `div/3`, `mod/3`, `min/3`, `max/3`, `pow/3`, `atan2/3`, `lt/2`, `gt/2`, `le/2`, `ge/2`, `between/3`, `smallest_divisor_from/3` |
|
|
247
|
+
| Strings and conversions | 15 | `str_concat/3`, `contains/2`, `matches/2`, `matches/3`, `not_matches/2`, `split/3`, `join/3`, `substring/4`, `replace/4`, `lowercase/2`, `uppercase/2`, `trim/2`, `number_string/2`, `atom_string/2`, `term_string/2` |
|
|
248
|
+
| Lists | 19 | `append/3`, `nth0/3`, `set_nth0/4`, `head/2`, `rest/2`, `last/2`, `take/3`, `drop/3`, `slice/4`, `member/2`, `select/3`, `not_member/2`, `reverse/2`, `length/2`, `sum_list/2`, `min_list/2`, `max_list/2`, `list_to_set/2`, `sort/2` |
|
|
249
|
+
| Aggregation | 5 | `findall/3`, `countall/2`, `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
250
|
+
| Control | 3 | `not/1`, `once/1`, `forall/2` |
|
|
251
|
+
| Context and terms | 5 | `holds/2`, `holds/3`, `functor/3`, `arg/3`, `compound_name_arguments/3` |
|
|
252
|
+
| **Total** | **80** | |
|
|
242
253
|
|
|
243
|
-
The builtin families cover unification, arithmetic, comparison, dates, strings, lists, aggregation, context terms, term inspection, and search control. Domain-specific number-theory and matrix helper modules were removed from the default registry because those predicates were examples/accelerators rather than a reusable portable surface. New reusable helpers cover common numeric functions, list slicing and summaries, string normalization/conversion, term inspection/construction, and `forall/2`. The complete bundled implementation list is kept in the top-level [README built-ins section](../README.md#built-ins-1), and the regression suite checks that table against the actual runtime registry.
|
|
244
254
|
|
|
245
255
|
To add a builtin, create or extend a module with `register(registry)` and call `registry.add(name, arity, handler, options)`. The default registry is assembled in [`src/builtins/registry.js`](../src/builtins/registry.js). Builtins that are only safe for specific argument modes should provide a `ready` predicate and `fallbackWhenNotReady: true`, so user-defined clauses remain visible until the builtin is applicable.
|
|
246
256
|
|
|
@@ -279,8 +289,6 @@ Use `holds/2` when you want to match the member term directly, for example `name
|
|
|
279
289
|
|
|
280
290
|
`matches/3` can create context data from named regular-expression captures, which is useful when text logs or messages need to become facts before later rules inspect them with `holds/2` or `holds/3`. See [`observability-log-correlation.pl`](../examples/observability-log-correlation.pl) for a complete log-correlation example.
|
|
281
291
|
|
|
282
|
-
The N3 counterpart of the context schema audit lives at [`examples/context-schema-audit.n3`](../examples/context-schema-audit.n3) with golden output in [`examples/output/context-schema-audit.md`](../examples/output/context-schema-audit.md).
|
|
283
|
-
|
|
284
292
|
|
|
285
293
|
## Example catalog
|
|
286
294
|
|
|
@@ -457,8 +465,8 @@ The conformance suite lives in [`test/conformance/`](../test/conformance/) as on
|
|
|
457
465
|
Common commands:
|
|
458
466
|
|
|
459
467
|
```sh
|
|
460
|
-
npm run test:eyelang #
|
|
461
|
-
npm
|
|
468
|
+
npm run test:eyelang # alias for npm test
|
|
469
|
+
npm test # full conformance, regression/API/white-box, examples, and proof examples
|
|
462
470
|
node test/run-conformance.mjs
|
|
463
471
|
node test/run-regression.mjs
|
|
464
472
|
node test/run-examples.mjs
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
- [9.6 Strings and atom constants](#96-strings-and-atom-constants)
|
|
40
40
|
- [9.7 Lists](#97-lists)
|
|
41
41
|
- [9.8 Aggregation and ordering](#98-aggregation-and-ordering)
|
|
42
|
-
- [9.9 Context
|
|
42
|
+
- [9.9 Context and term inspection](#99-context-and-term-inspection)
|
|
43
43
|
- [9.10 Search control](#910-search-control)
|
|
44
44
|
- [10. Implementation-specific built-ins](#10-implementation-specific-built-ins)
|
|
45
45
|
- [11. Declarations](#11-declarations)
|
|
@@ -475,8 +475,6 @@ The first goal can yield `holds((name(alice, "Alice"), knows(alice, bob)), name(
|
|
|
475
475
|
|
|
476
476
|
`holds/3` is the appropriate form for schema-style introspection because it exposes the predicate name and all arguments without assuming a fixed arity. For example, a single rule can inspect `heartbeat`, `source(sensor17)`, `temperature(sensor17, 38)`, and `signature(sensor17, sha256, Hash, Time)` as `heartbeat/0`, `source/1`, `temperature/2`, and `signature/4`; see [`context-schema-audit.pl`](../examples/context-schema-audit.pl).
|
|
477
477
|
|
|
478
|
-
The N3 example [`context-schema-audit.n3`](../examples/context-schema-audit.n3) shows the same idea in quoted graph form: members are encoded as predicates with RDF-list argument objects, then `log:includes` and `list:length` expose `Name + Arity` for schema checking.
|
|
479
|
-
|
|
480
478
|
### 9.10 Search control
|
|
481
479
|
|
|
482
480
|
| Built-in | Meaning |
|
package/index.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface EyelangRunOptions {
|
|
|
6
6
|
proof?: boolean;
|
|
7
7
|
why?: boolean;
|
|
8
8
|
explain?: boolean;
|
|
9
|
+
maxDepth?: number;
|
|
10
|
+
solutionLimit?: number;
|
|
11
|
+
registry?: BuiltinRegistry;
|
|
12
|
+
sourceMetadata?: boolean;
|
|
13
|
+
markRecursive?: boolean;
|
|
9
14
|
[key: string]: unknown;
|
|
10
15
|
}
|
|
11
16
|
|
|
@@ -20,61 +25,194 @@ export interface EyelangSourcePart {
|
|
|
20
25
|
filename?: string;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
export interface EyelangClause {
|
|
29
|
+
head: EyelangTerm;
|
|
30
|
+
body: EyelangTerm[];
|
|
31
|
+
index?: number;
|
|
32
|
+
filename?: string;
|
|
33
|
+
clauseNumber?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface EyelangPredicateGroup {
|
|
37
|
+
name: string;
|
|
38
|
+
arity: number;
|
|
39
|
+
clauses: EyelangClause[];
|
|
40
|
+
argIndexes: unknown[];
|
|
41
|
+
pairIndexes: unknown[];
|
|
42
|
+
memoized: boolean;
|
|
43
|
+
recursive: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type EyelangTerm = Term | { type: string; name: string; args?: EyelangTerm[]; arity?: number };
|
|
47
|
+
|
|
48
|
+
export class Term {
|
|
49
|
+
constructor(type: string, name?: unknown, args?: EyelangTerm[]);
|
|
50
|
+
type: string;
|
|
51
|
+
name: string;
|
|
52
|
+
args: EyelangTerm[];
|
|
53
|
+
get arity(): number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class Env {
|
|
57
|
+
constructor(bindings?: Iterable<readonly [string, EyelangTerm]> | null);
|
|
58
|
+
bindings: Map<string, EyelangTerm>;
|
|
59
|
+
clone(): Env;
|
|
60
|
+
has(name: string): boolean;
|
|
61
|
+
get(name: string): EyelangTerm | undefined;
|
|
62
|
+
bind(name: string, term: EyelangTerm): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
23
65
|
export class Program {
|
|
24
|
-
constructor(clauses?:
|
|
66
|
+
constructor(clauses?: EyelangClause[], options?: EyelangRunOptions);
|
|
67
|
+
clauses: EyelangClause[];
|
|
68
|
+
groups: Map<string, EyelangPredicateGroup>;
|
|
69
|
+
materializedGroups: Set<string>;
|
|
70
|
+
hasMaterialize: boolean;
|
|
25
71
|
static parse(source: string, options?: EyelangRunOptions): Program;
|
|
26
72
|
static parseSources(sources?: Array<string | EyelangSourcePart>, options?: EyelangRunOptions): Program;
|
|
27
|
-
|
|
73
|
+
makeGroup(name: string, arity: number): EyelangPredicateGroup;
|
|
74
|
+
indexClause(clause: EyelangClause): void;
|
|
75
|
+
findGroup(name: string, arity: number): EyelangPredicateGroup | null;
|
|
76
|
+
applyDeclarations(options?: EyelangRunOptions): void;
|
|
77
|
+
markRecursivePredicates(): void;
|
|
78
|
+
hasMaterializeDeclarations(): boolean;
|
|
79
|
+
groupIsMaterialized(group: EyelangPredicateGroup): boolean;
|
|
80
|
+
groupHasRule(group: EyelangPredicateGroup): boolean;
|
|
28
81
|
sourceFactLines(predicateKeys?: Set<string> | null): Set<string>;
|
|
82
|
+
materializationGoals(): EyelangTerm[];
|
|
29
83
|
}
|
|
30
84
|
|
|
31
|
-
export
|
|
32
|
-
|
|
33
|
-
|
|
85
|
+
export interface BuiltinDefinition {
|
|
86
|
+
name: string;
|
|
87
|
+
arity: number;
|
|
88
|
+
handler: BuiltinHandler;
|
|
89
|
+
deterministic: boolean;
|
|
90
|
+
ready: ((solver: Solver, goal: EyelangTerm, env: Env) => boolean) | null;
|
|
91
|
+
fallbackWhenNotReady: boolean;
|
|
92
|
+
shouldUse: ((solver: Solver, goal: EyelangTerm, env: Env) => boolean) | null;
|
|
93
|
+
}
|
|
34
94
|
|
|
35
|
-
export
|
|
36
|
-
|
|
95
|
+
export type BuiltinHandler = (context: { solver: Solver; goal: EyelangTerm; env: Env }) => Iterable<Env>;
|
|
96
|
+
|
|
97
|
+
export class BuiltinRegistry {
|
|
98
|
+
constructor();
|
|
99
|
+
defs: Map<string, BuiltinDefinition>;
|
|
100
|
+
add(name: string, arity: number, handler: BuiltinHandler, options?: Partial<BuiltinDefinition>): this;
|
|
101
|
+
get(name: string, arity: number): BuiltinDefinition | null;
|
|
37
102
|
}
|
|
38
103
|
|
|
39
104
|
export class Solver {
|
|
40
105
|
constructor(program: Program, options?: EyelangRunOptions);
|
|
106
|
+
program: Program;
|
|
107
|
+
registry: BuiltinRegistry;
|
|
108
|
+
maxDepth: number;
|
|
109
|
+
solutionLimit: number;
|
|
110
|
+
solutionsSeen: number;
|
|
111
|
+
active: unknown[];
|
|
112
|
+
memo: Map<string, unknown>;
|
|
41
113
|
stats: EyelangStats;
|
|
42
|
-
|
|
114
|
+
cloneForInnerGoal(solutionLimit?: number): Solver;
|
|
115
|
+
solve(goals: EyelangTerm | EyelangTerm[], env?: Env, depth?: number): Iterable<Env>;
|
|
116
|
+
activeVariant(goal: EyelangTerm, env: Env): boolean;
|
|
43
117
|
}
|
|
44
118
|
|
|
45
|
-
export
|
|
46
|
-
|
|
47
|
-
|
|
119
|
+
export const VAR: 'var';
|
|
120
|
+
export const ATOM: 'atom';
|
|
121
|
+
export const STRING: 'string';
|
|
122
|
+
export const NUMBER: 'number';
|
|
123
|
+
export const COMPOUND: 'compound';
|
|
48
124
|
|
|
125
|
+
export function variable(name: string): Term;
|
|
126
|
+
export function atom(name: string): Term;
|
|
127
|
+
export function stringTerm(value: string): Term;
|
|
128
|
+
export function numberTerm(value: string | number): Term;
|
|
129
|
+
export function compound(name: string, args?: EyelangTerm[]): Term;
|
|
130
|
+
export function emptyList(): Term;
|
|
131
|
+
export function cons(head: EyelangTerm, tail: EyelangTerm): Term;
|
|
132
|
+
export function deref(term: EyelangTerm, env: Env): EyelangTerm;
|
|
133
|
+
export function isScalar(term: EyelangTerm | null | undefined): boolean;
|
|
134
|
+
export function isEmptyList(term: EyelangTerm | null | undefined): boolean;
|
|
135
|
+
export function isCons(term: EyelangTerm | null | undefined): boolean;
|
|
136
|
+
export function isConjunction(term: EyelangTerm | null | undefined): boolean;
|
|
137
|
+
export function unify(left: EyelangTerm, right: EyelangTerm, env: Env): boolean;
|
|
138
|
+
export function cloneTerm(term: EyelangTerm): Term;
|
|
139
|
+
export function freshTerm(term: EyelangTerm, suffix: string | number): Term;
|
|
140
|
+
export function copyResolved(term: EyelangTerm, env: Env): Term;
|
|
141
|
+
export function termIsGround(term: EyelangTerm, env?: Env): boolean;
|
|
142
|
+
export function termToString(term: EyelangTerm, env?: Env, quoteStrings?: boolean): string;
|
|
143
|
+
export function lexicalValue(term: EyelangTerm, env: Env): string | null;
|
|
144
|
+
export function properListItems(list: EyelangTerm, env: Env): EyelangTerm[] | null;
|
|
145
|
+
export function listFromItems(items: EyelangTerm[], start?: number, end?: number, tail?: EyelangTerm): Term;
|
|
146
|
+
export function flattenConjunction(goal: EyelangTerm): EyelangTerm[];
|
|
147
|
+
export function termSignature(term: EyelangTerm | null | undefined): string | null;
|
|
148
|
+
export function variantTerms(left: EyelangTerm, leftEnv: Env, right: EyelangTerm, rightEnv: Env, pairs?: Map<string, string>, reverse?: Map<string, string>): boolean;
|
|
149
|
+
export function compareTerms(left: EyelangTerm, right: EyelangTerm): number;
|
|
150
|
+
export function isDecimalInteger(text: string | null | undefined): boolean;
|
|
151
|
+
export function compareIntegerText(left: string, right: string): number;
|
|
152
|
+
export function parseFiniteNumber(text: string | null | undefined): number | null;
|
|
153
|
+
export function numberTextFromDouble(value: number): string | null;
|
|
154
|
+
export function compareNumberText(left: string, right: string): number;
|
|
155
|
+
|
|
156
|
+
export function makeProgram(source: string, options?: EyelangRunOptions): Program;
|
|
157
|
+
export function parseClauses(source: string, options?: EyelangRunOptions): EyelangClause[];
|
|
158
|
+
export function parseProgramText(source: string, options?: EyelangRunOptions): EyelangClause[];
|
|
49
159
|
export function createDefaultRegistry(): BuiltinRegistry;
|
|
50
160
|
export function getDefaultRegistry(): BuiltinRegistry;
|
|
51
|
-
|
|
52
161
|
export function run(source: string | Program, options?: EyelangRunOptions): EyelangRunResult;
|
|
53
|
-
|
|
54
|
-
export function
|
|
55
|
-
export function
|
|
56
|
-
|
|
57
|
-
export const ATOM: 'atom';
|
|
58
|
-
export const VARIABLE: 'variable';
|
|
59
|
-
export const COMPOUND: 'compound';
|
|
60
|
-
export const NUMBER: 'number';
|
|
61
|
-
export const STRING: 'string';
|
|
62
|
-
export function atom(name: string): unknown;
|
|
63
|
-
export function variable(name: string): unknown;
|
|
64
|
-
export function compound(name: string, args?: unknown[]): unknown;
|
|
65
|
-
export function termToString(term: unknown, env?: Env, quoted?: boolean): string;
|
|
162
|
+
export function whyProof(program: Program, goal: EyelangTerm, options?: EyelangRunOptions): { ok: boolean; text: string };
|
|
163
|
+
export function whyNoProof(goal: EyelangTerm): string;
|
|
164
|
+
export function explainProof(program: Program, goal: EyelangTerm, options?: EyelangRunOptions): { ok: boolean; text: string };
|
|
66
165
|
|
|
67
166
|
declare const eyelang: {
|
|
68
|
-
|
|
167
|
+
VAR: typeof VAR;
|
|
168
|
+
ATOM: typeof ATOM;
|
|
169
|
+
STRING: typeof STRING;
|
|
170
|
+
NUMBER: typeof NUMBER;
|
|
171
|
+
COMPOUND: typeof COMPOUND;
|
|
172
|
+
Term: typeof Term;
|
|
173
|
+
Env: typeof Env;
|
|
69
174
|
Program: typeof Program;
|
|
175
|
+
Solver: typeof Solver;
|
|
176
|
+
BuiltinRegistry: typeof BuiltinRegistry;
|
|
177
|
+
variable: typeof variable;
|
|
178
|
+
atom: typeof atom;
|
|
179
|
+
stringTerm: typeof stringTerm;
|
|
180
|
+
numberTerm: typeof numberTerm;
|
|
181
|
+
compound: typeof compound;
|
|
182
|
+
emptyList: typeof emptyList;
|
|
183
|
+
cons: typeof cons;
|
|
184
|
+
deref: typeof deref;
|
|
185
|
+
isScalar: typeof isScalar;
|
|
186
|
+
isEmptyList: typeof isEmptyList;
|
|
187
|
+
isCons: typeof isCons;
|
|
188
|
+
isConjunction: typeof isConjunction;
|
|
189
|
+
unify: typeof unify;
|
|
190
|
+
cloneTerm: typeof cloneTerm;
|
|
191
|
+
freshTerm: typeof freshTerm;
|
|
192
|
+
copyResolved: typeof copyResolved;
|
|
193
|
+
termIsGround: typeof termIsGround;
|
|
194
|
+
termToString: typeof termToString;
|
|
195
|
+
lexicalValue: typeof lexicalValue;
|
|
196
|
+
properListItems: typeof properListItems;
|
|
197
|
+
listFromItems: typeof listFromItems;
|
|
198
|
+
flattenConjunction: typeof flattenConjunction;
|
|
199
|
+
termSignature: typeof termSignature;
|
|
200
|
+
variantTerms: typeof variantTerms;
|
|
201
|
+
compareTerms: typeof compareTerms;
|
|
202
|
+
isDecimalInteger: typeof isDecimalInteger;
|
|
203
|
+
compareIntegerText: typeof compareIntegerText;
|
|
204
|
+
parseFiniteNumber: typeof parseFiniteNumber;
|
|
205
|
+
numberTextFromDouble: typeof numberTextFromDouble;
|
|
206
|
+
compareNumberText: typeof compareNumberText;
|
|
70
207
|
makeProgram: typeof makeProgram;
|
|
71
208
|
parseClauses: typeof parseClauses;
|
|
72
209
|
parseProgramText: typeof parseProgramText;
|
|
73
|
-
Solver: typeof Solver;
|
|
74
|
-
Env: typeof Env;
|
|
75
|
-
BuiltinRegistry: typeof BuiltinRegistry;
|
|
76
210
|
createDefaultRegistry: typeof createDefaultRegistry;
|
|
77
211
|
getDefaultRegistry: typeof getDefaultRegistry;
|
|
212
|
+
run: typeof run;
|
|
213
|
+
whyProof: typeof whyProof;
|
|
214
|
+
whyNoProof: typeof whyNoProof;
|
|
215
|
+
explainProof: typeof explainProof;
|
|
78
216
|
};
|
|
79
217
|
|
|
80
218
|
export default eyelang;
|
package/package.json
CHANGED
package/test/run-regression.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import os from 'node:os';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { spawnSync } from 'node:child_process';
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import * as publicApi from '../src/index.js';
|
|
11
12
|
import {
|
|
12
13
|
run,
|
|
13
14
|
Program,
|
|
@@ -50,6 +51,7 @@ export function runRegression(reporter = new TestReporter()) {
|
|
|
50
51
|
|
|
51
52
|
try {
|
|
52
53
|
runSection(reporter, 'Regression', regressionCases());
|
|
54
|
+
runSection(reporter, 'Documentation sync', documentationSyncCases());
|
|
53
55
|
runSection(reporter, 'API', apiCases());
|
|
54
56
|
runSection(reporter, 'White-box', whiteBoxCases());
|
|
55
57
|
} finally {
|
|
@@ -188,26 +190,6 @@ why(
|
|
|
188
190
|
assertEqual(result.stderr, '', 'stderr');
|
|
189
191
|
},
|
|
190
192
|
},
|
|
191
|
-
{
|
|
192
|
-
name: 'README covers every mirrored example',
|
|
193
|
-
run: () => {
|
|
194
|
-
const examples = listExampleNames();
|
|
195
|
-
const readmeExamples = readmeCatalogExampleNames();
|
|
196
|
-
assertEqual(readmeExamples.join('\n'), examples.join('\n'), 'README example catalog');
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
{
|
|
200
|
-
name: 'README mirrors the Eyelang builtin registry',
|
|
201
|
-
run: () => {
|
|
202
|
-
const actual = registeredBuiltinNames();
|
|
203
|
-
const documented = readmeBuiltinNames();
|
|
204
|
-
assertEqual(documented.join('\n'), actual.join('\n'), 'README builtin catalog');
|
|
205
|
-
|
|
206
|
-
const { entries, names } = readmeBuiltinSummary();
|
|
207
|
-
assertEqual(entries, actual.length, 'README builtin entry count');
|
|
208
|
-
assertEqual(names, new Set(actual.map((item) => item.split('/')[0])).size, 'README builtin name count');
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
193
|
{
|
|
212
194
|
name: 'stdin input is accepted',
|
|
213
195
|
run: () => {
|
|
@@ -274,8 +256,44 @@ why(
|
|
|
274
256
|
];
|
|
275
257
|
}
|
|
276
258
|
|
|
259
|
+
|
|
260
|
+
function documentationSyncCases() {
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
name: 'language reference builtins match runtime registry',
|
|
264
|
+
run: () => assertArrayEqual(languageReferenceBuiltinNames(), registeredBuiltinNames(), 'builtins'),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
name: 'guide builtin catalog matches runtime registry',
|
|
268
|
+
run: () => {
|
|
269
|
+
assertArrayEqual(guideBuiltinNames(), registeredBuiltinNames(), 'builtins');
|
|
270
|
+
const summary = guideBuiltinSummary();
|
|
271
|
+
const actual = registeredBuiltinSummary();
|
|
272
|
+
assertEqual(summary.entries, actual.entries, 'builtin entry count');
|
|
273
|
+
assertEqual(summary.names, actual.names, 'builtin predicate name count');
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
name: 'guide example catalog matches examples directory',
|
|
278
|
+
run: () => assertArrayEqual(guideCatalogExampleNames(), listExampleNames(), 'example catalog'),
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'documentation local links and anchors resolve',
|
|
282
|
+
run: () => assertArrayEqual(findBrokenDocLinks(), [], 'broken documentation links'),
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
name: 'documented npm scripts exist in package.json',
|
|
286
|
+
run: () => assertArrayEqual(missingDocumentedPackageScripts(), [], 'missing documented npm scripts'),
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
}
|
|
290
|
+
|
|
277
291
|
function apiCases() {
|
|
278
292
|
return [
|
|
293
|
+
{
|
|
294
|
+
name: 'public type declarations match runtime exports',
|
|
295
|
+
run: () => assertArrayEqual(declaredValueExportNames(), runtimeExportNames(), 'public value exports'),
|
|
296
|
+
},
|
|
279
297
|
{
|
|
280
298
|
name: 'run materialization through public API without proof by default',
|
|
281
299
|
run: () => {
|
|
@@ -475,6 +493,7 @@ function runSection(reporter, name, cases) {
|
|
|
475
493
|
}
|
|
476
494
|
|
|
477
495
|
function sectionLabel(name) {
|
|
496
|
+
if (name === 'Documentation sync') return 'documentation sync';
|
|
478
497
|
if (name === 'API') return 'API';
|
|
479
498
|
if (name === 'White-box') return 'white-box';
|
|
480
499
|
return name.toLowerCase();
|
|
@@ -519,9 +538,9 @@ function listExampleNames() {
|
|
|
519
538
|
.sort();
|
|
520
539
|
}
|
|
521
540
|
|
|
522
|
-
function
|
|
523
|
-
const
|
|
524
|
-
const section = between(
|
|
541
|
+
function guideCatalogExampleNames() {
|
|
542
|
+
const guide = fs.readFileSync(path.join(packageRoot, 'docs', 'guide.md'), 'utf8');
|
|
543
|
+
const section = between(guide, '## Example catalog', '## Golden outputs, tests, and conformance');
|
|
525
544
|
return [...section.matchAll(/examples\/([A-Za-z0-9_-]+)\.pl/g)]
|
|
526
545
|
.map((match) => match[1])
|
|
527
546
|
.filter((name, index, names) => names.indexOf(name) === index)
|
|
@@ -532,56 +551,137 @@ function registeredBuiltinNames() {
|
|
|
532
551
|
return [...createDefaultRegistry().defs.keys()].sort();
|
|
533
552
|
}
|
|
534
553
|
|
|
535
|
-
function
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.map((
|
|
540
|
-
|
|
541
|
-
.sort();
|
|
554
|
+
function registeredBuiltinSummary() {
|
|
555
|
+
const names = registeredBuiltinNames();
|
|
556
|
+
return {
|
|
557
|
+
entries: names.length,
|
|
558
|
+
names: new Set(names.map((name) => name.split('/')[0])).size,
|
|
559
|
+
};
|
|
542
560
|
}
|
|
543
561
|
|
|
544
|
-
function
|
|
545
|
-
const
|
|
546
|
-
|
|
562
|
+
function guideBuiltinNames() {
|
|
563
|
+
const guide = fs.readFileSync(path.join(packageRoot, 'docs', 'guide.md'), 'utf8');
|
|
564
|
+
return documentedBuiltinNames(between(guide, '### Builtins', '## Aggregation helpers'));
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function guideBuiltinSummary() {
|
|
568
|
+
const guide = fs.readFileSync(path.join(packageRoot, 'docs', 'guide.md'), 'utf8');
|
|
569
|
+
const section = between(guide, '### Builtins', '## Aggregation helpers');
|
|
547
570
|
const match = section.match(/currently registers (\d+) name\/arity entries across (\d+) predicate names/);
|
|
548
|
-
if (match == null) throw new Error('
|
|
571
|
+
if (match == null) throw new Error('guide builtin summary not found');
|
|
549
572
|
return { entries: Number(match[1]), names: Number(match[2]) };
|
|
550
573
|
}
|
|
551
574
|
|
|
552
|
-
function
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
575
|
+
function languageReferenceBuiltinNames() {
|
|
576
|
+
const reference = fs.readFileSync(path.join(packageRoot, 'docs', 'language-reference.md'), 'utf8');
|
|
577
|
+
return documentedBuiltinNames(between(reference, '## 9. Standard built-in predicates', '## 10. Implementation-specific built-ins'));
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function documentedBuiltinNames(section) {
|
|
581
|
+
const names = [];
|
|
582
|
+
for (const line of section.split('\n')) {
|
|
583
|
+
if (!line.trim().startsWith('|') || !line.includes('`')) continue;
|
|
584
|
+
for (const match of line.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)\(([^`)]*)\)`/g)) {
|
|
585
|
+
const arity = match[2].trim() === '' ? 0 : match[2].split(',').length;
|
|
586
|
+
names.push(`${match[1]}/${arity}`);
|
|
587
|
+
}
|
|
588
|
+
for (const match of line.matchAll(/`([A-Za-z_][A-Za-z0-9_]*)\/(\d+)`/g)) {
|
|
589
|
+
names.push(`${match[1]}/${match[2]}`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return [...new Set(names)].sort();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function runtimeExportNames() {
|
|
596
|
+
return Object.keys(publicApi).sort();
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function declaredValueExportNames() {
|
|
600
|
+
const dts = fs.readFileSync(path.join(packageRoot, 'index.d.ts'), 'utf8');
|
|
601
|
+
return [...dts.matchAll(/^export\s+(?:declare\s+)?(?:class|function|const)\s+([A-Za-z_][A-Za-z0-9_]*)/gm)]
|
|
557
602
|
.map((match) => match[1])
|
|
603
|
+
.filter((name, index, names) => names.indexOf(name) === index)
|
|
558
604
|
.sort();
|
|
559
605
|
}
|
|
560
606
|
|
|
561
|
-
function
|
|
562
|
-
const
|
|
563
|
-
const
|
|
564
|
-
const
|
|
565
|
-
while (stack.length > 0) {
|
|
566
|
-
const file = stack.pop();
|
|
567
|
-
if (seen.has(file)) continue;
|
|
568
|
-
seen.add(file);
|
|
607
|
+
function missingDocumentedPackageScripts() {
|
|
608
|
+
const docs = documentationFiles();
|
|
609
|
+
const missing = [];
|
|
610
|
+
for (const file of docs) {
|
|
569
611
|
const text = fs.readFileSync(file, 'utf8');
|
|
570
|
-
for (const
|
|
571
|
-
|
|
572
|
-
if (!
|
|
573
|
-
const
|
|
574
|
-
|
|
612
|
+
for (const line of text.split('\n')) {
|
|
613
|
+
const trimmed = line.trim();
|
|
614
|
+
if (!trimmed.startsWith('npm ') && !line.includes('`npm ')) continue;
|
|
615
|
+
for (const match of line.matchAll(/\bnpm\s+(?:run\s+)?([A-Za-z0-9:_-]+)/g)) {
|
|
616
|
+
const command = match[1];
|
|
617
|
+
if (command === 'install') continue;
|
|
618
|
+
const script = command === 'test' ? 'test' : command;
|
|
619
|
+
if (!pkg.scripts?.[script]) missing.push(`${path.relative(packageRoot, file)}: npm ${command === 'test' ? 'test' : `run ${script}`}`);
|
|
620
|
+
}
|
|
575
621
|
}
|
|
576
622
|
}
|
|
577
|
-
return [...
|
|
623
|
+
return [...new Set(missing)].sort();
|
|
578
624
|
}
|
|
579
625
|
|
|
580
|
-
function
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
for (const
|
|
584
|
-
|
|
626
|
+
function findBrokenDocLinks() {
|
|
627
|
+
const broken = [];
|
|
628
|
+
const anchorsByFile = new Map();
|
|
629
|
+
for (const file of documentationFiles()) {
|
|
630
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
631
|
+
for (const target of markdownLinkTargets(text)) {
|
|
632
|
+
if (/^(?:https?:|mailto:)/i.test(target)) continue;
|
|
633
|
+
const [targetPathRaw, fragmentRaw] = target.split('#');
|
|
634
|
+
const targetPath = targetPathRaw === '' ? file : path.resolve(path.dirname(file), decodeURI(targetPathRaw));
|
|
635
|
+
const display = `${path.relative(packageRoot, file)} -> ${target}`;
|
|
636
|
+
if (!fs.existsSync(targetPath)) {
|
|
637
|
+
broken.push(`${display} (missing target)`);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (fragmentRaw != null && fragmentRaw !== '') {
|
|
641
|
+
const anchors = anchorsByFile.get(targetPath) ?? markdownAnchors(targetPath);
|
|
642
|
+
anchorsByFile.set(targetPath, anchors);
|
|
643
|
+
if (!anchors.has(fragmentRaw)) broken.push(`${display} (missing heading #${fragmentRaw})`);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return broken.sort();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function documentationFiles() {
|
|
651
|
+
return [
|
|
652
|
+
path.join(packageRoot, 'README.md'),
|
|
653
|
+
path.join(packageRoot, 'docs', 'guide.md'),
|
|
654
|
+
path.join(packageRoot, 'docs', 'language-reference.md'),
|
|
655
|
+
];
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function markdownLinkTargets(text) {
|
|
659
|
+
return [...text.matchAll(/!?\[[^\]\n]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)].map((match) => match[1]);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function markdownAnchors(file) {
|
|
663
|
+
if (!file.endsWith('.md')) return new Set();
|
|
664
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
665
|
+
const anchors = new Set();
|
|
666
|
+
const counts = new Map();
|
|
667
|
+
for (const match of text.matchAll(/^#{1,6}\s+(.+)$/gm)) {
|
|
668
|
+
const base = githubSlug(match[1]);
|
|
669
|
+
const count = counts.get(base) ?? 0;
|
|
670
|
+
counts.set(base, count + 1);
|
|
671
|
+
anchors.add(count === 0 ? base : `${base}-${count}`);
|
|
672
|
+
}
|
|
673
|
+
return anchors;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function githubSlug(heading) {
|
|
677
|
+
return heading
|
|
678
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
679
|
+
.replace(/<[^>]+>/g, '')
|
|
680
|
+
.trim()
|
|
681
|
+
.toLowerCase()
|
|
682
|
+
.replace(/[^\p{Letter}\p{Number}\s-]/gu, '')
|
|
683
|
+
.trim()
|
|
684
|
+
.replace(/\s+/g, '-');
|
|
585
685
|
}
|
|
586
686
|
|
|
587
687
|
function between(text, startMarker, endMarker) {
|
|
@@ -614,6 +714,16 @@ function assertNotIncludes(actual, expected, label) {
|
|
|
614
714
|
if (String(actual).includes(expected)) throw new Error(`${label} unexpectedly included ${format(expected)}\nactual: ${format(actual)}`);
|
|
615
715
|
}
|
|
616
716
|
|
|
717
|
+
function assertArrayEqual(actual, expected, label) {
|
|
718
|
+
const actualText = actual.join('\n');
|
|
719
|
+
const expectedText = expected.join('\n');
|
|
720
|
+
if (actualText !== expectedText) {
|
|
721
|
+
const onlyActual = actual.filter((item) => !expected.includes(item));
|
|
722
|
+
const onlyExpected = expected.filter((item) => !actual.includes(item));
|
|
723
|
+
throw new Error(`${label} mismatch\nonly actual: ${format(onlyActual)}\nonly expected: ${format(onlyExpected)}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
617
727
|
function format(value) {
|
|
618
728
|
return typeof value === 'string' ? JSON.stringify(value) : String(value);
|
|
619
729
|
}
|