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 CHANGED
@@ -4,6 +4,7 @@
4
4
  [![DOI](https://img.shields.io/badge/DOI-10.5281%2Fzenodo.20761726-blue.svg)](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 Prolog-style Horn-clause language and engine 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.
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. Builtins are enabled by normal predicate calls.
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 Prolog-like definite-clause language 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.
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 enough Prolog syntax to express Horn-clause reasoning, list processing, arithmetic examples, finite search, and context data, without operators, cut, modules, dynamic predicates, DCGs, or a complete ISO standard library.
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 "(" [term ("," term)*] ")"
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
- Zero-arity compounds are written with parentheses:
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 borrows familiar Prolog syntax and Horn-clause execution but is not ISO Prolog. Notable differences include:
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.3",
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",
@@ -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
- if (unify(goal.args[0], compound(name, args), next)) yield next;
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 isAtomCharCode(code) {
24
- // Stop at whitespace and the punctuation tokens that have syntactic meaning.
25
- return code > 0 &&
26
- !isWhitespaceCode(code) &&
27
- code !== 40 && code !== 41 && code !== 91 && code !== 93 &&
28
- code !== 44 && code !== 124 && code !== 46 &&
29
- code !== 39 && code !== 34 && code !== 58;
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
- const start = this.pos;
134
- while (this.pos < this.source.length && isAtomCharCode(this.source.charCodeAt(this.pos))) this.pos++;
135
- if (this.pos === start) throw new Error(`parse line ${line}: bad character ${JSON.stringify(ch)}`);
136
- let text = this.source.slice(start, this.pos);
137
- let type = isVariableStart(text) ? TOK.VAR : TOK.ATOM;
138
- if (type === TOK.VAR && text === '_') text = `__anon${this.anonymous++}`;
139
- return { type, text, line };
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 !== TOK.RPAREN) {
224
- while (true) {
225
- args.push(this.parseTerm());
226
- if (this.token.type === TOK.COMMA) {
227
- this.advance();
228
- continue;
229
- }
230
- break;
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('empty simple term');
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 (first === 95 || (first >= 65 && first <= 90)) {
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 (!left || !right || SIMPLE_ARG_FORBIDDEN.test(left) || SIMPLE_ARG_FORBIDDEN.test(right)) return null;
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) return parseFastBinaryRuleMatch(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) return { head: parseFastBinaryMatch(factMatch), body: [] };
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(zero_arity, nil()).
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]).
@@ -0,0 +1,4 @@
1
+ % Reference 4, 5.3: arity-zero data is written as an atom constant.
2
+ status(nil, ok).
3
+ answer(value, X) :- status(X, ok).
4
+ materialize(answer, 2).
@@ -1,10 +1,10 @@
1
- % Reference 9.1: term-inspection built-ins expose scalars, nested arguments, and zero arity compounds.
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(decompose_zero, pair(Name, Args)) :- compound_name_arguments(z(), Name, Args).
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(zero_arity, nil()).
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(decompose_zero, pair(z, [])).
6
+ answer(compose_atom_empty_args, z).
7
7
  answer(arg_zero_rejected, ok).
8
8
  answer(arg_too_large_rejected, ok).
@@ -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,4 +0,0 @@
1
- % Reference 4, 5.3: zero-arity compounds are written and matched with parentheses.
2
- status(nil(), ok).
3
- answer(value, X) :- status(X, ok).
4
- materialize(answer, 2).
@@ -1 +0,0 @@
1
- answer(K, V)
@@ -1 +0,0 @@
1
- answer(value, nil()).