eyeprolog 1.3.14 → 1.3.16

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
@@ -201,6 +201,23 @@ noun --> [world] | [prolog].
201
201
  %% goal: phrase(sentence, Words)
202
202
  ```
203
203
 
204
+ For a larger bidirectional example, see
205
+ [`examples/dcg-expression-language.pl`](examples/dcg-expression-language.pl).
206
+ It parses precedence-sensitive arithmetic into an AST, evaluates expressions
207
+ with variables, generates tokens back from an AST with only the necessary
208
+ parentheses, round-trips the generated form, and demonstrates `phrase/3`
209
+ remainder handling. The example uses accumulator nonterminals for
210
+ left-associative operators, so state is handed repeatedly from one nonterminal
211
+ to the next rather than hidden in host code.
212
+
213
+ Deep finite DCG traversal is kept relational but does not have to consume one
214
+ general solver frame per token. In particular, the interoperable `...//0`
215
+ helper from `library(iso_ext)` can scan a finite compact list iteratively, and a
216
+ following grammar that is statically known to leave the DCG state unchanged can
217
+ be continued without rebuilding a full clause-resolution frame for every
218
+ suffix. Open and remainder-producing uses still retain the ordinary DCG
219
+ solutions and backtracking behavior.
220
+
204
221
  EyeProlog also adds 128 public library predicate indicators to its 129-entry ISO
205
222
  profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
206
223
  modules under `src/lib/`; the remaining control predicates and finite-domain
