eyelang 1.3.0 → 1.3.2

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,7 +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 Prolog-like Horn-clause syntax with a few deliberate eyelang choices, such as `?x` variables for N3/SPARQL-style readability and explicit `table(path, 2).` declarations for tabled predicates.
7
+ Its source syntax is Prolog-like Horn-clause syntax with a few deliberate eyelang choices, such as `?x` variables for N3/SPARQL-style readability, explicit `table(path, 2).` declarations for tabled predicates, and advisory `mode/3` declarations for host tooling.
8
8
  It grew out of logic-language experiments in the EYE/N3 reasoning tradition, but is packaged here as its own project.
9
9
 
10
10
  ## Install and run
package/docs/guide.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  This guide introduces Eyelang, a small Horn-clause language and engine whose source syntax is Prolog-like but deliberately its own compact language 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 bin/eyelang.js` when working directly from a source checkout.
4
4
 
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/2` declarations such as `materialize(answer, 2).` to focus output on selected predicates.
5
+ Programs write relations directly, for example `ancestor(pat, emma)` or `status(case1, accepted)`. Absolute IRI atoms can be written explicitly with angle brackets, for example `<https://schema.org/name>`, when a program needs web identifiers without prefix declarations. 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/2` declarations such as `materialize(answer, 2).` to focus output on selected predicates.
6
6
 
7
7
 
8
8
  For the normative language definition, including lexical syntax, terms, clauses, goals, built-ins, `table/2`, `materialize/2`, and conformance boundaries, read the [Eyelang language reference](language-reference.md).
@@ -568,8 +568,14 @@ Ground facts use a fast path that avoids freshening and copying a rule body. Rec
568
568
  table(path, 2).
569
569
  ```
570
570
 
571
+ Predicates can also carry advisory mode and determinism declarations for documentation and host tooling:
571
572
 
572
- For large programs, keep helper predicates selective, bind arguments early, and declare focused output predicates with `materialize/2` when default output would otherwise solve broad helper goals.
573
+ ```eyelang
574
+ mode(path, 2, [in, out]).
575
+ semidet(edge, 2).
576
+ ```
577
+
578
+ For large programs, keep helper predicates selective, bind arguments early, document intended calling patterns with `mode/3` when helpful, and declare focused output predicates with `materialize/2` when default output would otherwise solve broad helper goals.
573
579
 
574
580
  ## Implementation limits
575
581
 
@@ -138,7 +138,7 @@ Each `?_` anonymous variable occurrence is fresh. A bare `_` is not a variable i
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. 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` or `http://example` MUST also be quoted if they are meant as one atom constant:
141
+ 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
142
 
143
143
  ```eyelang
144
144
  pat
@@ -148,9 +148,18 @@ case_123
148
148
  'org.schema'
149
149
  'eyereasoner.github'
150
150
  'a-b'
151
- 'http://example'
152
151
  ```
153
152
 
153
+ Absolute IRI atom constants MAY also be written between angle brackets. The content must be an absolute IRI-like lexical value with a scheme such as `https:` or `urn:`. Eyelang stores the content as the atom value and prints absolute IRI atoms with angle brackets on read-back:
154
+
155
+ ```eyelang
156
+ <https://example.org/alice>
157
+ <urn:example:bob>
158
+ triple(<https://example.org/alice>, <https://schema.org/name>, "Alice").
159
+ ```
160
+
161
+ Quoted absolute IRI atoms remain valid input, but read-back normalizes them to angle-bracket syntax. For example, `'https://example.org/alice'` prints as `<https://example.org/alice>`.
162
+
154
163
  A quoted atom constant is enclosed in single quotes. A single quote inside a quoted atom constant is represented by doubling it:
155
164
 
156
165
  ```eyelang
@@ -165,6 +174,8 @@ A graphic atom constant is one or more graphic characters from this set:
165
174
  #$&*+-/<=>?@^~\
166
175
  ```
167
176
 
