eyelang 2.0.0 → 4.0.1
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 +6 -7
- package/docs/guide.md +6 -6
- package/docs/language-reference.md +116 -35
- package/examples/derived-rule.pl +1 -1
- package/examples/good-cobbler.pl +1 -1
- package/package.json +2 -2
- package/src/cli.js +2 -2
- package/src/parser.js +112 -26
- package/src/program.js +10 -0
- package/src/solver.js +269 -14
- package/test/conformance/cases/builtins/037_matching_and_comparison.pl +1 -1
- package/test/conformance/cases/strings/030_strings_and_atoms.pl +1 -1
- package/test/conformance/cases/strings/090_string_substring_replace_edges.pl +5 -5
- package/test/run-conformance.mjs +1 -1
- package/test/run-regression.mjs +3 -4
- package/src/bin.js +0 -7
- package/test/conformance/cases/aggregation/044_findall_user_goal.query +0 -1
- package/test/conformance/cases/aggregation/057_countall_empty_and_nonempty.query +0 -1
- package/test/conformance/cases/aggregation/058_sumall_numeric_template.query +0 -1
- package/test/conformance/cases/aggregation/059_aggregate_min_template.query +0 -1
- package/test/conformance/cases/aggregation/060_aggregate_max_compound_key.query +0 -1
- package/test/conformance/cases/arithmetic/029_arithmetic_and_comparison.query +0 -1
- package/test/conformance/cases/arithmetic/039_numeric_functions.query +0 -1
- package/test/conformance/cases/arithmetic/040_between_enumeration.query +0 -1
- package/test/conformance/cases/arithmetic/041_smallest_divisor.query +0 -1
- package/test/conformance/cases/arithmetic/054_numeric_and_lexical_comparison.query +0 -1
- package/test/conformance/cases/atoms/008_graphic_atoms.query +0 -1
- package/test/conformance/cases/atoms/018_quoted_atom_readback.query +0 -1
- package/test/conformance/cases/builtins/037_matching_and_comparison.query +0 -1
- package/test/conformance/cases/control/014_failure_filters_answers.query +0 -1
- package/test/conformance/cases/control/043_once_user_predicate.query +0 -1
- package/test/conformance/cases/lists/005_list_deconstruction.query +0 -1
- package/test/conformance/cases/lists/015_improper_list_unification.query +0 -1
- package/test/conformance/cases/lists/020_nested_list_terms.query +0 -1
- package/test/conformance/cases/lists/031_lists_aggregation_ordering.query +0 -1
- package/test/conformance/cases/lists/035_list_relations.query +0 -1
- package/test/conformance/cases/lists/036_append_splits.query +0 -1
- package/test/conformance/cases/lists/045_sort_deduplicates_atoms.query +0 -1
- package/test/conformance/cases/lists/046_append_bound_prefix_suffix.query +0 -1
- package/test/conformance/cases/lists/047_nth0_index_generation.query +0 -1
- package/test/conformance/cases/lists/048_set_nth0_edges.query +0 -1
- package/test/conformance/cases/lists/049_select_duplicate_occurrences.query +0 -1
- package/test/conformance/cases/negation/033_negation_once_generators.query +0 -1
- package/test/conformance/cases/negation/042_negation_filter.query +0 -1
- package/test/conformance/cases/negation/050_not_member_filter.query +0 -1
- package/test/conformance/cases/rules/002_rule_recursion.query +0 -1
- package/test/conformance/cases/rules/011_predicate_arity.query +0 -1
- package/test/conformance/cases/rules/013_multiple_clauses_order.query +0 -1
- package/test/conformance/cases/rules/017_three_step_recursion.query +0 -1
- package/test/conformance/cases/strings/030_strings_and_atoms.query +0 -1
- package/test/conformance/cases/strings/055_string_matching_filters.query +0 -1
- package/test/conformance/cases/strings/056_string_and_atom_concat.query +0 -1
- package/test/conformance/cases/syntax/004_conjunction_and_parentheses.query +0 -1
- package/test/conformance/cases/syntax/006_comma_formula_data.query +0 -1
- package/test/conformance/cases/syntax/009_comments_and_whitespace.query +0 -1
- package/test/conformance/cases/syntax/019_parenthesized_three_conjuncts.query +0 -1
- package/test/conformance/cases/table/038_table_declaration.query +0 -1
- package/test/conformance/cases/terms/003_terms_and_readback.query +0 -1
- package/test/conformance/cases/unification/012_nested_compound_unification.query +0 -1
- package/test/conformance/cases/unification/034_equality_and_inequality.query +0 -1
- package/test/conformance/cases/variables/007_anonymous_variables.query +0 -1
- package/test/conformance/cases/variables/010_variable_scope_and_reuse.query +0 -1
package/README.md
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/eyelang)
|
|
4
4
|
[](https://doi.org/10.5281/zenodo.20761726)
|
|
5
5
|
|
|
6
|
-
Eyelang is a small
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
Eyelang is a small reasoning language for turning facts and rules into answers and proofs.
|
|
7
|
+
|
|
8
|
+
Prolog-like syntax. Small core. Inspectable results.
|
|
9
9
|
|
|
10
10
|
## Install and run
|
|
11
11
|
|
|
@@ -14,15 +14,15 @@ Eyelang has no runtime npm dependencies and no build step. From a source checkou
|
|
|
14
14
|
```bash
|
|
15
15
|
node bin/eyelang.js examples/ancestor.pl
|
|
16
16
|
node bin/eyelang.js --proof examples/socrates.pl
|
|
17
|
-
node bin/eyelang.js --warnings
|
|
17
|
+
node bin/eyelang.js --warnings test/conformance/warnings/negation/unstratified_mutual.pl
|
|
18
18
|
printf 'works(stdin, true) :- eq(ok, ok).\n' | node bin/eyelang.js -
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
For one-off local CLI use from the checkout, npm can run the package bin without a manual symlink:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
npm exec -- eyelang --version
|
|
25
|
-
npm exec -- eyelang examples/ancestor.pl
|
|
24
|
+
npm exec --yes --package=. -- eyelang --version
|
|
25
|
+
npm exec --yes --package=. -- eyelang examples/ancestor.pl
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
To install the checkout's `eyelang` command on your `PATH`, use npm's package link:
|
|
@@ -68,4 +68,3 @@ npm run conformance:report
|
|
|
68
68
|
npm run test:examples
|
|
69
69
|
npm run test:regression
|
|
70
70
|
```
|
|
71
|
-
|
package/docs/guide.md
CHANGED
|
@@ -36,8 +36,8 @@ printf 'works(stdin, true) :- eq(ok, ok).\n' | node bin/eyelang.js -
|
|
|
36
36
|
You can also use npm's local package-bin runner from the checkout:
|
|
37
37
|
|
|
38
38
|
```sh
|
|
39
|
-
npm exec -- eyelang --version
|
|
40
|
-
npm exec -- eyelang examples/ancestor.pl
|
|
39
|
+
npm exec --yes --package=. -- eyelang --version
|
|
40
|
+
npm exec --yes --package=. -- eyelang examples/ancestor.pl
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
To make the `eyelang` command available on your `PATH` while developing this checkout, prefer npm's package link instead of a manual symlink:
|
|
@@ -51,7 +51,7 @@ eyelang --version
|
|
|
51
51
|
|
|
52
52
|
## Running eyelang
|
|
53
53
|
|
|
54
|
-
The commands in this section use `eyelang` for readability. In a source checkout where you have not run `npm link` or `npm install -g .`, replace `eyelang` with `node bin/eyelang.js`, or run the command through `npm exec -- eyelang`.
|
|
54
|
+
The commands in this section use `eyelang` for readability. In a source checkout where you have not run `npm link` or `npm install -g .`, replace `eyelang` with `node bin/eyelang.js`, or run the command through `npm exec --yes --package=. -- eyelang`.
|
|
55
55
|
|
|
56
56
|
Show the package version:
|
|
57
57
|
|
|
@@ -246,7 +246,7 @@ eyelang -s examples/observability-log-correlation.pl
|
|
|
246
246
|
Add `-w` or `--warnings` when you want non-fatal portability diagnostics, such as unstratified `not/1` dependencies, printed to stderr while normal answer output still goes to stdout:
|
|
247
247
|
|
|
248
248
|
```sh
|
|
249
|
-
eyelang --warnings
|
|
249
|
+
eyelang --warnings test/conformance/warnings/negation/unstratified_mutual.pl
|
|
250
250
|
```
|
|
251
251
|
|
|
252
252
|
The playground has matching `--stats` and `--proof` checkboxes, so browser runs can show the same counters or explanations like the CLI.
|
|
@@ -573,7 +573,7 @@ A useful rule of thumb:
|
|
|
573
573
|
| Human-auditable derivations | Either | Both can emit proof explanations when requested. |
|
|
574
574
|
| Large generated Horn-clause workloads | eyelang | The engine specializes in predicate/arity indexing, scalar argument indexes, fast fact paths, and materialized output goals. |
|
|
575
575
|
|
|
576
|
-
On local smoke benchmarks, eyelang is substantially faster on large generated Horn-clause and recursion-heavy workloads. These numbers are 5-run medians with stdout redirected to `/dev/null`, using Node.js `v22.16.0`, eyelang from this checkout, and Eyeling package version `1.34.6` with its default output mode. The ratio is `Eyeling median / eyelang median`, so larger numbers
|
|
576
|
+
On local smoke benchmarks, eyelang is substantially faster on large generated Horn-clause and recursion-heavy workloads. These numbers are 5-run medians with stdout redirected to `/dev/null`, using Node.js `v22.16.0`, eyelang from this checkout, and Eyeling package version `1.34.6` with its default output mode. The ratio is `Eyeling median / eyelang median`, so larger numbers mea eyelang was faster.
|
|
577
577
|
|
|
578
578
|
| Example | eyelang median | Eyeling median | Ratio |
|
|
579
579
|
| --- | ---: | ---: | ---: |
|
|
@@ -618,4 +618,4 @@ When using `not/1` over user-defined predicates, keep the dependency graph strat
|
|
|
618
618
|
|
|
619
619
|
## Implementation limits
|
|
620
620
|
|
|
621
|
-
Eyelang is intentionally smaller than ISO Prolog. It has no operators, zero-arity compound syntax, cut, modules, dynamic database updates, DCGs, or complete ISO library. Arity-zero data is always written and read back as an atom, such as `nil`, never `nil()`. Negation is negation-as-failure through `not/1`. Search is goal-directed and expected to be finite for the selected output goals. Output explanations are non-normative proof printouts and do not change answer semantics.
|
|
621
|
+
Eyelang is intentionally smaller than ISO Prolog. It has no operators, zero-arity compound syntax, cut, modules, dynamic database updates, DCGs, or complete ISO library. Arity-zero data is always written and read back as an atom, such as `nil`, never `nil()`. Negation is negation-as-failure through `not/1`. Search is goal-directed and expected to be finite for the selected output goals. Output explanations are non-normative proof printouts and do not change answer semantics.
|
|
@@ -7,13 +7,16 @@
|
|
|
7
7
|
- [2. Design goals](#2-design-goals)
|
|
8
8
|
- [3. Lexical structure](#3-lexical-structure)
|
|
9
9
|
- [3.1 Character stream](#31-character-stream)
|
|
10
|
-
- [3.2
|
|
11
|
-
- [3.3
|
|
12
|
-
- [3.4
|
|
13
|
-
- [3.5
|
|
14
|
-
- [3.6
|
|
15
|
-
- [3.7
|
|
10
|
+
- [3.2 Unicode and UTF-8](#32-unicode-and-utf-8)
|
|
11
|
+
- [3.3 Comments](#33-comments)
|
|
12
|
+
- [3.4 Punctuation tokens](#34-punctuation-tokens)
|
|
13
|
+
- [3.5 Variables](#35-variables)
|
|
14
|
+
- [3.6 Atom constants](#36-atom-constants)
|
|
15
|
+
- [3.7 Strings](#37-strings)
|
|
16
|
+
- [3.8 Numbers](#38-numbers)
|
|
16
17
|
- [4. Surface grammar](#4-surface-grammar)
|
|
18
|
+
- [4.1 EBNF grammar](#41-ebnf-grammar)
|
|
19
|
+
- [4.2 Grammar notes](#42-grammar-notes)
|
|
17
20
|
- [5. Terms](#5-terms)
|
|
18
21
|
- [5.1 Variables](#51-variables)
|
|
19
22
|
- [5.2 Atom constants, strings, and numbers](#52-atom-constants-strings-and-numbers)
|
|
@@ -100,19 +103,32 @@ Non-goals include complete ISO Prolog compatibility, operator declarations, modu
|
|
|
100
103
|
## 3. Lexical structure
|
|
101
104
|
|
|
102
105
|
### 3.1 Character stream
|
|
103
|
-
|
|
104
106
|
Input is Unicode text. Whitespace separates tokens and is otherwise insignificant outside quoted strings and quoted atom constants.
|
|
105
107
|
|
|
106
|
-
### 3.2 Comments
|
|
107
108
|
|
|
109
|
+
### 3.2 Unicode and UTF-8
|
|
110
|
+
Eyelang source files are UTF-8.
|
|
111
|
+
|
|
112
|
+
Quoted atoms and strings may contain Unicode text:
|
|
113
|
+
|
|
114
|
+
```eyelang
|
|
115
|
+
name(alice, 'Élodie').
|
|
116
|
+
city('München').
|
|
117
|
+
message("café").
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Unquoted atoms and variables intentionally use ASCII syntax. This keeps programs portable and makes the Prolog-style distinction between atoms and variables clear: lowercase names are atoms, while uppercase and underscore names are variables.
|
|
121
|
+
|
|
122
|
+
Use quoted atoms for non-ASCII names.
|
|
123
|
+
|
|
124
|
+
### 3.3 Comments
|
|
108
125
|
A percent sign starts a line comment outside quoted strings and quoted atom constants. The comment extends to the end of the line.
|
|
109
126
|
|
|
110
127
|
```eyelang
|
|
111
128
|
parent(pat, jan). % this is a comment
|
|
112
129
|
```
|
|
113
130
|
|
|
114
|
-
### 3.
|
|
115
|
-
|
|
131
|
+
### 3.4 Punctuation tokens
|
|
116
132
|
The punctuation tokens are:
|
|
117
133
|
|
|
118
134
|
```text
|
|
@@ -121,8 +137,7 @@ The punctuation tokens are:
|
|
|
121
137
|
|
|
122
138
|
A colon outside `:-` is not part of the language. Namespace-like names SHOULD be written as explicit atom constants such as `person_type`, `odrl_permission`, or quoted atoms such as `'org.schema'`.
|
|
123
139
|
|
|
124
|
-
### 3.
|
|
125
|
-
|
|
140
|
+
### 3.5 Variables
|
|
126
141
|
A variable is either the bare anonymous variable `_`, or starts with an uppercase ASCII letter or underscore and then zero or more ASCII letters, digits, or underscores. This is the source-level variable spelling used by ISO Prolog.
|
|
127
142
|
|
|
128
143
|
Examples:
|
|
@@ -136,8 +151,7 @@ _
|
|
|
136
151
|
|
|
137
152
|
Each bare `_` anonymous variable occurrence is fresh. A name such as `_thing` is a named variable and is reused within its clause.
|
|
138
153
|
|
|
139
|
-
### 3.
|
|
140
|
-
|
|
154
|
+
### 3.6 Atom constants
|
|
141
155
|
A plain atom constant starts with a lowercase ASCII letter and is followed by zero or more ASCII letters, digits, or underscores. A dot is not part of a plain atom; dotted web spaces such as `'be.ugent'` or `'org.schema'` MUST be quoted if they are meant as one atom constant. Names such as `a-b` MUST also be quoted if they are meant as one atom constant:
|
|
142
156
|
|
|
143
157
|
```eyelang
|
|
@@ -176,12 +190,10 @@ A graphic atom constant is one or more graphic characters from this set:
|
|
|
176
190
|
|
|
177
191
|
Graphic atoms such as `<=>`, `<`, and `>=` remain graphic atoms. Longer names containing letters or punctuation outside the graphic set should be quoted, for example `'<abc>'`.
|
|
178
192
|
|
|
179
|
-
### 3.
|
|
180
|
-
|
|
193
|
+
### 3.7 Strings
|
|
181
194
|
A string is enclosed in double quotes. The implementation supports common escapes such as `\n`, `\t`, `\"`, and `\\`.
|
|
182
195
|
|
|
183
|
-
### 3.
|
|
184
|
-
|
|
196
|
+
### 3.8 Numbers
|
|
185
197
|
Numbers are scalar terms. Integers, decimal numbers, and scientific notation are accepted:
|
|
186
198
|
|
|
187
199
|
```eyelang
|
|
@@ -196,33 +208,103 @@ Integer arithmetic built-ins use arbitrary-precision decimal strings where possi
|
|
|
196
208
|
|
|
197
209
|
## 4. Surface grammar
|
|
198
210
|
|
|
199
|
-
This grammar
|
|
211
|
+
This section gives the portable source grammar in EBNF. Lexical tokens are defined in section 3. Whitespace and comments may appear between tokens and are otherwise ignored.
|
|
212
|
+
|
|
213
|
+
### 4.1 EBNF grammar
|
|
200
214
|
|
|
201
215
|
```text
|
|
202
|
-
program
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
216
|
+
program ::= { clause } ;
|
|
217
|
+
|
|
218
|
+
clause ::= head, "."
|
|
219
|
+
| head, ":-", goal-list, "." ;
|
|
220
|
+
|
|
221
|
+
head ::= term ;
|
|
222
|
+
|
|
223
|
+
goal-list ::= term, { ",", term } ;
|
|
224
|
+
|
|
225
|
+
term ::= variable
|
|
226
|
+
| atom-constant
|
|
227
|
+
| string
|
|
228
|
+
| number
|
|
229
|
+
| compound
|
|
230
|
+
| list
|
|
231
|
+
| parenthesized-term ;
|
|
232
|
+
|
|
233
|
+
compound ::= atom-constant, "(", term, { ",", term }, ")" ;
|
|
234
|
+
|
|
235
|
+
list ::= "[", "]"
|
|
236
|
+
| "[", list-items, "]" ;
|
|
237
|
+
|
|
238
|
+
list-items ::= term, { ",", term }, [ "|", term ] ;
|
|
239
|
+
|
|
240
|
+
parenthesized-term ::= "(", term, [ ",", term, { ",", term } ], ")" ;
|
|
241
|
+
|
|
242
|
+
variable ::= "_"
|
|
243
|
+
| variable-start, { name-continue } ;
|
|
244
|
+
|
|
245
|
+
atom-constant ::= plain-atom
|
|
246
|
+
| quoted-atom
|
|
247
|
+
| graphic-atom ;
|
|
248
|
+
|
|
249
|
+
plain-atom ::= lowercase-letter, { name-continue } ;
|
|
250
|
+
|
|
251
|
+
quoted-atom ::= "'", { quoted-atom-char }, "'" ;
|
|
252
|
+
|
|
253
|
+
quoted-atom-char ::= non-single-quote-char
|
|
254
|
+
| "''"
|
|
255
|
+
| escape-sequence ;
|
|
256
|
+
|
|
257
|
+
string ::= '"', { string-char }, '"' ;
|
|
258
|
+
|
|
259
|
+
string-char ::= non-double-quote-char
|
|
260
|
+
| '""'
|
|
261
|
+
| escape-sequence ;
|
|
262
|
+
|
|
263
|
+
number ::= [ "-" ], digits, [ ".", digits ], [ exponent ] ;
|
|
264
|
+
|
|
265
|
+
exponent ::= ( "e" | "E" ), [ "+" | "-" ], digits ;
|
|
266
|
+
|
|
267
|
+
graphic-atom ::= graphic-char, { graphic-char } ;
|
|
268
|
+
|
|
269
|
+
variable-start ::= uppercase-letter | "_" ;
|
|
270
|
+
|
|
271
|
+
name-continue ::= uppercase-letter | lowercase-letter | digit | "_" ;
|
|
272
|
+
|
|
273
|
+
digits ::= digit, { digit } ;
|
|
274
|
+
|
|
275
|
+
escape-sequence ::= "\\", any-char ;
|
|
276
|
+
|
|
277
|
+
uppercase-letter ::= "A" | ... | "Z" ;
|
|
278
|
+
lowercase-letter ::= "a" | ... | "z" ;
|
|
279
|
+
digit ::= "0" | ... | "9" ;
|
|
280
|
+
|
|
281
|
+
non-single-quote-char ::= any source character except "'", "\\", or end of input ;
|
|
282
|
+
non-double-quote-char ::= any source character except '"', "\\", or end of input ;
|
|
283
|
+
any-char ::= any source character except end of input ;
|
|
284
|
+
|
|
285
|
+
graphic-char ::= "#" | "$" | "&" | "*" | "+" | "-" | "/" | "<"
|
|
286
|
+
| "=" | ">" | "@" | "^" | "~" | "\\" ;
|
|
213
287
|
```
|
|
214
288
|
|
|
215
|
-
|
|
289
|
+
### 4.2 Grammar notes
|
|
216
290
|
|
|
217
|
-
|
|
291
|
+
The `atom-constant` nonterminal is a lexical class for symbolic scalar terms, not an atomic formula. Atomic formulas are represented by the `compound` alternative when such a term appears as a clause head, rule body, or selected goal. The functor or predicate name is always an atom constant; Eyelang does not support variables in functor or predicate position.
|
|
218
292
|
|
|
219
|
-
|
|
293
|
+
A portable clause head SHOULD be a compound term. Non-compound heads are parsed, but they are not useful in the current predicate index.
|
|
294
|
+
|
|
295
|
+
Compound syntax always has at least one argument. Arity-zero data is written as an atom constant, not as a zero-arity compound:
|
|
220
296
|
|
|
221
297
|
```eyelang
|
|
222
298
|
value(example, nil).
|
|
223
299
|
```
|
|
224
300
|
|
|
225
|
-
The syntax `nil()` is intentionally rejected so
|
|
301
|
+
The syntax `nil()` is intentionally rejected so Eyelang source and read-back output use one representation for arity-zero data. Host APIs SHOULD follow the same rule: constructing a term with an atom name and an empty argument list is canonicalized to the atom constant itself.
|
|
302
|
+
|
|
303
|
+
Parentheses around a single term are accepted and denote that same term. Parentheses around two or more comma-separated terms denote a right-associated comma term using the functor `','/2`. When such a comma term appears as a goal, it is evaluated as conjunction.
|
|
304
|
+
|
|
305
|
+
A quoted atom represents an atom constant. A doubled single quote inside a quoted atom represents one literal single quote. A backslash may introduce an escaped character in quoted atoms and strings. Portable source SHOULD use the common escapes `\\`, `\n`, `\t`, `\'` in quoted atoms, and `\\`, `\n`, `\t`, `\"` in strings.
|
|
306
|
+
|
|
307
|
+
A percent sign outside a quoted atom or string starts a line comment that extends to the end of the line.
|
|
226
308
|
|
|
227
309
|
## 5. Terms
|
|
228
310
|
|
|
@@ -770,4 +852,3 @@ status(a, open) :- open(a).
|
|
|
770
852
|
URL input uses host networking support when available. Hosts SHOULD treat downloaded programs as untrusted code because they can trigger expensive search.
|
|
771
853
|
|
|
772
854
|
Programs SHOULD be written with finite search in mind. Broad default materialization can be expensive for helper predicates; use `materialize/2` declarations and concise output predicates when needed.
|
|
773
|
-
|
package/examples/derived-rule.pl
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
% { X a cat. } => { { Y a dog. } => { test is true. }. }.
|
|
7
7
|
%
|
|
8
8
|
% The inner implication is represented directly as quoted formula data.
|
|
9
|
-
% var(y) is not
|
|
9
|
+
% var(y) is not a eyelang variable; it is a ground term that names
|
|
10
10
|
% a variable placeholder inside the quoted formula.
|
|
11
11
|
|
|
12
12
|
% Output declarations: materialize/2 selects the relations written to this example's golden output.
|
package/examples/good-cobbler.pl
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
% Good cobbler, adapted from Eyeling's examples/good-cobbler.n3.
|
|
2
2
|
%
|
|
3
3
|
% The Eyeling result is a quoted assertion saying that joe is a good Cobbler.
|
|
4
|
-
% Here the quoted assertion is represented as
|
|
4
|
+
% Here the quoted assertion is represented as a eyelang term.
|
|
5
5
|
|
|
6
6
|
% Output declarations: materialize/2 selects the relations written to this example's golden output.
|
|
7
7
|
materialize(is, 2).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyelang",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "A small
|
|
3
|
+
"version": "4.0.1",
|
|
4
|
+
"description": "A small reasoning language with ISO Prolog-compatible surface syntax.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"types": "./index.d.ts",
|
package/src/cli.js
CHANGED
|
@@ -139,8 +139,8 @@ Usage:
|
|
|
139
139
|
eyelang [options] [file-or-url.pl|- ...]
|
|
140
140
|
|
|
141
141
|
Input:
|
|
142
|
-
file-or-url.pl
|
|
143
|
-
- Read
|
|
142
|
+
file-or-url.pl Read a Eyelang program from a local file or http(s) URL.
|
|
143
|
+
- Read a Eyelang program from standard input.
|
|
144
144
|
|
|
145
145
|
Options:
|
|
146
146
|
-h, --help Show this help text and exit.
|
package/src/parser.js
CHANGED
|
@@ -335,6 +335,7 @@ function parseClausesFastNoSource(source) {
|
|
|
335
335
|
cache.set(key, value);
|
|
336
336
|
return value;
|
|
337
337
|
};
|
|
338
|
+
|
|
338
339
|
const isFastScalarToken = (text) => SIMPLE_VARIABLE.test(text) || SIMPLE_ATOM.test(text) || GRAPHIC_ATOM.test(text) || SIMPLE_NUMBER.test(text);
|
|
339
340
|
const scalarOrVariableFast = (text) => {
|
|
340
341
|
if (!text || !isFastScalarToken(text)) throw new Error('bad simple term');
|
|
@@ -350,8 +351,117 @@ function parseClausesFastNoSource(source) {
|
|
|
350
351
|
if ((first === 45 || isDigitCode(first)) && SIMPLE_NUMBER.test(text)) return cached(numberCache, text, numberTerm);
|
|
351
352
|
return atom(text);
|
|
352
353
|
};
|
|
354
|
+
|
|
355
|
+
const trimRange = (text, start, end) => {
|
|
356
|
+
while (start < end && isWhitespaceCode(text.charCodeAt(start))) start++;
|
|
357
|
+
while (end > start && isWhitespaceCode(text.charCodeAt(end - 1))) end--;
|
|
358
|
+
return [start, end];
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const tokenKindInRange = (text, start, end) => {
|
|
362
|
+
if (start >= end) return null;
|
|
363
|
+
const first = text.charCodeAt(start);
|
|
364
|
+
if (first === 95 || (first >= 65 && first <= 90)) {
|
|
365
|
+
for (let i = start + 1; i < end; i++) if (!isNameContinueCode(text.charCodeAt(i))) return null;
|
|
366
|
+
return 'var';
|
|
367
|
+
}
|
|
368
|
+
if (first >= 97 && first <= 122) {
|
|
369
|
+
for (let i = start + 1; i < end; i++) if (!isNameContinueCode(text.charCodeAt(i))) return null;
|
|
370
|
+
return 'atom';
|
|
371
|
+
}
|
|
372
|
+
let allGraphic = true;
|
|
373
|
+
for (let i = start; i < end; i++) {
|
|
374
|
+
if (!isGraphicAtomCode(text.charCodeAt(i))) { allGraphic = false; break; }
|
|
375
|
+
}
|
|
376
|
+
if (allGraphic) return 'atom';
|
|
377
|
+
return null;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const simpleNumberInRange = (text, start, end) => {
|
|
381
|
+
let i = start;
|
|
382
|
+
if (text.charCodeAt(i) === 45) i++;
|
|
383
|
+
if (i >= end || !isDigitCode(text.charCodeAt(i))) return false;
|
|
384
|
+
while (i < end && isDigitCode(text.charCodeAt(i))) i++;
|
|
385
|
+
if (i < end && text.charCodeAt(i) === 46) {
|
|
386
|
+
i++;
|
|
387
|
+
if (i >= end || !isDigitCode(text.charCodeAt(i))) return false;
|
|
388
|
+
while (i < end && isDigitCode(text.charCodeAt(i))) i++;
|
|
389
|
+
}
|
|
390
|
+
if (i < end && (text.charCodeAt(i) === 101 || text.charCodeAt(i) === 69)) {
|
|
391
|
+
i++;
|
|
392
|
+
if (i < end && (text.charCodeAt(i) === 43 || text.charCodeAt(i) === 45)) i++;
|
|
393
|
+
if (i >= end || !isDigitCode(text.charCodeAt(i))) return false;
|
|
394
|
+
while (i < end && isDigitCode(text.charCodeAt(i))) i++;
|
|
395
|
+
}
|
|
396
|
+
return i === end;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const scalarOrVariableRange = (text, start, end) => {
|
|
400
|
+
[start, end] = trimRange(text, start, end);
|
|
401
|
+
const kind = tokenKindInRange(text, start, end);
|
|
402
|
+
const value = text.slice(start, end);
|
|
403
|
+
if (kind === 'var') {
|
|
404
|
+
if (value === '_') return variable(`__anon${anonymous++}`);
|
|
405
|
+
const existing = variableCache.get(value);
|
|
406
|
+
if (existing) return existing;
|
|
407
|
+
const term = variable(value);
|
|
408
|
+
variableCache.set(value, term);
|
|
409
|
+
return term;
|
|
410
|
+
}
|
|
411
|
+
if (kind === 'atom') return atom(value);
|
|
412
|
+
if (simpleNumberInRange(text, start, end)) return cached(numberCache, value, numberTerm);
|
|
413
|
+
return null;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const parseBinaryCompoundRange = (text, start = 0, end = text.length) => {
|
|
417
|
+
[start, end] = trimRange(text, start, end);
|
|
418
|
+
let i = start;
|
|
419
|
+
const first = text.charCodeAt(i);
|
|
420
|
+
if (!(first >= 97 && first <= 122)) return null;
|
|
421
|
+
i++;
|
|
422
|
+
while (i < end && isNameContinueCode(text.charCodeAt(i))) i++;
|
|
423
|
+
const nameEnd = i;
|
|
424
|
+
while (i < end && isWhitespaceCode(text.charCodeAt(i))) i++;
|
|
425
|
+
if (text.charCodeAt(i) !== 40) return null;
|
|
426
|
+
i++;
|
|
427
|
+
const arg1Start = i;
|
|
428
|
+
while (i < end && text.charCodeAt(i) !== 44 && text.charCodeAt(i) !== 40 && text.charCodeAt(i) !== 41 && text.charCodeAt(i) !== 91 && text.charCodeAt(i) !== 93 && text.charCodeAt(i) !== 124 && text.charCodeAt(i) !== 34 && text.charCodeAt(i) !== 39) i++;
|
|
429
|
+
if (i >= end || text.charCodeAt(i) !== 44) return null;
|
|
430
|
+
const arg1End = i;
|
|
431
|
+
i++;
|
|
432
|
+
const arg2Start = i;
|
|
433
|
+
while (i < end && text.charCodeAt(i) !== 41 && text.charCodeAt(i) !== 40 && text.charCodeAt(i) !== 44 && text.charCodeAt(i) !== 91 && text.charCodeAt(i) !== 93 && text.charCodeAt(i) !== 124 && text.charCodeAt(i) !== 34 && text.charCodeAt(i) !== 39) i++;
|
|
434
|
+
if (i >= end || text.charCodeAt(i) !== 41) return null;
|
|
435
|
+
const arg2End = i;
|
|
436
|
+
i++;
|
|
437
|
+
while (i < end && isWhitespaceCode(text.charCodeAt(i))) i++;
|
|
438
|
+
if (i !== end) return null;
|
|
439
|
+
const left = scalarOrVariableRange(text, arg1Start, arg1End);
|
|
440
|
+
if (!left) return null;
|
|
441
|
+
const right = scalarOrVariableRange(text, arg2Start, arg2End);
|
|
442
|
+
if (!right) return null;
|
|
443
|
+
return compound(text.slice(start, nameEnd), [left, right]);
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const parseFastLine = (text) => {
|
|
447
|
+
if (!text.endsWith('.')) return null;
|
|
448
|
+
const end = text.length - 1;
|
|
449
|
+
const rule = text.indexOf(':-');
|
|
450
|
+
if (rule < 0) {
|
|
451
|
+
const head = parseBinaryCompoundRange(text, 0, end);
|
|
452
|
+
return head ? { head, body: [] } : null;
|
|
453
|
+
}
|
|
454
|
+
if (text.indexOf(':-', rule + 2) >= 0) return null;
|
|
455
|
+
const head = parseBinaryCompoundRange(text, 0, rule);
|
|
456
|
+
if (!head) return null;
|
|
457
|
+
const bodyGoal = parseBinaryCompoundRange(text, rule + 2, end);
|
|
458
|
+
return bodyGoal ? { head, body: [bodyGoal] } : null;
|
|
459
|
+
};
|
|
460
|
+
|
|
353
461
|
const scalarOrVariable = (text) => scalarOrVariableFast(text.trim());
|
|
354
462
|
const parseBinaryCompound = (text) => {
|
|
463
|
+
const parsed = parseBinaryCompoundRange(text, 0, text.length);
|
|
464
|
+
if (parsed) return parsed;
|
|
355
465
|
text = text.trim();
|
|
356
466
|
const open = text.indexOf('(');
|
|
357
467
|
if (open <= 0 || text[text.length - 1] !== ')') return null;
|
|
@@ -366,35 +476,11 @@ function parseClausesFastNoSource(source) {
|
|
|
366
476
|
if (!isFastScalarToken(left) || !isFastScalarToken(right)) return null;
|
|
367
477
|
return compound(name, [scalarOrVariable(left), scalarOrVariable(right)]);
|
|
368
478
|
};
|
|
369
|
-
|
|
370
|
-
if (!isFastScalarToken(match[2]) || !isFastScalarToken(match[3])) return null;
|
|
371
|
-
return compound(match[1], [scalarOrVariableFast(match[2]), scalarOrVariableFast(match[3])]);
|
|
372
|
-
};
|
|
373
|
-
const parseFastBinaryRuleMatch = (match) => {
|
|
374
|
-
if (!isFastScalarToken(match[2]) || !isFastScalarToken(match[3]) || !isFastScalarToken(match[5]) || !isFastScalarToken(match[6])) return null;
|
|
375
|
-
return {
|
|
376
|
-
head: compound(match[1], [scalarOrVariableFast(match[2]), scalarOrVariableFast(match[3])]),
|
|
377
|
-
body: [compound(match[4], [scalarOrVariableFast(match[5]), scalarOrVariableFast(match[6])])],
|
|
378
|
-
};
|
|
379
|
-
};
|
|
380
|
-
const parseFastLine = (text) => {
|
|
381
|
-
if (!text.endsWith('.')) return null;
|
|
382
|
-
const ruleMatch = FAST_BINARY_RULE.exec(text);
|
|
383
|
-
if (ruleMatch) {
|
|
384
|
-
const parsed = parseFastBinaryRuleMatch(ruleMatch);
|
|
385
|
-
if (parsed) return parsed;
|
|
386
|
-
}
|
|
387
|
-
const factMatch = FAST_BINARY_FACT.exec(text);
|
|
388
|
-
if (factMatch) {
|
|
389
|
-
const head = parseFastBinaryMatch(factMatch);
|
|
390
|
-
if (head) return { head, body: [] };
|
|
391
|
-
}
|
|
392
|
-
return null;
|
|
393
|
-
};
|
|
479
|
+
|
|
394
480
|
const parseSimple = (text) => {
|
|
395
|
-
if (!text.endsWith('.') || text.includes('\n')) return null;
|
|
396
481
|
const fast = parseFastLine(text);
|
|
397
482
|
if (fast) return fast;
|
|
483
|
+
if (!text.endsWith('.') || text.includes('\n')) return null;
|
|
398
484
|
text = text.slice(0, -1);
|
|
399
485
|
const rule = text.indexOf(':-');
|
|
400
486
|
if (rule < 0) {
|
package/src/program.js
CHANGED
|
@@ -44,6 +44,7 @@ export class Program {
|
|
|
44
44
|
mode: null,
|
|
45
45
|
determinism: null,
|
|
46
46
|
recursive: false,
|
|
47
|
+
scalarFactsOnly: true,
|
|
47
48
|
negationStratum: null,
|
|
48
49
|
};
|
|
49
50
|
if (arity > 2) {
|
|
@@ -64,6 +65,9 @@ export class Program {
|
|
|
64
65
|
group = this.makeGroup(head.name, head.arity);
|
|
65
66
|
this.groups.set(key, group);
|
|
66
67
|
}
|
|
68
|
+
clause.groundHead = termHasNoVariables(head);
|
|
69
|
+
clause.scalarHead = head.type === COMPOUND && head.args.every(isScalar);
|
|
70
|
+
if (clause.body.length !== 0 || !clause.scalarHead) group.scalarFactsOnly = false;
|
|
67
71
|
group.clauses.push(clause);
|
|
68
72
|
for (let i = 0; i < head.arity; i++) indexOne(group.argIndexes[i], head.args[i], clause);
|
|
69
73
|
for (const pair of group.pairIndexes) indexPair(pair, head, clause);
|
|
@@ -265,6 +269,12 @@ export class Program {
|
|
|
265
269
|
|
|
266
270
|
|
|
267
271
|
|
|
272
|
+
|
|
273
|
+
function termHasNoVariables(term) {
|
|
274
|
+
if (!term || term.type === 'var') return false;
|
|
275
|
+
return !term.args?.some((arg) => !termHasNoVariables(arg));
|
|
276
|
+
}
|
|
277
|
+
|
|
268
278
|
function collectGoalDependencies(goal, negated) {
|
|
269
279
|
if (goal.type !== COMPOUND) return [];
|
|
270
280
|
if (goal.name === ',' && goal.arity === 2) {
|
package/src/solver.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Depth-first eyelang solver with builtin dispatch, memoization, and guarded recursion handling.
|
|
2
2
|
// Most semantic decisions still flow through unification; optimizations only select candidates earlier.
|
|
3
|
-
import { COMPOUND, Env, copyResolved, flattenConjunction, freshTerm, termIsGround, termToString, unify, variantTerms } from './term.js';
|
|
3
|
+
import { COMPOUND, Env, compound, copyResolved, flattenConjunction, freshTerm, termIsGround, termToString, unify, variantTerms } from './term.js';
|
|
4
4
|
import { createDefaultRegistry } from './builtins/registry.js';
|
|
5
5
|
import { selectClauseCandidates } from './program.js';
|
|
6
6
|
|
|
@@ -156,6 +156,7 @@ export class Solver {
|
|
|
156
156
|
}
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
if (!group.tabled && tryPushScalarFactRunFrames(stack, this, [goal, ...rest], env, depth, active)) break;
|
|
159
160
|
pushUserGoalUncachedFrames(stack, this, group, goal, rest, env, depth, active);
|
|
160
161
|
break;
|
|
161
162
|
}
|
|
@@ -230,6 +231,14 @@ export class Solver {
|
|
|
230
231
|
const candidates = selectClauseCandidates(group, goal, env);
|
|
231
232
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
232
233
|
for (const clause of pass) {
|
|
234
|
+
if (clause.body.length === 0 && clause.scalarHead) {
|
|
235
|
+
const next = matchScalarFact(goal, clause.head, env);
|
|
236
|
+
if (!next) continue;
|
|
237
|
+
this.stats.unify_calls++;
|
|
238
|
+
yield* this.solve(rest, next, depth + 1);
|
|
239
|
+
if (this.solutionsSeen >= this.solutionLimit) return;
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
233
242
|
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
234
243
|
const id = nextFreshId();
|
|
235
244
|
const freshHead = freshTerm(clause.head, id);
|
|
@@ -285,6 +294,14 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
285
294
|
const frames = [];
|
|
286
295
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
287
296
|
for (const clause of pass) {
|
|
297
|
+
if (clause.body.length === 0 && clause.scalarHead) {
|
|
298
|
+
const next = matchScalarFact(goal, clause.head, env);
|
|
299
|
+
if (next) {
|
|
300
|
+
solver.stats.unify_calls++;
|
|
301
|
+
frames.push({ kind: 'goals', goals: rest, env: next, depth: depth + 1, active });
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
288
305
|
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
289
306
|
const id = nextFreshId();
|
|
290
307
|
const freshHead = freshTerm(clause.head, id);
|
|
@@ -309,6 +326,184 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
309
326
|
}
|
|
310
327
|
|
|
311
328
|
|
|
329
|
+
|
|
330
|
+
const SCALAR_FACT_RUN_FRAME_LIMIT = 100000;
|
|
331
|
+
|
|
332
|
+
function tryPushScalarFactRunFrames(stack, solver, goals, env, depth, active) {
|
|
333
|
+
// Consecutive lookups into predicates that are entirely scalar ground facts
|
|
334
|
+
// are common in data-heavy joins. Execute such a prefix as one iterative join
|
|
335
|
+
// using local binding arrays, so intermediate fact candidates do not allocate
|
|
336
|
+
// cloned Env maps.
|
|
337
|
+
let runLength = 0;
|
|
338
|
+
const groups = [];
|
|
339
|
+
while (runLength < goals.length) {
|
|
340
|
+
const goal = goals[runLength];
|
|
341
|
+
if (!goal || goal.kind === 'releaseActive' || goal.kind === 'memoStore') break;
|
|
342
|
+
if (goal.type !== COMPOUND) break;
|
|
343
|
+
const def = solver.registry.get(goal.name, goal.arity);
|
|
344
|
+
if (def) break;
|
|
345
|
+
const group = solver.program.findGroup(goal.name, goal.arity);
|
|
346
|
+
if (!group || group.tabled || !group.scalarFactsOnly) break;
|
|
347
|
+
groups.push(group);
|
|
348
|
+
runLength++;
|
|
349
|
+
}
|
|
350
|
+
if (runLength < 2) return false;
|
|
351
|
+
|
|
352
|
+
const rest = goals.slice(runLength);
|
|
353
|
+
const localStack = [{ index: 0, names: [], values: [], depth }];
|
|
354
|
+
const frames = [];
|
|
355
|
+
|
|
356
|
+
while (localStack.length) {
|
|
357
|
+
const state = localStack.pop();
|
|
358
|
+
solver.stats.max_depth = Math.max(solver.stats.max_depth, state.depth);
|
|
359
|
+
if (state.index === runLength) {
|
|
360
|
+
const next = env.clone();
|
|
361
|
+
for (let i = 0; i < state.names.length; i++) next.bind(state.names[i], state.values[i]);
|
|
362
|
+
frames.push({ kind: 'goals', goals: rest, env: next, depth: state.depth, active });
|
|
363
|
+
if (frames.length > SCALAR_FACT_RUN_FRAME_LIMIT) return false;
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const goal = goals[state.index];
|
|
368
|
+
if (activeMightContain(goal, active) && activeVariantIn(goal, envWithLocal(env, state.names, state.values), active)) continue;
|
|
369
|
+
solver.stats.solve_one_goal_calls++;
|
|
370
|
+
const candidates = selectScalarFactCandidates(groups[state.index], goal, env, state.names, state.values);
|
|
371
|
+
const nextStates = [];
|
|
372
|
+
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
373
|
+
for (const clause of pass) {
|
|
374
|
+
const match = matchScalarFactLocal(goal, clause.head, env, state.names, state.values);
|
|
375
|
+
if (!match) continue;
|
|
376
|
+
solver.stats.unify_calls++;
|
|
377
|
+
nextStates.push({ index: state.index + 1, names: match.names, values: match.values, depth: state.depth + 1 });
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
for (let i = nextStates.length - 1; i >= 0; i--) localStack.push(nextStates[i]);
|
|
381
|
+
if (solver.solutionsSeen >= solver.solutionLimit) break;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (let i = frames.length - 1; i >= 0; i--) stack.push(frames[i]);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
function activeMightContain(goal, active) {
|
|
390
|
+
if (active.length === 0 || goal.type !== COMPOUND) return false;
|
|
391
|
+
for (const entry of active) {
|
|
392
|
+
const activeGoal = entry.goal;
|
|
393
|
+
if (activeGoal?.type === COMPOUND && activeGoal.name === goal.name && activeGoal.arity === goal.arity) return true;
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function envWithLocal(env, names, values) {
|
|
399
|
+
if (names.length === 0) return env;
|
|
400
|
+
return {
|
|
401
|
+
has(name) { return names.includes(name) || env.has(name); },
|
|
402
|
+
get(name) {
|
|
403
|
+
const index = names.indexOf(name);
|
|
404
|
+
return index >= 0 ? values[index] : env.get(name);
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function selectScalarFactCandidates(group, goal, env, names, values) {
|
|
410
|
+
let bestPrimary = group.clauses;
|
|
411
|
+
let bestFallback = [];
|
|
412
|
+
let bestLen = group.clauses.length;
|
|
413
|
+
let indexed = false;
|
|
414
|
+
|
|
415
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
416
|
+
const arg = derefScalarMatch(goal.args[i], env, names, values);
|
|
417
|
+
if (!isScalarTerm(arg)) continue;
|
|
418
|
+
const index = group.argIndexes[i];
|
|
419
|
+
const bucket = index.buckets.get(arg.name) ?? [];
|
|
420
|
+
const candidateLen = bucket.length + index.fallback.length;
|
|
421
|
+
if (!indexed || candidateLen < bestLen) {
|
|
422
|
+
bestPrimary = bucket;
|
|
423
|
+
bestFallback = index.fallback;
|
|
424
|
+
bestLen = candidateLen;
|
|
425
|
+
indexed = true;
|
|
426
|
+
if (bestLen === 0) break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const pair of group.pairIndexes) {
|
|
431
|
+
const left = derefScalarMatch(goal.args[pair.left], env, names, values);
|
|
432
|
+
const right = derefScalarMatch(goal.args[pair.right], env, names, values);
|
|
433
|
+
if (!isScalarTerm(left) || !isScalarTerm(right)) continue;
|
|
434
|
+
const bucket = pair.buckets.get(`${left.name}\x1f${right.name}`) ?? [];
|
|
435
|
+
const candidateLen = bucket.length + pair.fallback.length;
|
|
436
|
+
if (!indexed || candidateLen < bestLen) {
|
|
437
|
+
bestPrimary = bucket;
|
|
438
|
+
bestFallback = pair.fallback;
|
|
439
|
+
bestLen = candidateLen;
|
|
440
|
+
indexed = true;
|
|
441
|
+
if (bestLen === 0) break;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return { primary: bestPrimary, fallback: bestFallback };
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function matchScalarFactLocal(goal, head, env, names, values) {
|
|
449
|
+
if (goal.type !== COMPOUND || head.type !== COMPOUND) return null;
|
|
450
|
+
if (goal.name !== head.name || goal.arity !== head.arity) return null;
|
|
451
|
+
|
|
452
|
+
let nextNames = names;
|
|
453
|
+
let nextValues = values;
|
|
454
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
455
|
+
const factArg = head.args[i];
|
|
456
|
+
const arg = derefScalarMatch(goal.args[i], env, nextNames, nextValues);
|
|
457
|
+
if (arg.type === 'var') {
|
|
458
|
+
if (nextNames === names) {
|
|
459
|
+
nextNames = names.slice();
|
|
460
|
+
nextValues = values.slice();
|
|
461
|
+
}
|
|
462
|
+
nextNames.push(arg.name);
|
|
463
|
+
nextValues.push(factArg);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
if (!isScalarTerm(arg) || arg.name !== factArg.name) return null;
|
|
467
|
+
}
|
|
468
|
+
return { names: nextNames, values: nextValues };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function matchScalarFact(goal, head, env) {
|
|
472
|
+
// A scalar ground fact has no variables to freshen and no compound structure
|
|
473
|
+
// to traverse. Match the goal arguments directly and clone only after the
|
|
474
|
+
// candidate has succeeded.
|
|
475
|
+
if (goal.type !== COMPOUND || head.type !== COMPOUND) return null;
|
|
476
|
+
if (goal.name !== head.name || goal.arity !== head.arity) return null;
|
|
477
|
+
|
|
478
|
+
const names = [];
|
|
479
|
+
const values = [];
|
|
480
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
481
|
+
const factArg = head.args[i];
|
|
482
|
+
let arg = derefScalarMatch(goal.args[i], env, names, values);
|
|
483
|
+
if (arg.type === 'var') {
|
|
484
|
+
names.push(arg.name);
|
|
485
|
+
values.push(factArg);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (!isScalarTerm(arg) || arg.name !== factArg.name) return null;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const next = env.clone();
|
|
492
|
+
for (let i = 0; i < names.length; i++) next.bind(names[i], values[i]);
|
|
493
|
+
return next;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function derefScalarMatch(term, env, names, values) {
|
|
497
|
+
let current = term;
|
|
498
|
+
for (let guard = 0; current?.type === 'var' && guard < 128; guard++) {
|
|
499
|
+
const localIndex = names.indexOf(current.name);
|
|
500
|
+
if (localIndex >= 0) current = values[localIndex];
|
|
501
|
+
else if (env.has(current.name)) current = env.get(current.name);
|
|
502
|
+
else break;
|
|
503
|
+
}
|
|
504
|
+
return current;
|
|
505
|
+
}
|
|
506
|
+
|
|
312
507
|
function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth, active) {
|
|
313
508
|
// Compress deterministic ground single-goal chains such as deep taxonomy
|
|
314
509
|
// proofs: a(ind, n100000) -> a(ind, n99999) -> ... -> a(ind, n0).
|
|
@@ -331,7 +526,7 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
|
|
|
331
526
|
// finite taxonomy chains with the recursive maxDepth guard.
|
|
332
527
|
if (solver.solutionsSeen >= solver.solutionLimit) return true;
|
|
333
528
|
solver.stats.max_depth = Math.max(solver.stats.max_depth, currentDepth);
|
|
334
|
-
const key =
|
|
529
|
+
const key = groundChainKey(currentGoal);
|
|
335
530
|
if (seen.has(key)) return true;
|
|
336
531
|
if (activeVariantIn(currentGoal, currentEnv, active)) return true;
|
|
337
532
|
if (solver.groundChainSuccess.has(key)) {
|
|
@@ -346,28 +541,22 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
|
|
|
346
541
|
for (const pass of [candidates.primary, candidates.fallback]) {
|
|
347
542
|
for (const clause of pass) {
|
|
348
543
|
if (headCannotMatch(currentGoal, clause.head, currentEnv)) continue;
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
solver.stats.unify_calls++;
|
|
354
|
-
if (!unify(currentGoal, freshHead, next)) continue;
|
|
355
|
-
matches.push({ body: freshBody, env: next });
|
|
544
|
+
const match = matchGroundClause(currentGoal, clause);
|
|
545
|
+
if (match === undefined) return false;
|
|
546
|
+
if (match === null) continue;
|
|
547
|
+
matches.push(match);
|
|
356
548
|
if (matches.length > 1) return false;
|
|
357
549
|
}
|
|
358
550
|
}
|
|
359
551
|
|
|
360
552
|
if (matches.length !== 1) return false;
|
|
361
553
|
const match = matches[0];
|
|
362
|
-
if (match.
|
|
554
|
+
if (match.done) {
|
|
363
555
|
rememberGroundChainSuccess(solver, seen);
|
|
364
556
|
stack.push({ kind: 'goals', goals: rest, env: baseEnv, depth: depth + 1, active });
|
|
365
557
|
return true;
|
|
366
558
|
}
|
|
367
|
-
|
|
368
|
-
const nextGoal = match.body[0];
|
|
369
|
-
if (nextGoal.type !== COMPOUND || !termIsGround(nextGoal, match.env)) return false;
|
|
370
|
-
const resolvedNextGoal = copyResolved(nextGoal, match.env);
|
|
559
|
+
const resolvedNextGoal = match.nextGoal;
|
|
371
560
|
const nextGroup = solver.program.findGroup(resolvedNextGoal.name, resolvedNextGoal.arity);
|
|
372
561
|
if (!nextGroup) return false;
|
|
373
562
|
|
|
@@ -378,6 +567,72 @@ function tryPushGroundChainFrames(stack, solver, group, goal, rest, env, depth,
|
|
|
378
567
|
}
|
|
379
568
|
|
|
380
569
|
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
function matchGroundClause(goal, clause) {
|
|
573
|
+
if (clause.head.type !== COMPOUND || goal.type !== COMPOUND) return undefined;
|
|
574
|
+
if (clause.head.name !== goal.name || clause.head.arity !== goal.arity) return null;
|
|
575
|
+
|
|
576
|
+
const names = [];
|
|
577
|
+
const values = [];
|
|
578
|
+
for (let i = 0; i < goal.arity; i++) {
|
|
579
|
+
const headArg = clause.head.args[i];
|
|
580
|
+
const goalArg = goal.args[i];
|
|
581
|
+
if (headArg.type === 'var') {
|
|
582
|
+
let index = names.indexOf(headArg.name);
|
|
583
|
+
if (index < 0) {
|
|
584
|
+
names.push(headArg.name);
|
|
585
|
+
values.push(goalArg);
|
|
586
|
+
} else if (!sameGroundTerm(values[index], goalArg)) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
} else if (isScalarTerm(headArg)) {
|
|
590
|
+
if (!sameGroundTerm(headArg, goalArg)) return null;
|
|
591
|
+
} else {
|
|
592
|
+
return undefined;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (clause.body.length === 0) return { done: true };
|
|
597
|
+
if (clause.body.length !== 1) return undefined;
|
|
598
|
+
const bodyGoal = clause.body[0];
|
|
599
|
+
if (bodyGoal.type !== COMPOUND) return undefined;
|
|
600
|
+
const args = [];
|
|
601
|
+
for (const arg of bodyGoal.args) {
|
|
602
|
+
if (arg.type === 'var') {
|
|
603
|
+
const index = names.indexOf(arg.name);
|
|
604
|
+
if (index < 0) return undefined;
|
|
605
|
+
args.push(values[index]);
|
|
606
|
+
} else if (isScalarTerm(arg)) {
|
|
607
|
+
args.push(arg);
|
|
608
|
+
} else {
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return { nextGoal: compound(bodyGoal.name, args) };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function isScalarTerm(term) {
|
|
616
|
+
return term && (term.type === 'atom' || term.type === 'string' || term.type === 'number');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function sameGroundTerm(left, right) {
|
|
620
|
+
if (left?.type !== right?.type || left?.name !== right?.name) return false;
|
|
621
|
+
const arity = left.args?.length ?? 0;
|
|
622
|
+
if (arity !== (right.args?.length ?? 0)) return false;
|
|
623
|
+
for (let i = 0; i < arity; i++) if (!sameGroundTerm(left.args[i], right.args[i])) return false;
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function groundChainKey(term) {
|
|
628
|
+
if (term?.type === COMPOUND) {
|
|
629
|
+
let out = `${term.name}/${term.arity}`;
|
|
630
|
+
for (let i = 0; i < term.arity; i++) out += `${groundChainKey(term.args[i])}`;
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
return `${term?.type ?? ''}:${term?.name ?? ''}`;
|
|
634
|
+
}
|
|
635
|
+
|
|
381
636
|
function rememberGroundChainSuccess(solver, seen) {
|
|
382
637
|
for (const key of seen) solver.groundChainSuccess.add(key);
|
|
383
638
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
% Reference 9.3, 9.6: lexical comparison and simple text matching.
|
|
2
|
-
answer(matches, true) :- matches("eyelang", "
|
|
2
|
+
answer(matches, true) :- matches("eyelang", "eye").
|
|
3
3
|
answer(not_matches, true) :- not_matches("eyelang", "cat").
|
|
4
4
|
answer(lex_lt, true) :- lt(alpha, beta).
|
|
5
5
|
answer(lex_gt, true) :- gt(beta, alpha).
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
% Reference 9.6: substring and replace have finite boundary behavior.
|
|
2
2
|
materialize(answer, 2).
|
|
3
|
-
answer(prefix, X) :- substring("
|
|
4
|
-
answer(middle, X) :- substring("
|
|
5
|
-
answer(suffix, X) :- substring("
|
|
6
|
-
answer(empty_at_end, X) :- substring("
|
|
7
|
-
answer(out_of_range_rejected, ok) :- not(substring("
|
|
3
|
+
answer(prefix, X) :- substring("eyelanglet", 0, 3, X).
|
|
4
|
+
answer(middle, X) :- substring("eyelanglet", 3, 2, X).
|
|
5
|
+
answer(suffix, X) :- substring("eyelanglet", 2, 3, X).
|
|
6
|
+
answer(empty_at_end, X) :- substring("eyelanglet", 10, 0, X).
|
|
7
|
+
answer(out_of_range_rejected, ok) :- not(substring("eyelanglet", 10, 2, X)).
|
|
8
8
|
answer(replace_all, X) :- replace("banana", "na", "NA", X).
|
|
9
9
|
answer(replace_missing, X) :- replace("banana", "x", "y", X).
|
package/test/run-conformance.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { TestReporter, isMainModule } from './test-style.mjs';
|
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
12
12
|
const packageRoot = path.resolve(root, '..');
|
|
13
|
-
const cliBin = path.join(packageRoot, '
|
|
13
|
+
const cliBin = path.join(packageRoot, 'bin', 'eyelang.js');
|
|
14
14
|
const filterArg = process.argv[2] ?? null;
|
|
15
15
|
|
|
16
16
|
export function runConformance(reporter = new TestReporter(), requestedFilter = null) {
|
package/test/run-regression.mjs
CHANGED
|
@@ -40,8 +40,7 @@ import { hashHex } from '../src/hash.js';
|
|
|
40
40
|
|
|
41
41
|
const testRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)));
|
|
42
42
|
const packageRoot = path.resolve(testRoot, '..');
|
|
43
|
-
const
|
|
44
|
-
const bin = path.join(runtimeRoot, 'bin.js');
|
|
43
|
+
const bin = path.join(packageRoot, 'bin', 'eyelang.js');
|
|
45
44
|
const pkg = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
46
45
|
let tmp = null;
|
|
47
46
|
let tmpCounter = 0;
|
|
@@ -182,7 +181,7 @@ why(
|
|
|
182
181
|
assertIncludes(result.stdout, '-v, --version', 'stdout');
|
|
183
182
|
assertIncludes(result.stdout, '-w, --warnings', 'stdout');
|
|
184
183
|
assertIncludes(result.stdout, '-v, --version Show the package version and exit.\n -w, --warnings Print non-fatal portability warnings to stderr.', 'stdout');
|
|
185
|
-
assertIncludes(result.stdout, 'Read
|
|
184
|
+
assertIncludes(result.stdout, 'Read a Eyelang program', 'stdout');
|
|
186
185
|
assertEqual(result.stderr, '', 'stderr');
|
|
187
186
|
},
|
|
188
187
|
},
|
|
@@ -207,7 +206,7 @@ why(
|
|
|
207
206
|
{
|
|
208
207
|
name: 'npm exec can run package CLI bin from checkout',
|
|
209
208
|
run: () => {
|
|
210
|
-
const result = spawnSync('npm', ['exec', '--', 'eyelang', '--version'], {
|
|
209
|
+
const result = spawnSync('npm', ['exec', '--yes', '--package=.', '--', 'eyelang', '--version'], {
|
|
211
210
|
cwd: packageRoot,
|
|
212
211
|
encoding: 'utf8',
|
|
213
212
|
env: { ...process.env, npm_config_update_notifier: 'false' },
|
package/src/bin.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
(countall(item(X), C), countall(missing(X), Z))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
sumall(S, score(_item, S), Sum)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
aggregate_min(S, item(Name, S), score(Name, S), Bests, Best)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
aggregate_max([S, Name], Name, score(Name, S), Key, Bestname)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
value(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(X, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ancestor(pat, X)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
ok(X, Y)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
reach(a, X)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
value(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|