@@ -0,0 +1,147 @@
1
+ % Parse, evaluate, and regenerate arithmetic expressions with a DCG.
2
+ %
3
+ % The parser builds an AST while respecting precedence and left associativity.
4
+ % The additive_tail//2 and multiplicative_tail//2 nonterminals use an
5
+ % accumulator instead of left recursion. A second DCG pretty-prints an AST
6
+ % with only the parentheses required to preserve its structure, making the
7
+ % example useful in both directions.
8
+ %% goal: dcg_expression_example(X0, X1)
9
+
10
+ % expression//1: + and - are the lowest-precedence operators.
11
+ expression(AST) -->
12
+ term(First),
13
+ additive_tail(First, AST).
14
+
15
+ additive_tail(Left, AST) -->
16
+ ['+'],
17
+ term(Right),
18
+ { Next = add(Left, Right) },
19
+ additive_tail(Next, AST).
20
+ additive_tail(Left, AST) -->
21
+ ['-'],
22
+ term(Right),
23
+ { Next = sub(Left, Right) },
24
+ additive_tail(Next, AST).
25
+ additive_tail(AST, AST) --> [].
26
+
27
+ % term//1: * and / bind more tightly than + and -.
28
+ term(AST) -->
29
+ unary(First),
30
+ multiplicative_tail(First, AST).
31
+
32
+ multiplicative_tail(Left, AST) -->
33
+ ['*'],
34
+ unary(Right),
35
+ { Next = mul(Left, Right) },
36
+ multiplicative_tail(Next, AST).
37
+ multiplicative_tail(Left, AST) -->
38
+ ['/'],
39
+ unary(Right),
40
+ { Next = div(Left, Right) },
41
+ multiplicative_tail(Next, AST).
42
+ multiplicative_tail(AST, AST) --> [].
43
+
44
+ % Unary minus nests, so --x is represented as neg(neg(var(x))).
45
+ unary(neg(AST)) --> ['-'], unary(AST).
46
+ unary(AST) --> primary(AST).
47
+
48
+ primary(lit(Number)) -->
49
+ [Number],
50
+ { number(Number) }.
51
+ primary(var(Name)) -->
52
+ [Name],
53
+ { variable_name(Name) }.
54
+ primary(AST) --> ['('], expression(AST), [')'].
55
+
56
+ variable_name(x).
57
+ variable_name(y).
58
+ variable_name(z).
59
+
60
+ % A small evaluator for the AST produced by the grammar.
61
+ evaluate(lit(Number), _, Number).
62
+ evaluate(var(Name), Environment, Value) :-
63
+ lookup(Name, Environment, Value).
64
+ evaluate(neg(AST), Environment, Value) :-
65
+ evaluate(AST, Environment, Inner),
66
+ Value is -Inner.
67
+ evaluate(add(Left, Right), Environment, Value) :-
68
+ evaluate(Left, Environment, L),
69
+ evaluate(Right, Environment, R),
70
+ Value is L + R.
71
+ evaluate(sub(Left, Right), Environment, Value) :-
72
+ evaluate(Left, Environment, L),
73
+ evaluate(Right, Environment, R),
74
+ Value is L - R.
75
+ evaluate(mul(Left, Right), Environment, Value) :-
76
+ evaluate(Left, Environment, L),
77
+ evaluate(Right, Environment, R),
78
+ Value is L * R.
79
+ evaluate(div(Left, Right), Environment, Value) :-
80
+ evaluate(Left, Environment, L),
81
+ evaluate(Right, Environment, R),
82
+ Value is L / R.
83
+
84
+ lookup(Name, [Name-Value|_], Value).
85
+ lookup(Name, [_|Rest], Value) :-
86
+ lookup(Name, Rest, Value).
87
+
88
+ % Precedence-aware generation. The right operand is emitted at a stricter
89
+ % minimum precedence than the left operand, preserving left associativity.
90
+ emit_expression(AST) --> emit(AST, 0).
91
+
92
+ emit(AST, Minimum) -->
93
+ { precedence(AST, Precedence),
94
+ parenthesize(Precedence, Minimum, Wrap) },
95
+ emit_wrapped(Wrap, AST).
96
+
97
+ emit_wrapped(yes, AST) --> ['('], emit_node(AST), [')'].
98
+ emit_wrapped(no, AST) --> emit_node(AST).
99
+
100
+ emit_node(lit(Number)) --> [Number].
101
+ emit_node(var(Name)) --> [Name].
102
+ emit_node(neg(AST)) --> ['-'], emit(AST, 30).
103
+ emit_node(add(Left, Right)) -->
104
+ emit(Left, 10), ['+'], emit(Right, 11).
105
+ emit_node(sub(Left, Right)) -->
106
+ emit(Left, 10), ['-'], emit(Right, 11).
107
+ emit_node(mul(Left, Right)) -->
108
+ emit(Left, 20), ['*'], emit(Right, 21).
109
+ emit_node(div(Left, Right)) -->
110
+ emit(Left, 20), ['/'], emit(Right, 21).
111
+
112
+ precedence(add(_, _), 10).
113
+ precedence(sub(_, _), 10).
114
+ precedence(mul(_, _), 20).
115
+ precedence(div(_, _), 20).
116
+ precedence(neg(_), 30).
117
+ precedence(lit(_), 40).
118
+ precedence(var(_), 40).
119
+
120
+ parenthesize(Precedence, Minimum, yes) :- Precedence < Minimum.
121
+ parenthesize(Precedence, Minimum, no) :- Precedence >= Minimum.
122
+
123
+ % Parse a mixed-precedence expression.
124
+ dcg_expression_example(parsed, AST) :-
125
+ phrase(expression(AST),
126
+ [2, '+', 3, '*', '(', 4, '-', 1, ')']).
127
+
128
+ % Evaluate a parsed expression with variables.
129
+ dcg_expression_example(evaluated, Value) :-
130
+ phrase(expression(AST), [x, '*', '(', y, '+', 2, ')', '-', z]),
131
+ evaluate(AST, [x-4, y-3, z-1], Value).
132
+
133
+ % Generate the minimal parentheses needed to preserve a right-nested
134
+ % subtraction tree, then parse the generated tokens back to the same AST.
135
+ dcg_expression_example(round_trip, Tokens) :-
136
+ AST = sub(lit(20), sub(lit(5), lit(3))),
137
+ phrase(emit_expression(AST), Tokens),
138
+ phrase(expression(AST), Tokens).
139
+
140
+ % phrase/3 lets a larger language parse an expression prefix and keep the rest.
141
+ dcg_expression_example(remainder, Rest) :-
142
+ phrase(expression(mul(var(x), lit(2))),
143
+ [x, '*', 2, then, stop], Rest).
144
+
145
+ % Missing closing parentheses are rejected.
146
+ dcg_expression_example(rejected, malformed_parentheses) :-
147
+ \+ phrase(expression(_), [2, '*', '(', 3, '+', 4]).
@@ -0,0 +1,5 @@
1
+ dcg_expression_example(parsed, add(lit(2), mul(lit(3), sub(lit(4), lit(1))))).
2
+ dcg_expression_example(evaluated, 19).
3
+ dcg_expression_example(round_trip, [20, -, '(', 5, -, 3, ')']).
4
+ dcg_expression_example(remainder, [then, stop]).
5
+ dcg_expression_example(rejected, malformed_parentheses).
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.14",
6
+ "version": "1.3.16",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/playground.html CHANGED
@@ -481,6 +481,7 @@
481
481
  "critical-path-schedule",