177
+ Angle-bracket IRI syntax is recognized only for absolute IRI-like contents. Graphic atoms such as `<=>`, `<`, and `>=` remain graphic atoms.
178
+
168
179
  ### 3.6 Strings
169
180
 
170
181
  A string is enclosed in double quotes. The implementation supports common escapes such as `\n`, `\t`, `\"`, and `\\`.
@@ -539,6 +550,33 @@ materialize(reason, 2).
539
550
 
540
551
  `materialize/2` affects host output selection only; it does not change the logical meaning of the program. Materialized output facts are not asserted as new source facts for subsequent output goals. A host MAY solve several materialized predicates in one solver run, and tabled predicate answers MAY be reused within that run, but this reuse is controlled by `table/2`, not by materialization.
541
552
 
553
+ ### 11.3 Advisory modes and determinism
554
+
555
+ ```eyelang
556
+ mode(path, 2, [in, out]).
557
+ det(root, 1).
558
+ semidet(edge, 2).
559
+ ```
560
+
561
+ `mode/3`, `det/2`, and `semidet/2` are advisory declarations. They describe a predicate group's intended calling pattern and determinism, but they do not change proof search or answer output. Because they are ordinary facts, programs may also query them.
562
+
563
+ For `mode(?name, ?arity, ?modes)`, the first argument MUST be an atom constant naming the predicate, the second argument MUST be a non-negative integer arity, and the third argument MUST be a proper list whose length is equal to the arity. Portable mode atoms are:
564
+
565
+ - `in`: the argument is expected to be supplied by the caller;
566
+ - `out`: the argument is expected to be produced by the predicate;
567
+ - `any`: no portable mode commitment is made for that argument.
568
+
569
+ `det(?name, ?arity)` declares that a predicate is intended to produce exactly one answer for calls in its documented modes. `semidet(?name, ?arity)` declares that a predicate is intended to produce zero or one answer. This specification does not require runtime enforcement; hosts MAY use these declarations for linting, documentation, indexing decisions, or editor support.
570
+
571
+ Example:
572
+
573
+ ```eyelang
574
+ mode(member, 2, [out, in]).
575
+ semidet(member, 2).
576
+ ```
577
+
578
+ The example documents the common checking/generation mode where the list is supplied and the member is enumerated. A future linting host could warn if a program calls `member/2` outside that intended mode, but a conforming solver still treats `mode/3` and `semidet/2` as ordinary facts plus metadata.
579
+
542
580
  ## 12. Eyelang Sockets
543
581
 
544
582
  A **eyelang Socket** is a declared semantic opening in a eyelang program where facts, rules, tools, datasets, or agents can plug in knowledge through an explicit contract while preserving eyelang-readable reasoning and explanations.
@@ -637,6 +675,7 @@ A conforming eyelang implementation supports the standard language described abo
637
675
  - the standard built-ins listed in section 9;
638
676
  - `table/2` declarations;
639
677
  - `materialize/2` declarations;
678
+ - advisory `mode/3`, `det/2`, and `semidet/2` declarations;
640
679
  - default derived output;
641
680
  - explanation output when the host exposes proof output.
642
681
 
@@ -659,7 +698,7 @@ eyelang source is intended to be familiar to Prolog readers, but eyelang is not
659
698
  - no variables in functor or predicate position;
660
699
  - no occurs check in unification.
661
700
 
662
- Programs intended to be portable to eyelang SHOULD use `?` variables, 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'`.
701
+ Programs intended to be portable to eyelang SHOULD use `?` variables, avoid ISO-specific syntax, and keep terms explicit. Atom names that are not plain lowercase-starting names, graphic atom tokens, or angle-bracket absolute IRI atoms SHOULD be written as quoted atoms, for example `'a-b'`.
663
702
 
664
703
  ## 16. Examples
665
704
 
