eyelang 0.1.3 → 0.1.5
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 +1 -20
- package/docs/guide.md +14 -4
- package/docs/language-reference.md +14 -10
- package/package.json +2 -2
- package/src/builtins/terms.js +2 -1
- package/src/parser.js +66 -29
- package/src/term.js +1 -0
- package/test/conformance/cases/003_terms_and_readback.pl +1 -1
- package/test/conformance/cases/016_arity_zero_atom.pl +4 -0
- package/test/conformance/cases/073_term_introspection_edges.pl +2 -2
- package/test/conformance/expected/003_terms_and_readback.pl +1 -1
- package/test/conformance/expected/016_arity_zero_atom.pl +1 -0
- package/test/conformance/expected/073_term_introspection_edges.pl +1 -1
- package/test/run-regression.mjs +17 -20
- package/test/conformance/cases/016_zero_arity_compound.pl +0 -4
- package/test/conformance/cases/016_zero_arity_compound.query +0 -1
- package/test/conformance/expected/016_zero_arity_compound.pl +0 -1
package/README.md
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
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
|
+
Its source syntax is a deliberately small subset of ordinary Prolog term and Horn-clause syntax.
|
|
7
8
|
It grew out of logic-language experiments in the EYE/N3 reasoning tradition, but is packaged here as its own project.
|
|
8
9
|
|
|
9
10
|
## Install and run
|
|
@@ -33,26 +34,6 @@ console.log(result.stdout);
|
|
|
33
34
|
- [Guide](docs/guide.md)
|
|
34
35
|
- [Language reference](docs/language-reference.md)
|
|
35
36
|
|
|
36
|
-
### Eyelang built-ins
|
|
37
|
-
|
|
38
|
-
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:
|
|
39
|
-
|
|
40
|
-
| Family | Count | Built-ins |
|
|
41
|
-
|---|---:|---|
|
|
42
|
-
| Core and host | 4 | `eq/2`, `neq/2`, `local_time/1`, `difference/3` |
|
|
43
|
-
| 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` |
|
|
44
|
-
| 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` |
|
|
45
|
-
| 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` |
|
|
46
|
-
| Aggregation | 5 | `findall/3`, `countall/2`, `sumall/3`, `aggregate_min/5`, `aggregate_max/5` |
|
|
47
|
-
| Control | 3 | `not/1`, `once/1`, `forall/2` |
|
|
48
|
-
| Context and terms | 5 | `holds/2`, `holds/3`, `functor/3`, `arg/3`, `compound_name_arguments/3` |
|
|
49
|
-
| **Total** | **80** | |
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
## Custom built-ins
|
|
53
|
-
|
|
54
|
-
Custom built-ins can be supplied by creating a `BuiltinRegistry` and passing it to the solver or `run` API.
|
|
55
|
-
|
|
56
37
|
## Tests
|
|
57
38
|
|
|
58
39
|
```bash
|
package/docs/guide.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Eyelang Guide
|
|
2
2
|
|
|
3
|
-
This guide introduces Eyelang, a small
|
|
3
|
+
This guide introduces Eyelang, a small Horn-clause language and engine whose source syntax is a deliberate subset of ordinary Prolog syntax for rules, goals, answers, and proofs. Eyelang works over ordinary terms, lists, arithmetic, strings, and finite search. Run it with the `eyelang` CLI, or use `node src/bin.js` when working directly from a source checkout.
|
|
4
4
|
|
|
5
5
|
Programs write relations directly, for example `ancestor(pat, emma)` or `status(case1, accepted)`. Eyelang output is ordinary Eyelang syntax: by default, the CLI materializes selected answer facts and prints those facts only. Pass `--proof` (or `-p`) when you also want each answer followed by a `why/2` explanation fact that records the proof. Programs may add `materialize(Name, Arity).` declarations to focus output on selected predicates.
|
|
6
6
|
|
|
@@ -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](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:
|
|
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
|
|
|
@@ -522,4 +532,4 @@ For large programs, keep helper predicates selective, bind arguments early, and
|
|
|
522
532
|
|
|
523
533
|
## Implementation limits
|
|
524
534
|
|
|
525
|
-
Eyelang is intentionally smaller than ISO Prolog. It has no operators, cut, modules, dynamic database updates, DCGs, or complete ISO library. 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.
|
|
535
|
+
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. 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.
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
|
|
63
63
|
## Abstract
|
|
64
64
|
|
|
65
|
-
Eyelang is a compact
|
|
65
|
+
Eyelang is a compact definite-clause language whose surface syntax is a deliberately small subset of ordinary Prolog term and clause syntax for rule-based programs over ordinary terms, lists, arithmetic, strings, and finite search. A Eyelang program is a finite sequence of facts and Horn clauses. The underlying declarative semantics of the pure language is **Herbrand semantics**: constants, compound terms, and lists denote themselves, and predicates denote sets of ground atomic formulas over those terms. Evaluation is goal-directed: goals are solved by unification against facts, rules, and a fixed set of built-in predicates.
|
|
66
66
|
|
|
67
|
-
Eyelang is intentionally smaller than ISO Prolog. It supports
|
|
67
|
+
Eyelang is intentionally smaller than ISO Prolog. It supports a Prolog-syntax subset sufficient to express Horn-clause reasoning, list processing, arithmetic examples, finite search, and context data, without operators, cut, modules, dynamic predicates, DCGs, zero-arity compound syntax, or a complete ISO standard library.
|
|
68
68
|
|
|
69
69
|
## 1. Terminology and normative language
|
|
70
70
|
|
|
@@ -138,12 +138,14 @@ Each `_` anonymous variable occurrence is fresh.
|
|
|
138
138
|
|
|
139
139
|
### 3.5 Atom constants
|
|
140
140
|
|
|
141
|
-
A plain atom constant starts with a lowercase ASCII letter and is followed by zero or more ASCII letters, digits, or underscores:
|
|
141
|
+
A plain atom constant starts with a lowercase ASCII letter and is followed by zero or more ASCII letters, digits, or underscores. This follows the usual Prolog unquoted-atom shape; names such as `a-b` or `http://example` MUST be quoted if they are meant as one atom constant:
|
|
142
142
|
|
|
143
143
|
```prolog
|
|
144
144
|
pat
|
|
145
145
|
type
|
|
146
146
|
case_123
|
|
147
|
+
'a-b'
|
|
148
|
+
'http://example'
|
|
147
149
|
```
|
|
148
150
|
|
|
149
151
|
A quoted atom constant is enclosed in single quotes. A single quote inside a quoted atom constant is represented by doubling it:
|
|
@@ -190,23 +192,24 @@ term ::= variable
|
|
|
190
192
|
| atom_constant
|
|
191
193
|
| string
|
|
192
194
|
| number
|
|
193
|
-
| atom_constant "("
|
|
195
|
+
| atom_constant "(" term ("," term)* ")"
|
|
194
196
|
| "[" [list_items] "]"
|
|
195
197
|
| "(" term ("," term)+ ")"
|
|
196
198
|
list_items ::= term ("," term)* ["|" term]
|
|
197
199
|
```
|
|
198
200
|
|
|
199
|
-
Here `atom_constant` is a lexical class for symbolic scalar terms, not an atomic formula. Atomic formulas are represented by the grammar alternative `atom_constant "(" ... ")"` when such a compound appears in a clause head, rule body, or selected goal.
|
|
201
|
+
Here `atom_constant` is a lexical class for symbolic scalar terms, not an atomic formula. Atomic formulas are represented by the grammar alternative `atom_constant "(" ... ")"` when such a compound appears in a clause head, rule body, or selected goal. Compound syntax always has at least one argument.
|
|
200
202
|
|
|
201
203
|
A clause head SHOULD be a compound term. Non-compound heads are parsed but are not useful in the current predicate index.
|
|
202
204
|
|
|
203
|
-
|
|
205
|
+
Arity-zero data is written as an atom constant, not as a zero-arity compound:
|
|
204
206
|
|
|
205
207
|
```prolog
|
|
206
|
-
nil
|
|
207
|
-
value(example, nil()).
|
|
208
|
+
value(example, nil).
|
|
208
209
|
```
|
|
209
210
|
|
|
211
|
+
The syntax `nil()` is intentionally rejected so eyelang source and read-back output remain inside the Prolog syntax subset used by this language.
|
|
212
|
+
|
|
210
213
|
## 5. Terms
|
|
211
214
|
|
|
212
215
|
### 5.1 Variables
|
|
@@ -642,9 +645,10 @@ Conformance cases live in the repository under `test/conformance/`. They are run
|
|
|
642
645
|
|
|
643
646
|
## 15. Relationship to ISO Prolog
|
|
644
647
|
|
|
645
|
-
eyelang
|
|
648
|
+
eyelang source is intended to be a subset of familiar Prolog term and Horn-clause syntax, but eyelang is not ISO Prolog. Notable differences include:
|
|
646
649
|
|
|
647
650
|
- no operators or operator declarations;
|
|
651
|
+
- no zero-arity compound syntax such as `nil()`;
|
|
648
652
|
- no cut;
|
|
649
653
|
- no modules;
|
|
650
654
|
- no dynamic database update;
|
|
@@ -653,7 +657,7 @@ eyelang borrows familiar Prolog syntax and Horn-clause execution but is not ISO
|
|
|
653
657
|
- no variables in functor or predicate position;
|
|
654
658
|
- no occurs check in unification.
|
|
655
659
|
|
|
656
|
-
Programs intended to be portable to eyelang SHOULD avoid ISO-specific syntax and keep terms explicit.
|
|
660
|
+
Programs intended to be portable to eyelang SHOULD avoid ISO-specific syntax and keep terms explicit. Atom names that are not plain lowercase-starting names or graphic atom tokens SHOULD be written as quoted atoms, for example `'a-b'`.
|
|
657
661
|
|
|
658
662
|
## 16. Examples
|
|
659
663
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyelang",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A small logic programming language for rules, goals, answers, and proofs.",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "A small Prolog-syntax-subset logic programming language for rules, goals, answers, and proofs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
7
7
|
"types": "./index.d.ts",
|
package/src/builtins/terms.js
CHANGED
|
@@ -56,7 +56,8 @@ function* compoundNameArguments({ goal, env }) {
|
|
|
56
56
|
const args = properListItems(goal.args[2], env);
|
|
57
57
|
if (name == null || !args) return;
|
|
58
58
|
const next = env.clone();
|
|
59
|
-
|
|
59
|
+
const built = args.length === 0 ? atom(name) : compound(name, args);
|
|
60
|
+
if (unify(goal.args[0], built, next)) yield next;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
function scalarNameTerm(term) {
|
package/src/parser.js
CHANGED
|
@@ -15,18 +15,27 @@ function isDigitCode(code) {
|
|
|
15
15
|
return code >= 48 && code <= 57;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
function isAsciiLetterCode(code) {
|
|
19
|
+
return (code >= 65 && code <= 90) || (code >= 97 && code <= 122);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isNameContinueCode(code) {
|
|
23
|
+
return code === 95 || isAsciiLetterCode(code) || isDigitCode(code);
|
|
24
|
+
}
|
|
25
|
+
|
|
18
26
|
function isVariableStart(text) {
|
|
19
27
|
const code = text.charCodeAt(0);
|
|
20
28
|
return code === 95 || (code >= 65 && code <= 90);
|
|
21
29
|
}
|
|
22
30
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
function isPlainAtomStartCode(code) {
|
|
32
|
+
return code >= 97 && code <= 122;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const graphicAtomChars = '#$&*+-/<=>?@^~\\';
|
|
36
|
+
|
|
37
|
+
function isGraphicAtomCode(code) {
|
|
38
|
+
return graphicAtomChars.includes(String.fromCharCode(code));
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
class Parser {
|
|
@@ -130,13 +139,30 @@ class Parser {
|
|
|
130
139
|
return { type: TOK.NUMBER, text: this.source.slice(start, this.pos), line };
|
|
131
140
|
}
|
|
132
141
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
if (isVariableStart(ch)) {
|
|
143
|
+
const start = this.pos;
|
|
144
|
+
this.take();
|
|
145
|
+
while (isNameContinueCode(this.peek().charCodeAt(0))) this.take();
|
|
146
|
+
let text = this.source.slice(start, this.pos);
|
|
147
|
+
if (text === '_') text = `__anon${this.anonymous++}`;
|
|
148
|
+
return { type: TOK.VAR, text, line };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (isPlainAtomStartCode(ch.charCodeAt(0))) {
|
|
152
|
+
const start = this.pos;
|
|
153
|
+
this.take();
|
|
154
|
+
while (isNameContinueCode(this.peek().charCodeAt(0))) this.take();
|
|
155
|
+
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (isGraphicAtomCode(ch.charCodeAt(0))) {
|
|
159
|
+
const start = this.pos;
|
|
160
|
+
this.take();
|
|
161
|
+
while (isGraphicAtomCode(this.peek().charCodeAt(0))) this.take();
|
|
162
|
+
return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(`parse line ${line}: bad character ${JSON.stringify(ch)}`);
|
|
140
166
|
}
|
|
141
167
|
advance() {
|
|
142
168
|
this.token = this.nextToken();
|
|
@@ -220,15 +246,16 @@ class Parser {
|
|
|
220
246
|
if (this.token.type === TOK.LPAREN) {
|
|
221
247
|
this.advance();
|
|
222
248
|
const args = [];
|
|
223
|
-
if (this.token.type
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
if (this.token.type === TOK.RPAREN) {
|
|
250
|
+
throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(name)} for arity zero data`);
|
|
251
|
+
}
|
|
252
|
+
while (true) {
|
|
253
|
+
args.push(this.parseTerm());
|
|
254
|
+
if (this.token.type === TOK.COMMA) {
|
|
255
|
+
this.advance();
|
|
256
|
+
continue;
|
|
231
257
|
}
|
|
258
|
+
break;
|
|
232
259
|
}
|
|
233
260
|
this.expect(TOK.RPAREN, ')');
|
|
234
261
|
this.advance();
|
|
@@ -286,9 +313,11 @@ function isSimpleName(text) {
|
|
|
286
313
|
}
|
|
287
314
|
|
|
288
315
|
const SIMPLE_NUMBER = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
289
|
-
const SIMPLE_ARG_FORBIDDEN = /[\s()[\]|"']/;
|
|
290
316
|
const FAST_BINARY_FACT = /^([a-z][A-Za-z0-9_]*)\(\s*([^,\s()[\]|"']+)\s*,\s*([^,\s()[\]|"']+)\s*\)\.$/;
|
|
291
317
|
const FAST_BINARY_RULE = /^([a-z][A-Za-z0-9_]*)\(\s*([^,\s()[\]|"']+)\s*,\s*([^,\s()[\]|"']+)\s*\)\s*:-\s*([a-z][A-Za-z0-9_]*)\(\s*([^,\s()[\]|"']+)\s*,\s*([^,\s()[\]|"']+)\s*\)\.$/;
|
|
318
|
+
const SIMPLE_VARIABLE = /^[_A-Z][A-Za-z0-9_]*$/;
|
|
319
|
+
const SIMPLE_ATOM = /^[a-z][A-Za-z0-9_]*$/;
|
|
320
|
+
const GRAPHIC_ATOM = /^[#$&*+\-\/<=>?@^~\\]+$/;
|
|
292
321
|
|
|
293
322
|
function parseClausesFastNoSource(source) {
|
|
294
323
|
source = String(source ?? '');
|
|
@@ -306,11 +335,12 @@ function parseClausesFastNoSource(source) {
|
|
|
306
335
|
cache.set(key, value);
|
|
307
336
|
return value;
|
|
308
337
|
};
|
|
338
|
+
const isFastScalarToken = (text) => SIMPLE_VARIABLE.test(text) || SIMPLE_ATOM.test(text) || GRAPHIC_ATOM.test(text) || SIMPLE_NUMBER.test(text);
|
|
309
339
|
const scalarOrVariableFast = (text) => {
|
|
310
|
-
if (!text) throw new Error('
|
|
340
|
+
if (!text || !isFastScalarToken(text)) throw new Error('bad simple term');
|
|
311
341
|
const first = text.charCodeAt(0);
|
|
312
342
|
if (text === '_') return variable(`__anon${anonymous++}`);
|
|
313
|
-
if (
|
|
343
|
+
if (SIMPLE_VARIABLE.test(text)) {
|
|
314
344
|
const existing = variableCache.get(text);
|
|
315
345
|
if (existing) return existing;
|
|
316
346
|
const value = variable(text);
|
|
@@ -318,7 +348,6 @@ function parseClausesFastNoSource(source) {
|
|
|
318
348
|
return value;
|
|
319
349
|
}
|
|
320
350
|
if ((first === 45 || isDigitCode(first)) && SIMPLE_NUMBER.test(text)) return cached(numberCache, text, numberTerm);
|
|
321
|
-
if (first === 34 && text.endsWith('"')) return cached(stringCache, text.slice(1, -1), stringTerm);
|
|
322
351
|
return atom(text);
|
|
323
352
|
};
|
|
324
353
|
const scalarOrVariable = (text) => scalarOrVariableFast(text.trim());
|
|
@@ -334,13 +363,15 @@ function parseClausesFastNoSource(source) {
|
|
|
334
363
|
if (comma < 0 || inner.indexOf(',', comma + 1) >= 0) return null;
|
|
335
364
|
const left = inner.slice(0, comma).trim();
|
|
336
365
|
const right = inner.slice(comma + 1).trim();
|
|
337
|
-
if (!
|
|
366
|
+
if (!isFastScalarToken(left) || !isFastScalarToken(right)) return null;
|
|
338
367
|
return compound(name, [scalarOrVariable(left), scalarOrVariable(right)]);
|
|
339
368
|
};
|
|
340
369
|
const parseFastBinaryMatch = (match) => {
|
|
370
|
+
if (!isFastScalarToken(match[2]) || !isFastScalarToken(match[3])) return null;
|
|
341
371
|
return compound(match[1], [scalarOrVariableFast(match[2]), scalarOrVariableFast(match[3])]);
|
|
342
372
|
};
|
|
343
373
|
const parseFastBinaryRuleMatch = (match) => {
|
|
374
|
+
if (!isFastScalarToken(match[2]) || !isFastScalarToken(match[3]) || !isFastScalarToken(match[5]) || !isFastScalarToken(match[6])) return null;
|
|
344
375
|
return {
|
|
345
376
|
head: compound(match[1], [scalarOrVariableFast(match[2]), scalarOrVariableFast(match[3])]),
|
|
346
377
|
body: [compound(match[4], [scalarOrVariableFast(match[5]), scalarOrVariableFast(match[6])])],
|
|
@@ -349,9 +380,15 @@ function parseClausesFastNoSource(source) {
|
|
|
349
380
|
const parseFastLine = (text) => {
|
|
350
381
|
if (!text.endsWith('.')) return null;
|
|
351
382
|
const ruleMatch = FAST_BINARY_RULE.exec(text);
|
|
352
|
-
if (ruleMatch)
|
|
383
|
+
if (ruleMatch) {
|
|
384
|
+
const parsed = parseFastBinaryRuleMatch(ruleMatch);
|
|
385
|
+
if (parsed) return parsed;
|
|
386
|
+
}
|
|
353
387
|
const factMatch = FAST_BINARY_FACT.exec(text);
|
|
354
|
-
if (factMatch)
|
|
388
|
+
if (factMatch) {
|
|
389
|
+
const head = parseFastBinaryMatch(factMatch);
|
|
390
|
+
if (head) return { head, body: [] };
|
|
391
|
+
}
|
|
355
392
|
return null;
|
|
356
393
|
};
|
|
357
394
|
const parseSimple = (text) => {
|
package/src/term.js
CHANGED
|
@@ -192,6 +192,7 @@ export function termToString(term, env = new Env(), quoteStrings = true) {
|
|
|
192
192
|
if (resolved.type === STRING) return writeString(resolved.name, quoteStrings);
|
|
193
193
|
if (resolved.type === ATOM) return writeAtom(resolved.name);
|
|
194
194
|
if (resolved.type === NUMBER) return resolved.name;
|
|
195
|
+
if (resolved.type === COMPOUND && resolved.arity === 0) return writeAtom(resolved.name);
|
|
195
196
|
if (isConjunction(resolved)) {
|
|
196
197
|
const parts = [];
|
|
197
198
|
let cursor = resolved;
|
|
@@ -9,7 +9,7 @@ raw_value(integer, -42).
|
|
|
9
9
|
raw_value(decimal, 0.25).
|
|
10
10
|
raw_value(scientific, 1.25e-3).
|
|
11
11
|
raw_value(compound, pair(3, nested(atom, [x, y]))).
|
|
12
|
-
raw_value(
|
|
12
|
+
raw_value(arity_zero_atom, nil).
|
|
13
13
|
raw_value(empty_list, []).
|
|
14
14
|
raw_value(proper_list, [a, b, c]).
|
|
15
15
|
raw_value(improper_list, [a, b | tail]).
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
% Reference 9.1: term-inspection built-ins expose scalars, nested arguments, and
|
|
1
|
+
% Reference 9.1: term-inspection built-ins expose scalars, nested arguments, and atom construction from an empty argument list.
|
|
2
2
|
materialize(answer, 2).
|
|
3
3
|
answer(functor_atom, pair(Name, Arity)) :- functor(alpha, Name, Arity).
|
|
4
4
|
answer(functor_number, pair(Name, Arity)) :- functor(42, Name, Arity).
|
|
5
5
|
answer(functor_string, pair(Name, Arity)) :- functor("hi", Name, Arity).
|
|
6
6
|
answer(arg_nested, X) :- arg(1, path(edge(a, b), c), X).
|
|
7
7
|
answer(compose_nested, X) :- compound_name_arguments(X, outer, [inner(a), [b, c]]).
|
|
8
|
-
answer(
|
|
8
|
+
answer(compose_atom_empty_args, X) :- compound_name_arguments(X, z, []).
|
|
9
9
|
answer(arg_zero_rejected, ok) :- not(arg(0, edge(a, b), X)).
|
|
10
10
|
answer(arg_too_large_rejected, ok) :- not(arg(3, edge(a, b), X)).
|
|
@@ -7,7 +7,7 @@ value(integer, -42).
|
|
|
7
7
|
value(decimal, 0.25).
|
|
8
8
|
value(scientific, 1.25e-3).
|
|
9
9
|
value(compound, pair(3, nested(atom, [x, y]))).
|
|
10
|
-
value(
|
|
10
|
+
value(arity_zero_atom, nil).
|
|
11
11
|
value(empty_list, []).
|
|
12
12
|
value(proper_list, [a, b, c]).
|
|
13
13
|
value(improper_list, [a, b | tail]).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
answer(value, nil).
|
|
@@ -3,6 +3,6 @@ answer(functor_number, pair(42, 0)).
|
|
|
3
3
|
answer(functor_string, pair("hi", 0)).
|
|
4
4
|
answer(arg_nested, edge(a, b)).
|
|
5
5
|
answer(compose_nested, outer(inner(a), [b, c])).
|
|
6
|
-
answer(
|
|
6
|
+
answer(compose_atom_empty_args, z).
|
|
7
7
|
answer(arg_zero_rejected, ok).
|
|
8
8
|
answer(arg_too_large_rejected, ok).
|
package/test/run-regression.mjs
CHANGED
|
@@ -188,26 +188,6 @@ why(
|
|
|
188
188
|
assertEqual(result.stderr, '', 'stderr');
|
|
189
189
|
},
|
|
190
190
|
},
|
|
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
191
|
{
|
|
212
192
|
name: 'stdin input is accepted',
|
|
213
193
|
run: () => {
|
|
@@ -393,6 +373,23 @@ function whiteBoxCases() {
|
|
|
393
373
|
assertEqual(termIsGround(resolved), true, 'ground after copy');
|
|
394
374
|
},
|
|
395
375
|
},
|
|
376
|
+
|
|
377
|
+
{
|
|
378
|
+
name: 'parser rejects non-Prolog unquoted atom spelling',
|
|
379
|
+
run: () => {
|
|
380
|
+
let threw = false;
|
|
381
|
+
try { parseProgramText('value(a-b, ok).\n'); } catch (_) { threw = true; }
|
|
382
|
+
assertEqual(threw, true, 'a-b must be quoted');
|
|
383
|
+
},
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
name: 'parser rejects zero-arity compound syntax',
|
|
387
|
+
run: () => {
|
|
388
|
+
let threw = false;
|
|
389
|
+
try { parseProgramText('value(nil(), ok).\n'); } catch (_) { threw = true; }
|
|
390
|
+
assertEqual(threw, true, 'zero-arity compound rejection');
|
|
391
|
+
},
|
|
392
|
+
},
|
|
396
393
|
{
|
|
397
394
|
name: 'parser preserves list syntax readback',
|
|
398
395
|
run: () => {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(K, V)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
answer(value, nil()).
|