482
482
  "cyclic-path",
483
483
  "dcg-command-parser",
484
+ "dcg-expression-language",
484
485
  "d3-group",
485
486
  "dairy-energy-balance",
486
487
  "data-negotiation",
package/src/solver.js CHANGED
@@ -513,6 +513,23 @@ export class Solver {
513
513
  break;
514
514
  }
515
515
 
516
+ const betweenIterator = bundledBetweenIterator(this, group, goal, env);
517
+ if (betweenIterator != null) {
518
+ const firstResult = betweenIterator.next();
519
+ if (firstResult.done) break;
520
+ stack.push({
521
+ kind: 'resumeBuiltin',
522
+ iterator: betweenIterator,
523
+ goals: rest,
524
+ depth: depth + 1,
525
+ active,
526
+ });
527
+ goals = rest;
528
+ env = firstResult.value;
529
+ depth++;
530
+ continue;
531
+ }
532
+
516
533
  const memberIterator = bundledMemberIterator(this, group, goal, env);
517
534
  if (memberIterator != null) {
518
535
  const firstResult = memberIterator.next();
@@ -1216,6 +1233,53 @@ function pushWfsAnswerFrames(stack, model, group, goal, rest, env, depth, active
1216
1233
  }
1217
1234
  }
1218
1235
 