package/index.d.ts CHANGED
@@ -40,6 +40,8 @@ export interface EyelangPredicateGroup {
40
40
  argIndexes: unknown[];
41
41
  pairIndexes: unknown[];
42
42
  tabled: boolean;
43
+ mode: string[] | null;
44
+ determinism: 'det' | 'semidet' | null;
43
45
  recursive: boolean;
44
46
  }
45
47
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eyelang",
3
- "version": "1.3.0",
3
+ "version": "1.3.2",
4
4
  "description": "A small Prolog-like logic programming language for rules, goals, answers, and proofs.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
package/src/parser.js CHANGED
@@ -7,6 +7,22 @@ const TOK = {
7
7
  LPAREN: '(', RPAREN: ')', LBRACKET: '[', RBRACKET: ']', COMMA: ',', BAR: '|', DOT: '.', IF: ':-'
8
8
  };
9
9
 
10
+ function isAbsoluteIriText(text) {
11
+ return /^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"'{}|\\^`]*$/.test(text);
12
+ }
13
+
14
+ function hasAngleIriToken(source, pos) {
15
+ if (source[pos] !== '<') return false;
16
+ let text = '';
17
+ for (let i = pos + 1; i < source.length; i++) {
18
+ const ch = source[i];
19
+ if (ch === '>') return text.length > 0 && isAbsoluteIriText(text);
20
+ if (ch === '\n' || ch === '\r' || /\s/.test(ch) || ch === '<') return false;
21
+ text += ch;
22
+ }
23
+ return false;
24
+ }
25
+
10
26
  function isWhitespaceCode(code) {
11
27
  return code === 32 || code === 9 || code === 10 || code === 13 || code === 12 || code === 11;
12
28
  }
@@ -97,6 +113,18 @@ class Parser {
97
113
  }
98
114
  if (ch === ':') throw new Error('colon names are not supported; use name or prefix_name');
99
115
 