1236
+ function bundledBetweenIterator(solver, group, goal, env) {
1237
+ if (solver.registry.eyePrologLibrary !== true ||
1238
+ group.module !== 'prologue' || group.name !== 'between' || group.arity !== 3 ||
1239
+ group.bundledLibrary !== true || group.clauses.length !== 1) {
1240
+ return null;
1241
+ }
1242
+
1243
+ // The portable Prologue definition is intentionally kept as the semantic
1244
+ // source of between/3. Its recursive helper, however, carries the output
1245
+ // variable through one fresh clause instance per integer. With persistent
1246
+ // environments that builds a growing variable-alias chain, so dereferencing
1247
+ // the generated value in the caller repeatedly revisits all earlier frames.
1248
+ // Enumerate the canonical bundled relation directly while leaving user
1249
+ // definitions and non-EyeProlog registries on the ordinary Prolog path.
1250
+ return bundledBetweenSolutions(solver, goal, env);
1251
+ }
1252
+
1253
+ function requireBetweenInteger(term, env) {
1254
+ const value = deref(term, env);
1255
+ if (value.type === VAR) throw new PrologError('instantiation_error');
1256
+ if (value.type !== NUMBER || !isDecimalInteger(value.name)) {
1257
+ throw new PrologError('type_error(integer)', value);
1258
+ }
1259
+ return BigInt(value.name);
1260
+ }
1261
+
1262
+ function* bundledBetweenSolutions(solver, goal, env) {
1263
+ const lower = requireBetweenInteger(goal.args[0], env);
1264
+ const upper = requireBetweenInteger(goal.args[1], env);
1265
+ const requested = deref(goal.args[2], env);
1266
+
1267
+ if (requested.type !== VAR) {
1268
+ if (requested.type !== NUMBER || !isDecimalInteger(requested.name)) {
1269
+ throw new PrologError('type_error(integer)', requested);
1270
+ }
1271
+ const value = BigInt(requested.name);
1272
+ if (value >= lower && value <= upper) yield env;
1273
+ return;
1274
+ }
1275
+
1276
+ for (let value = lower; value <= upper; value++) {
1277
+ const next = env.clone();
1278
+ solver.stats.unify_calls++;
1279
+ if (unify(goal.args[2], numberTerm(value), next)) yield next;
1280
+ }
1281
+ }
1282
+
1219
1283
  function bundledMemberIterator(solver, group, goal, env) {
1220
1284
  if (solver.registry.eyePrologLibrary !== true ||
1221
1285
  !['lists', 'prologue'].includes(group.module) || group.name !== 'member' || group.arity !== 2 ||
@@ -2398,6 +2398,22 @@ child.stdin.write(\`consult(${consultedAtom}).\\n\`);
2398
2398
  assertEqual(result.stderr, '', 'stderr');
2399
2399
  },
2400
2400
  },
2401
+ {
2402
+ name: 'between/3 generated values avoid recursive environment chains (issue #52)',
2403
+ run: () => {
2404
+ const goalText = 'between(1, 1024, X), X < 0';
2405
+ const program = Program.parse('', { autoloadGoals: [goalText] });
2406
+ const solver = new Solver(program, { registry: getEyePrologRegistry() });
2407
+ const goal = parseGoalText(goalText, {
2408
+ operatorDefinitions: [...solver.program.operators.values()],
2409
+ });
2410
+ let answers = 0;
2411
+ for (const _env of solver.solve([goal], new Env(), 0)) answers++;
2412
+ assertEqual(answers, 0, 'positive generated values fail X < 0');
2413
+ assertEqual(solver.stats.unify_calls, 1024, 'one output unification per generated integer');
2414
+ assertEqual(solver.stats.max_depth <= 4, true, 'generation stays at bounded solver depth');
2415
+ },
2416
+ },
2401
2417
  {
2402
2418
  name: 'library(lists) length/2 stays relational and call_nth/2 autoloads (issue #28)',
2403
2419
  run: () => {
@@ -5632,6 +5632,62 @@ that argument. A variable body raises `instantiation_error`; a non-callable
5632
5632
  body raises `type_error(callable)`. EyeProlog performs terminal-sequence checks
5633
5633
  and reports the portable ISO `type_error(list)` error term.
5634
5634
 
5635
+ #### A bidirectional expression grammar
5636
+
5637
+ DCGs become more useful when the grammar produces a structured term rather than
5638
+ merely accepting a token list. The checked
5639
+ [`dcg-expression-language.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dcg-expression-language.pl)
5640
+ example implements a small arithmetic language in both directions. Its parser
5641
+ respects precedence and left associativity while constructing an abstract syntax
5642
+ tree:
5643
+
5644
+ ```text
5645
+ expression(AST) -->
5646
+ term(First),
5647
+ additive_tail(First, AST).
5648
+
5649
+ additive_tail(Left, AST) -->
5650
+ ['+'], term(Right),
5651
+ { Next = add(Left, Right) },
5652
+ additive_tail(Next, AST).
5653
+ additive_tail(AST, AST) --> [].
5654
+ ```
5655
+
5656
+ The accumulator removes left recursion without moving parsing into JavaScript.
5657
+ A second DCG walks the AST in the other direction and emits only the parentheses
5658
+ needed to preserve its structure. The example therefore exercises parsing,
5659
+ semantic actions, nonterminal-to-nonterminal state hand-off, generation,
5660
+ backtracking, `phrase/3` remainder handling, and AST-to-token-to-AST
5661
+ round-tripping. The checked answers are in
5662
+ [`examples/output/dcg-expression-language.pl`](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dcg-expression-language.pl).
5663
+
5664
+ #### Deep sequence hand-off
5665
+
5666
+ `library(iso_ext)` provides the common `...//0` helper, which describes an
5667
+ arbitrary number of input elements. It is not part of ISO Part 3, but it is a
5668
+ useful interoperability and stress-test relation. A compact hand-off test is:
5669
+
5670
+ ```text
5671
+ a --> ..., epsilon.
5672
+ epsilon --> [].
5673
+ ```
5674
+
5675
+ Here the remaining sequence is repeatedly passed from `...//0` to another
5676
+ nonterminal. For a finite compact list, EyeProlog can scan the arbitrary
5677
+ sequence iteratively instead of consuming one ordinary solver depth level per
5678
+ list cell. If the continuation is structurally proven to be a zero-width
5679
+ identity grammar such as `epsilon//0`, the hand-off can be continued without
5680
+ constructing a fresh general clause-resolution frame at every suffix. The list
5681
+ spine is still traversed; this is a control/allocation optimization rather than
5682
+ an O(1) semantic shortcut.
5683
+
5684
+ The optimization is deliberately narrow. `phrase(..., Sequence, Rest)` still
5685
+ enumerates the valid remainders, open or non-compact inputs retain ordinary
5686
+ relational behavior, and grammars that can consume or constrain the remainder
5687
+ are not treated as identity continuations. `time/1` can be used in normal mode
5688
+ to measure such runs; its inference counter records solver-level inferences and
5689
+ does not count every internal step of an optimized scanner.
5690
+
5635
5691
  Part 3 leaves `\+//1` and standalone `->//2` implementation dependent.
5636
5692
  EyeProlog uses non-consuming negation (`\+ Body` tests from the current state)
5637
5693
  and threads the state produced by the condition into the then-grammar.
@@ -6895,7 +6951,7 @@ Review questions:
6895
6951
  </figure>
6896
6952
 
6897
6953
  The [examples directory](https://github.com/eyereasoner/eyeprolog/tree/main/examples/) is the book's executable companion. The
6898
- top-level directory contains **211 self-contained runnable programs**. Every
6954
+ top-level directory contains **212 self-contained runnable programs**. Every
6899
6955
  source program has an exact answer file under
6900
6956
  [examples/output](https://github.com/eyereasoner/eyeprolog/tree/main/examples/output/), and **61 selected programs** have a checked
6901
6957
  explanation under [examples/proof](https://github.com/eyereasoner/eyeprolog/tree/main/examples/proof/). The thematic tables below link every top-level program and open the program
@@ -6953,6 +7009,7 @@ mode at a time.
6953
7009
  | [Atomic conversion](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-atomic-conversion.pl) | Atom splitting, character atoms, Unicode codes, and numeric parsing. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-atomic-conversion.pl) |
6954
7010
  | [Control and errors](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-control-and-errors.pl) | `call/1`, `once/1`, cut, if-then-else, `throw/1`, and `catch/3`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-control-and-errors.pl) |
6955
7011
  | [DCG command parser](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dcg-command-parser.pl) | A Part 3 grammar parses token lists into application terms, generates tokens, preserves a remainder, and rejects malformed input. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dcg-command-parser.pl) |
7012
+ | [DCG expression language](https://github.com/eyereasoner/eyeprolog/blob/main/examples/dcg-expression-language.pl) | A precedence-aware bidirectional grammar builds arithmetic ASTs, evaluates variable expressions, regenerates minimally parenthesized tokens, round-trips syntax, and preserves a remainder. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/dcg-expression-language.pl) |
6956
7013
  | [Dynamic database](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-dynamic-database.pl) | Initialization and ordered updates to a declared dynamic procedure. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-dynamic-database.pl) · [proof](https://github.com/eyereasoner/eyeprolog/blob/main/examples/proof/iso-dynamic-database.pl) |
6957
7014
  | [Grouped solutions](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-grouped-solutions.pl) | `findall/3`, `bagof/3`, `setof/3`, existential qualification, and `clause/2`. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-grouped-solutions.pl) |
6958
7015
  | [Integer arithmetic](https://github.com/eyereasoner/eyeprolog/blob/main/examples/iso-integer-arithmetic.pl) | Integer quotient/remainder choices plus bit operations. | [answers](https://github.com/eyereasoner/eyeprolog/blob/main/examples/output/iso-integer-arithmetic.pl) |
@@ -7297,7 +7354,7 @@ hand.
7297
7354
 
7298
7355
  #### Running and extending the corpus
7299
7356
 
7300
- Run all 211 normal answer goldens and the 61 selected proof goldens with:
7357
+ Run all 212 normal answer goldens and the 61 selected proof goldens with:
7301
7358
 
7302
7359
  ```sh
7303
7360
  npm run test:examples
@@ -7361,7 +7418,7 @@ The complete suite must pass before release. The file-based conformance corpus
7361
7418
  contains 791 cases, including 386 focused ISO
7362
7419
  cases derived from the success, failure, mode, and error behavior in
7363
7420
  ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
7364
- Separate exact-output suites check 211 normal
7421
+ Separate exact-output suites check 212 normal
7365
7422
  examples and 61 proof examples; all extracted book programs are parsed and
7366
7423
  their declared goals are executed. The eight-case
7367
7424
  playground contract suite imports the production worker, sends real reasoning
package/why-eyeprolog.md CHANGED
@@ -60,6 +60,14 @@ positive Datalog closures with a shared relation-wide table, but the admission
60
60
  heuristics are implementation details. Programs should rely on the documented
61
61
  semantics and finiteness conditions, not on a particular internal threshold.
62
62
 
63
+ DCGs follow the same rule. Their meaning remains the ISO Part 3 difference-list
64
+ model, while finite sequence scans and proven zero-width hand-offs may use
65
+ lighter internal control paths so deep grammars do not pay one general solver
66
+ frame per token. Relational remainder-producing modes are preserved. The
67
+ checked `examples/dcg-expression-language.pl` program shows the declarative side
68
+ of that design: one grammar builds precedence-aware syntax trees and another
69
+ generates minimally parenthesized token sequences back from them.
70
+
63
71
  ## Why proofs?
64
72
 
65
73
  An answer says that a goal succeeded. A proof records one successful route