116
+ if (hasAngleIriToken(this.source, this.pos)) {
117
+ this.take(); // <
118
+ let text = '';
119
+ while (true) {
120
+ if (!this.peek()) throw new Error(`parse line ${line}: unterminated IRI`);
121
+ const value = this.take();
122
+ if (value === '>') break;
123
+ text += value;
124
+ }
125
+ return { type: TOK.ATOM, text, line };
126
+ }
127
+
100
128
  if (ch === '"' || ch === "'") {
101
129
  const quote = this.take();
102
130
  let text = '';
package/src/program.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Program representation and clause indexing.
2
2
  // Indexes are deliberately conservative: they speed up common scalar arguments but never replace unification as the final check.
3
- import { ATOM, COMPOUND, Env, compound, deref, flattenConjunction, isScalar, termToString } from './term.js';
3
+ import { ATOM, COMPOUND, Env, compound, deref, flattenConjunction, isScalar, properListItems, termToString } from './term.js';
4
4
  import { parseClauses } from './parser.js';
5
5
 
6
6
  export class Program {
@@ -40,6 +40,8 @@ export class Program {
40
40
  argIndexes: Array.from({ length: arity }, () => ({ buckets: new Map(), fallback: [] })),
41
41
  pairIndexes: [],
42
42
  tabled: false,
43
+ mode: null,
44
+ determinism: null,
43
45
  recursive: false,
44
46
  };
45
47
  if (arity > 2) {
@@ -70,16 +72,31 @@ export class Program {
70
72
  applyDeclarations(options = {}) {
71
73
  for (const clause of this.clauses) {
72
74
  const h = clause.head;
73
- if (clause.body.length !== 0 || h.type !== COMPOUND || h.arity !== 2) continue;
74
- const [name, arity] = h.args;
75
- if (name.type !== ATOM || arity.type !== 'number') continue;
76
- const key = `${name.name}/${Number(arity.name)}`;
77
- if (h.name === 'table') {
78
- const group = this.groups.get(key);
79
- if (group) group.tabled = true;
80
- } else if (h.name === 'materialize') {
81
- this.hasMaterialize = true;
82
- this.materializedGroups.add(key);
75
+ if (clause.body.length !== 0 || h.type !== COMPOUND) continue;
76
+
77
+ if (h.arity === 2) {
78
+ const indicator = declarationIndicator(h.args[0], h.args[1]);
79
+ if (!indicator) continue;
80
+ const group = this.groups.get(indicator.key);
81
+ if (h.name === 'table') {
82
+ if (group) group.tabled = true;
83
+ } else if (h.name === 'materialize') {
84
+ this.hasMaterialize = true;
85
+ this.materializedGroups.add(indicator.key);
86
+ } else if ((h.name === 'det' || h.name === 'semidet') && group) {
87
+ group.determinism = h.name;
88
+ }
89
+ continue;
90
+ }
91
+
92
+ if (h.name === 'mode' && h.arity === 3) {
93
+ const indicator = declarationIndicator(h.args[0], h.args[1]);
94
+ if (!indicator) continue;
95
+ const modes = declarationModes(h.args[2]);
96
+ if (modes && modes.length === indicator.arity) {
97
+ const group = this.groups.get(indicator.key);
98
+ if (group) group.mode = modes;
99
+ }
83
100
  }
84
101
  }
85
102
  if (options.markRecursive !== false) this.markRecursivePredicates();
@@ -159,6 +176,26 @@ export class Program {
159
176
  }
160
177
  }
161
178
 
179
+
180
+ function declarationIndicator(name, arity) {
181
+ if (name?.type !== ATOM || arity?.type !== 'number') return null;
182
+ if (!/^\d+$/.test(arity.name)) return null;
183
+ const arityNumber = Number(arity.name);
184
+ return { name: name.name, arity: arityNumber, key: `${name.name}/${arityNumber}` };
185
+ }
186
+
187
+ function declarationModes(term) {
188
+ const items = properListItems(term, new Env());
189
+ if (!items) return null;
190
+ const modes = [];
191
+ for (const item of items) {
192
+ if (item.type !== ATOM) return null;
193
+ if (!['in', 'out', 'any'].includes(item.name)) return null;
194
+ modes.push(item.name);
195
+ }
196
+ return modes;
197
+ }
198
+
162
199
  function indexOne(index, arg, clause) {
163
200
  if (isScalar(arg)) {
164
201
  const bucket = index.buckets.get(arg.name);
package/src/term.js CHANGED
@@ -135,6 +135,14 @@ export function termIsGround(term, env = new Env()) {
135
135
 
136
136
  const graphicAtomChars = new Set('#$&*+-/<=>?@^~\\'.split(''));
137
137
 
138
+ function isAbsoluteIriText(text) {
139
+ return /^[A-Za-z][A-Za-z0-9+.-]*:[^\s<>"'{}|\\^`]*$/.test(text);
140
+ }
141
+
142
+ function writeAngleIri(name) {
143
+ return `<${name}>`;
144
+ }
145
+
138
146
  function atomNeedsQuotes(name) {
139
147
  if (!name) return true;
140
148
  if (name === '[]') return false;
@@ -156,6 +164,7 @@ function quoteAtom(name) {
156
164
  }
157
165
 
158
166
  function writeAtom(name) {
167
+ if (isAbsoluteIriText(name)) return writeAngleIri(name);
159
168
  return atomNeedsQuotes(name) ? quoteAtom(name) : name;
160
169
  }
161
170
 
@@ -0,0 +1,7 @@
1
+ % Reference 3.5: absolute IRIs may be written as angle-bracket atom constants.
2
+ materialize(iri_subject, 1).
3
+ materialize(iri_object, 1).
4
+ triple(<https://example.org/alice>, <https://schema.org/name>, "Alice").
5
+ triple(<urn:example:bob>, <https://schema.org/knows>, <https://example.org/alice>).
6
+ iri_subject(?iri) :- triple(?iri, ?_, ?_).
7
+ iri_object(?iri) :- triple(?_, <https://schema.org/knows>, ?iri).
@@ -0,0 +1,11 @@
1
+ % Reference 11.3: mode/3 and det/2 or semidet/2 are ordinary facts and advisory declarations.
2
+ mode(path, 2, [in, out]).
3
+ det(path, 2).
4
+ semidet(edge, 2).
5
+ materialize(answer, 2).
6
+ edge(a, b).
7
+ path(?x, ?y) :- edge(?x, ?y).
8
+ answer(mode_path, ?modes) :- mode(path, 2, ?modes).
9
+ answer(det_path, ok) :- det(path, 2).
10
+ answer(semidet_edge, ok) :- semidet(edge, 2).
11
+ answer(path, ?y) :- path(a, ?y).
@@ -0,0 +1,3 @@
1
+ iri_subject(<https://example.org/alice>).
2
+ iri_subject(<urn:example:bob>).
3
+ iri_object(<https://example.org/alice>).
@@ -0,0 +1,4 @@
1
+ answer(mode_path, [in, out]).
2
+ answer(det_path, ok).
3
+ answer(semidet_edge, ok).
4
+ answer(path, b).
@@ -529,6 +529,27 @@ function whiteBoxCases() {
529
529
  assertEqual(termToString(clauses[0].head, new Env(), true), "p(web('be.ugent', josd), 'org.schema')", 'head');
530
530
  },
531
531
  },
532
+ {
533
+ name: 'parser accepts angle-bracket absolute IRI atoms',
534
+ run: () => {
535
+ const clauses = parseProgramText('p(<https://example.org/alice>, <urn:example:bob>).\n');
536
+ assertEqual(termToString(clauses[0].head, new Env(), true), 'p(<https://example.org/alice>, <urn:example:bob>)', 'head');
537
+ },
538
+ },
539
+ {
540
+ name: 'readback prints absolute IRI atoms with angle brackets',
541
+ run: () => {
542
+ const clauses = parseProgramText("p('https://example.org/alice').\n");
543
+ assertEqual(termToString(clauses[0].head, new Env(), true), 'p(<https://example.org/alice>)', 'head');
544
+ },
545
+ },
546
+ {
547
+ name: 'angle IRI syntax does not steal graphic atom syntax',
548
+ run: () => {
549
+ const clauses = parseProgramText('p(<=>).\n');
550
+ assertEqual(termToString(clauses[0].head, new Env(), true), 'p(<=>)', 'head');
551
+ },
552
+ },
532
553
  {
533
554
  name: 'list construction round-trips through properListItems',
534
555
  run: () => {
@@ -595,6 +616,25 @@ function whiteBoxCases() {
595
616
  assertEqual(group.tabled, false, 'memoize/2 is not a table declaration');
596
617
  },
597
618
  },
619
+ {
620
+ name: 'mode and determinism declarations annotate predicate groups',
621
+ run: () => {
622
+ const program = Program.parse('mode(path, 2, [in, out]).\ndet(path, 2).\nedge(a, b).\npath(?x, ?y) :- edge(?x, ?y).\n');
623
+ const group = program.findGroup('path', 2);
624
+ assertEqual(Boolean(group), true, 'path/2 group exists');
625
+ assertEqual(group.mode.join(','), 'in,out', 'path/2 mode');
626
+ assertEqual(group.determinism, 'det', 'path/2 determinism');
627
+ },
628
+ },
629
+ {
630
+ name: 'semidet declaration annotates predicate groups',
631
+ run: () => {
632
+ const program = Program.parse('semidet(edge, 2).\nedge(a, b).\n');
633
+ const group = program.findGroup('edge', 2);
634
+ assertEqual(Boolean(group), true, 'edge/2 group exists');
635
+ assertEqual(group.determinism, 'semidet', 'edge/2 determinism');
636
+ },
637
+ },
598
638
  {
599
639
  name: 'challenging examples keep dynamic-programming predicates tabled',
600
640
  run: () => {