eyeprolog 1.3.41 → 1.3.42

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
@@ -221,10 +221,18 @@ newly upgraded cases run directly against the upstream Codex expectation.
221
221
  EyeProlog does not yet claim independent certification or closure of every
222
222
  normative Part 1 requirement.
223
223
 
224
- ## ISO modules and definite clause grammars
224
+ ## Module and definite clause grammar compatibility profiles
225
225
 
226
- EyeProlog implements ISO/IEC 13211-2 modules and the grammar rules and
227
- `phrase/2-3` predicates of ISO/IEC TS 13211-3:2025. For example:
226
+ Normal EyeProlog supports the widely used `module/2`, `use_module/1-2`,
227
+ `meta_predicate/1`, and `Module:Goal` interface reflected in later WG17 module
228
+ amendment work. This is a practical module compatibility profile; EyeProlog
229
+ does **not** currently claim a clause-by-clause implementation or certification
230
+ of the complete ISO/IEC 13211-2:2000 module model.
231
+
232
+ Definite clause grammar support follows the ISO/IEC TS 13211-3 grammar-rule and
233
+ `phrase/2-3` model and is exercised by the conformance corpus. As with Part 1,
234
+ that implementation evidence is not an independent certification of every
235
+ Part 3 requirement. For example:
228
236
 
229
237
  ```prolog
230
238
  sentence --> [hello], noun.
@@ -254,7 +262,7 @@ EyeProlog also adds 128 public library predicate indicators to its 129-entry ISO
254
262
  profile. **88 are implemented entirely as ordinary Prolog clauses** in focused
255
263
  modules under `src/lib/`; the remaining control predicates and finite-domain
256
264
  `library(clpz)` kernel use backtrackable host support.
257
- They are ISO/IEC 13211-2 modules loaded explicitly by purpose, such as
265
+ They are ordinary Prolog modules using EyeProlog's documented module compatibility surface, loaded explicitly by purpose, such as
258
266
  `library(lists)`, `library(lambda)`, `library(strings)`, `library(aggregate)`, or
259
267
  `library(clpz)`.
260
268
  Portable text
@@ -234,7 +234,7 @@ npm run generate
234
234
  ## Chapter 38: Language and ISO profile
235
235
 
236
236
  - [01-city.pl](chapter-38/01-city.pl)
237
- - [02-look_ahead.pl](chapter-38/02-look_ahead.pl) — ISO Part 3 definite clause grammars
237
+ - [02-look_ahead.pl](chapter-38/02-look_ahead.pl) — Part 3-oriented definite clause grammars
238
238
 
239
239
  ## Chapter 39: Built-in predicates by programming role
240
240
 
@@ -1,2 +1,2 @@
1
- % From The Art of EyeProlog, Chapter 38 — ISO Part 3 definite clause grammars.
1
+ % From The Art of EyeProlog, Chapter 38 — Part 3-oriented definite clause grammars.
2
2
  look_ahead(X), [X] --> [X].
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.3.41",
6
+ "version": "1.3.42",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
package/src/iso.js CHANGED
@@ -662,6 +662,11 @@ function* currentPrologFlagBuiltin({ solver, goal, env }) {
662
662
  throw new PrologError('domain_error(prolog_flag)', flag);
663
663
  }
664
664
  for (const [name, definition] of solver.prologFlags) {
665
+ // ISO 7.11.1.1: when bounded=false, max_integer and min_integer have no
666
+ // current value and current_prolog_flag/2 must therefore not enumerate
667
+ // them. The definitions remain registered so attempts to change these
668
+ // non-changeable flags still receive the normal flag error handling.
669
+ if (definition.value == null) continue;
665
670
  const next = env.clone();
666
671
  if (unify(goal.args[0], atom(name), next) && unify(goal.args[1], definition.value, next)) yield next;
667
672
  }
package/src/parser.js CHANGED
@@ -207,7 +207,11 @@ class Parser {
207
207
  this.strictIso = options.isoStrict === true;
208
208
  this.parserFlagState = options.parserFlagState ?? {
209
209
  doubleQuotes: options.doubleQuotes ?? 'chars',
210
+ charConversion: 'on',
211
+ charConversions: new Map(),
210
212
  };
213
+ this.parserFlagState.charConversion ??= 'on';
214
+ this.parserFlagState.charConversions ??= new Map();
211
215
  if (!['chars', 'codes', 'atom'].includes(this.parserFlagState.doubleQuotes)) {
212
216
  throw new Error(`invalid double_quotes parser flag: ${this.parserFlagState.doubleQuotes}`);
213
217
  }
@@ -223,6 +227,26 @@ class Parser {
223
227
  this.previousToken = null;
224
228
  this.token = this.nextToken();
225
229
  }
230
+ convertCharacter(character) {
231
+ if (this.parserFlagState.charConversion !== 'on' || !character) return character;
232
+ return this.parserFlagState.charConversions.get(character) ?? character;
233
+ }
234
+ rawPeek(offset = 0) {
235
+ return this.source[this.pos + offset] ?? '';
236
+ }
237
+ rawTake() {
238
+ const ch = this.rawPeek();
239
+ if (ch) {
240
+ this.pos++;
241
+ if (ch === '\n') this.line++;
242
+ }
243
+ return ch;
244
+ }
245
+ convertedSlice(start, end) {
246
+ let out = '';
247
+ for (const ch of this.source.slice(start, end)) out += this.convertCharacter(ch);
248
+ return out;
249
+ }
226
250
  terminatingFullStop(index = this.pos) {
227
251
  // read/1 tries each possible full stop in turn. While parsing a later
228
252
  // candidate, a preceding dot after a graphic character belongs to that
@@ -231,12 +255,12 @@ class Parser {
231
255
  // designated read-term candidate.
232
256
  if (this.readTermEnd != null) {
233
257
  if (index === this.readTermEnd) return true;
234
- if (index < this.readTermEnd && continuesGraphicToken(this.source, index)) {
235
- const next = this.source[index + 1] ?? '';
258
+ if (index < this.readTermEnd && continuesGraphicToken(this.source, index, (ch) => this.convertCharacter(ch))) {
259
+ const next = this.convertCharacter(this.source[index + 1] ?? '');
236
260
  if (next === '%' || /^[\u0009-\u000d\u0020]$/.test(next)) return false;
237
261
  }
238
262
  }
239
- return isTerminatingFullStop(this.source, index);
263
+ return isTerminatingFullStop(this.source, index, (ch) => this.convertCharacter(ch));
240
264
  }
241
265
  defineOperator(priority, specifier, name) {
242
266
  defineParserOperator(this, priority, specifier, name);
@@ -273,12 +297,30 @@ class Parser {
273
297
  for (const name of names) this.defineOperator(priority, specifierTerm.name, name);
274
298
  return true;
275
299
  }
276
- applyParserFlagDirective(directive) {
277
- if (directive.type !== 'compound' || directive.name !== 'set_prolog_flag' || directive.arity !== 2) return;
278
- const [flag, value] = directive.args;
279
- if (flag.type === 'atom' && flag.name === 'double_quotes' &&
280
- value.type === 'atom' && ['chars', 'codes', 'atom'].includes(value.name)) {
281
- this.parserFlagState.doubleQuotes = value.name;
300
+ applyParserFlagDirective(directive, line = this.line) {
301
+ if (directive.type !== 'compound' || directive.arity !== 2) return;
302
+ if (directive.name === 'set_prolog_flag') {
303
+ const [flag, value] = directive.args;
304
+ if (flag.type === 'atom' && flag.name === 'double_quotes' &&
305
+ value.type === 'atom' && ['chars', 'codes', 'atom'].includes(value.name)) {
306
+ this.parserFlagState.doubleQuotes = value.name;
307
+ } else if (flag.type === 'atom' && flag.name === 'char_conversion' &&
308
+ value.type === 'atom' && ['on', 'off'].includes(value.name)) {
309
+ this.parserFlagState.charConversion = value.name;
310
+ }
311
+ return;
312
+ }
313
+ if (directive.name === 'char_conversion') {
314
+ const [input, output] = directive.args;
315
+ const validCharacter = (term) => term.type === 'atom' && Array.from(term.name).length === 1;
316
+ if (input.type === 'var' || output.type === 'var') {
317
+ throw new Error(`parse line ${line}: char_conversion/2 arguments must be instantiated characters`);
318
+ }
319
+ if (!validCharacter(input) || !validCharacter(output)) {
320
+ throw new Error(`parse line ${line}: char_conversion/2 requires one-character atoms`);
321
+ }
322
+ if (input.name === output.name) this.parserFlagState.charConversions.delete(input.name);
323
+ else this.parserFlagState.charConversions.set(input.name, output.name);
282
324
  }
283
325
  }
284
326
  applyImportedLibraryOperators(directive) {
@@ -301,46 +343,45 @@ class Parser {
301
343
  return null;
302
344
  }
303
345
  peek(offset = 0) {
304
- return this.source[this.pos + offset] ?? '';
346
+ return this.convertCharacter(this.rawPeek(offset));
305
347
  }
306
348
  take() {
307
- const ch = this.peek();
308
- if (ch) {
349
+ const raw = this.rawPeek();
350
+ const ch = this.convertCharacter(raw);
351
+ if (raw) {
309
352
  this.pos++;
310
- if (ch === '\n') this.line++;
353
+ if (raw === '\n') this.line++;
311
354
  }
312
355
  return ch;
313
356
  }
314
357
  skipWhitespaceAndComments() {
315
- const source = this.source;
316
- const len = source.length;
317
358
  while (true) {
318
- while (this.pos < len) {
319
- const code = source.charCodeAt(this.pos);
320
- if (!isWhitespaceCode(code)) break;
321
- if (code === 10) this.line++;
322
- this.pos++;
359
+ while (this.rawPeek()) {
360
+ const ch = this.peek();
361
+ if (!isWhitespaceCode(ch.charCodeAt(0))) break;
362
+ this.take();
323
363
  }
324
- if (source.charCodeAt(this.pos) === 37) { // % line comment
325
- while (this.pos < len && source.charCodeAt(this.pos) !== 10) this.pos++;
364
+ if (this.peek() === '%') {
365
+ while (this.rawPeek() && this.peek() !== '\n') this.take();
326
366
  continue;
327
367
  }
328
- if (source[this.pos] === '/' && source[this.pos + 1] === '*') {
368
+ if (this.peek() === '/' && this.peek(1) === '*') {
329
369
  const line = this.line;
330
- this.pos += 2;
331
- while (this.pos < len && !(source[this.pos] === '*' && source[this.pos + 1] === '/')) {
332
- if (source.charCodeAt(this.pos) === 10) this.line++;
333
- this.pos++;
334
- }
335
- if (this.pos >= len) throw new Error(`parse line ${line}: unterminated block comment`);
336
- this.pos += 2;
370
+ this.take();
371
+ this.take();
372
+ while (this.rawPeek() && !(this.peek() === '*' && this.peek(1) === '/')) this.take();
373
+ if (!this.rawPeek()) throw new Error(`parse line ${line}: unterminated block comment`);
374
+ this.take();
375
+ this.take();
337
376
  continue;
338
377
  }
339
378
  break;
340
379
  }
341
380
  }
342
381
  readEscape(line, options = {}) {
343
- const escaped = this.take();
382
+ const takeChar = () => options.raw ? this.rawTake() : this.take();
383
+ const peekChar = () => options.raw ? this.rawPeek() : this.peek();
384
+ const escaped = takeChar();
344
385
  if (!escaped) throw new Error(`parse line ${line}: unterminated escape sequence`);
345
386
 
346
387
  // ISO 6.4.2 permits a continuation escape only inside quoted tokens: a
@@ -350,9 +391,9 @@ class Parser {
350
391
  if (options.allowContinuation !== false) return '';
351
392
  throw new Error(`parse line ${line}: bad escape sequence`);
352
393
  }
353
- if (escaped === '\r' && this.peek() === '\n') {
394
+ if (escaped === '\r' && peekChar() === '\n') {
354
395
  if (options.allowContinuation !== false) {
355
- this.take();
396
+ takeChar();
356
397
  return '';
357
398
  }
358
399
  throw new Error(`parse line ${line}: bad escape sequence`);
@@ -363,8 +404,8 @@ class Parser {
363
404
 
364
405
  if (escaped === 'x') {
365
406
  let digits = '';
366
- while (/^[0-9A-Fa-f]$/.test(this.peek())) digits += this.take();
367
- if (!digits || this.take() !== '\\') throw new Error(`parse line ${line}: bad hexadecimal escape`);
407
+ while (/^[0-9A-Fa-f]$/.test(peekChar())) digits += takeChar();
408
+ if (!digits || takeChar() !== '\\') throw new Error(`parse line ${line}: bad hexadecimal escape`);
368
409
  const code = Number.parseInt(digits, 16);
369
410
  if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
370
411
  throw new Error(`parse line ${line}: character escape out of range`);
@@ -373,8 +414,8 @@ class Parser {
373
414
  }
374
415
  if (/^[0-7]$/.test(escaped)) {
375
416
  let digits = escaped;
376
- while (/^[0-7]$/.test(this.peek())) digits += this.take();
377
- if (this.take() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
417
+ while (/^[0-7]$/.test(peekChar())) digits += takeChar();
418
+ if (takeChar() !== '\\') throw new Error(`parse line ${line}: bad octal escape`);
378
419
  const code = Number.parseInt(digits, 8);
379
420
  if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) {
380
421
  throw new Error(`parse line ${line}: character escape out of range`);
@@ -411,7 +452,7 @@ class Parser {
411
452
  this.take();
412
453
  while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
413
454
  !this.terminatingFullStop()) this.take();
414
- return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
455
+ return { type: TOK.ATOM, text: this.convertedSlice(start, this.pos), line };
415
456
  }
416
457
  if (ch === '!') {
417
458
  this.take();
@@ -442,20 +483,29 @@ class Parser {
442
483
  }
443
484
 
444
485
  if (ch === '"' || ch === "'") {
486
+ // Whether an input character is quoted is determined from the target
487
+ // source before character conversion (ISO 8.14.1 note 1). A literal
488
+ // quote therefore protects its contents from Convc; if a conversion
489
+ // itself produces a quote token, the following raw characters remain
490
+ // subject to conversion while that resulting token is parsed.
491
+ const rawOpening = this.rawPeek();
445
492
  const quote = this.take();
493
+ const literalQuote = rawOpening === quote;
494
+ const peekQuoted = () => literalQuote ? this.rawPeek() : this.peek();
495
+ const takeQuoted = () => literalQuote ? this.rawTake() : this.take();
446
496
  let text = '';
447
497
  while (true) {
448
- if (!this.peek()) throw new Error(`parse line ${line}: unterminated quoted term`);
449
- let value = this.take();
498
+ if (!peekQuoted()) throw new Error(`parse line ${line}: unterminated quoted term`);
499
+ let value = takeQuoted();
450
500
  if (value === quote) {
451
- if (this.peek() === quote) {
452
- this.take();
501
+ if (peekQuoted() === quote) {
502
+ takeQuoted();
453
503
  value = quote;
454
504
  } else {
455
505
  break;
456
506
  }
457
- } else if (value === '\\' && this.peek()) {
458
- value = this.readEscape(line);
507
+ } else if (value === '\\' && peekQuoted()) {
508
+ value = this.readEscape(line, { raw: literalQuote });
459
509
  } else if (value !== ' ' && isWhitespaceCode(value.charCodeAt(0))) {
460
510
  // ISO 6.4.2.1 allows an ordinary space in a quoted character, but
461
511
  // not literal layout characters such as tab or newline. Newlines
@@ -489,17 +539,20 @@ class Parser {
489
539
  (this.peek(2) !== "'" || this.peek(3) === "'") &&
490
540
  !(this.peek(2) === '\\' && this.peek(3) === '\n');
491
541
  if (startsQuotedCharacter) {
542
+ const rawQuotedCharacter = this.rawPeek() === '0' && this.rawPeek(1) === "'";
492
543
  this.take();
493
544
  this.take();
494
- let value = this.take();
545
+ const takeCharacter = () => rawQuotedCharacter ? this.rawTake() : this.take();
546
+ const peekCharacter = () => rawQuotedCharacter ? this.rawPeek() : this.peek();
547
+ let value = takeCharacter();
495
548
  if (value) {
496
549
  const firstCode = value.charCodeAt(0);
497
550
  if (firstCode >= 0xd800 && firstCode <= 0xdbff) {
498
- const secondCode = this.peek().charCodeAt(0);
551
+ const secondCode = peekCharacter().charCodeAt(0);
499
552
  if (secondCode < 0xdc00 || secondCode > 0xdfff) {
500
553
  throw new Error(`parse line ${line}: bad character code constant`);
501
554
  }
502
- value += this.take();
555
+ value += takeCharacter();
503
556
  } else if (firstCode >= 0xdc00 && firstCode <= 0xdfff) {
504
557
  throw new Error(`parse line ${line}: bad character code constant`);
505
558
  }
@@ -512,10 +565,10 @@ class Parser {
512
565
  // apostrophe is doubled just as it is inside a quoted atom. Thus
513
566
  // 0''' is one numeric token denoting character code 39, while the
514
567
  // undoubled 0'' is not a complete single quoted character.
515
- if (this.peek() !== "'") throw new Error(`parse line ${line}: bad character code constant`);
516
- this.take();
568
+ if (peekCharacter() !== "'") throw new Error(`parse line ${line}: bad character code constant`);
569
+ takeCharacter();
517
570
  } else if (value === '\\') {
518
- value = this.readEscape(line, { allowContinuation: false });
571
+ value = this.readEscape(line, { allowContinuation: false, raw: rawQuotedCharacter });
519
572
  }
520
573
  const code = value.codePointAt(0);
521
574
  return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
@@ -550,14 +603,14 @@ class Parser {
550
603
  // followed by the name E9 and is not a valid term without an operator.
551
604
  if (hasFraction && (this.peek() === 'e' || this.peek() === 'E')) {
552
605
  let idx = this.pos + 1;
553
- if (this.source[idx] === '+' || this.source[idx] === '-') idx++;
554
- if (isDigitCode((this.source[idx] ?? '').charCodeAt(0))) {
606
+ if (['+', '-'].includes(this.convertCharacter(this.source[idx] ?? ''))) idx++;
607
+ if (isDigitCode(this.convertCharacter(this.source[idx] ?? '').charCodeAt(0))) {
555
608
  this.take();
556
609
  if (this.peek() === '+' || this.peek() === '-') this.take();
557
610
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
558
611
  }
559
612
  }
560
- let text = this.source.slice(start, this.pos);
613
+ let text = this.convertedSlice(start, this.pos);
561
614
  if (!hasFraction) text = BigInt(text).toString();
562
615
  else text = finiteFloatTokenText(text);
563
616
  return { type: TOK.NUMBER, text, line };
@@ -567,7 +620,7 @@ class Parser {
567
620
  const start = this.pos;
568
621
  this.take();
569
622
  while (isNameContinueCode(this.peek().charCodeAt(0))) this.take();
570
- const text = this.source.slice(start, this.pos);
623
+ const text = this.convertedSlice(start, this.pos);
571
624
  return { type: TOK.VAR, text, line };
572
625
  }
573
626
 
@@ -575,7 +628,7 @@ class Parser {
575
628
  const start = this.pos;
576
629
  this.take();
577
630
  while (isNameContinueCode(this.peek().charCodeAt(0))) this.take();
578
- return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
631
+ return { type: TOK.ATOM, text: this.convertedSlice(start, this.pos), line };
579
632
  }
580
633
 
581
634
  if (isGraphicAtomCode(ch.charCodeAt(0))) {
@@ -583,7 +636,7 @@ class Parser {
583
636
  this.take();
584
637
  while (isGraphicAtomCode(this.peek().charCodeAt(0)) &&
585
638
  !this.terminatingFullStop()) this.take();
586
- return { type: TOK.ATOM, text: this.source.slice(start, this.pos), line };
639
+ return { type: TOK.ATOM, text: this.convertedSlice(start, this.pos), line };
587
640
  }
588
641
 
589
642
  throw new Error(`parse line ${line}: bad character ${JSON.stringify(ch)}`);
@@ -934,7 +987,7 @@ class Parser {
934
987
  throw new Error(`parse line ${line}: bad term`);
935
988
  }
936
989
  this.expect(TOK.DOT, '.');
937
- this.applyParserFlagDirective(directive);
990
+ this.applyParserFlagDirective(directive, line);
938
991
  this.applyImportedLibraryOperators(directive);
939
992
  this.advance();
940
993
  const clause = { head: compound(':-', [directive]), body: [] };
@@ -1080,12 +1133,16 @@ export function parseClauses(source, options = {}) {
1080
1133
  const ownsParserFlagState = options.parserFlagState == null;
1081
1134
  const initialDoubleQuotes = options.doubleQuotes ?? 'chars';
1082
1135
  const parserOptions = ownsParserFlagState
1083
- ? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes } }
1136
+ ? { ...options, parserFlagState: { doubleQuotes: initialDoubleQuotes, charConversion: 'on', charConversions: new Map() } }
1084
1137
  : options;
1085
1138
  if (options.sourceMetadata === false && options.readTermEnd == null) {
1086
1139
  const clauses = parseClausesFastNoSource(source, null, null, parserOptions);
1087
1140
  if (clauses) return clauses;
1088
- if (ownsParserFlagState) parserOptions.parserFlagState.doubleQuotes = initialDoubleQuotes;
1141
+ if (ownsParserFlagState) {
1142
+ parserOptions.parserFlagState.doubleQuotes = initialDoubleQuotes;
1143
+ parserOptions.parserFlagState.charConversion = 'on';
1144
+ parserOptions.parserFlagState.charConversions.clear();
1145
+ }
1089
1146
  }
1090
1147
  return new Parser(source, parserOptions).parseProgram();
1091
1148
  }
@@ -1381,11 +1438,19 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
1381
1438
  return head && bodyGoal ? { head, body: [bodyGoal] } : null;
1382
1439
  };
1383
1440
 
1441
+ const preparationConversionActive = () =>
1442
+ options.parserFlagState?.charConversion === 'on' &&
1443
+ (options.parserFlagState?.charConversions?.size ?? 0) > 0;
1444
+
1384
1445
  const flush = () => {
1385
1446
  const text = chunk.trim();
1386
1447
  chunk = '';
1387
1448
  if (!text) return true;
1388
- const simple = parseSimple(text);
1449
+ // Once a preparation-time char_conversion/2 mapping is active, every
1450
+ // subsequent source character must pass through the full tokenizer. The
1451
+ // compact parser deliberately operates on raw source ranges, so using it
1452
+ // here would silently bypass Convc for otherwise simple clauses.
1453
+ const simple = preparationConversionActive() ? null : parseSimple(text);
1389
1454
  if (simple) {
1390
1455
  accept(simple);
1391
1456
  return true;
@@ -1408,7 +1473,7 @@ function parseClausesFastNoSource(source, emit = null, emitBinary = null, option
1408
1473
  if (contentEnd > contentStart && source.charCodeAt(contentEnd - 1) === 13) contentEnd--;
1409
1474
  [contentStart, contentEnd] = trimRange(source, contentStart, contentEnd);
1410
1475
  if (contentStart < contentEnd && source.charCodeAt(contentStart) !== 37) {
1411
- if (!chunk && source.charCodeAt(contentEnd - 1) === 46) {
1476
+ if (!chunk && source.charCodeAt(contentEnd - 1) === 46 && !preparationConversionActive()) {
1412
1477
  if (emitFastBinaryRange(source, contentStart, contentEnd)) {
1413
1478
  // The program builder accepted a compact binary clause directly.
1414
1479
  } else {
package/src/program.js CHANGED
@@ -650,7 +650,7 @@ export function autoloadProgramGoals(program, inputs, options = {}) {
650
650
  false,
651
651
  { isoStrict: program.strictIso },
652
652
  );
653
- const parserFlagState = { doubleQuotes: program.doubleQuotes ?? options.doubleQuotes ?? 'chars' };
653
+ const parserFlagState = { doubleQuotes: program.doubleQuotes ?? options.doubleQuotes ?? 'chars', charConversion: 'on', charConversions: new Map() };
654
654
  const goals = parseInteropGoalInputs(inputs, {
655
655
  ...options,
656
656
  isoStrict: program.strictIso,
@@ -684,7 +684,7 @@ function loadSourcesIntoBuilder(builder, sources, options, fast) {
684
684
  const ensured = new Set();
685
685
  const loadedModules = new Set();
686
686
  const operatorState = createParserOperatorState([], true, { isoStrict: options.isoStrict === true });
687
- const parserFlagState = { doubleQuotes: options.doubleQuotes ?? 'chars' };
687
+ const parserFlagState = { doubleQuotes: options.doubleQuotes ?? 'chars', charConversion: 'on', charConversions: new Map() };
688
688
  const prepared = sources.map((source) => ({
689
689
  source,
690
690
  options: { ...sourceOptionsFor(source, options), operatorState, parserFlagState },
package/src/solver.js CHANGED
@@ -955,8 +955,8 @@ function defaultPrologFlags(unknown = 'error', strictIso = false) {
955
955
  ['integer_rounding_function', { value: compound('toward_zero', []), allowed: ['toward_zero'], changeable: false }],
956
956
  ['char_conversion', { value: compound('on', []), allowed: ['on', 'off'], changeable: true }],
957
957
  ['debug', { value: compound('off', []), allowed: ['on', 'off'], changeable: true }],
958
- ['max_integer', { value: compound('unbounded', []), allowed: ['unbounded'], changeable: false }],
959
- ['min_integer', { value: compound('unbounded', []), allowed: ['unbounded'], changeable: false }],
958
+ ['max_integer', { value: null, allowed: ['unbounded'], changeable: false }],
959
+ ['min_integer', { value: null, allowed: ['unbounded'], changeable: false }],
960
960
  ['max_arity', { value: compound('unbounded', []), allowed: ['unbounded'], changeable: false }],
961
961
  ['unknown', { value: compound(unknown, []), allowed: ['error', 'fail', 'warning'], changeable: true }],
962
962
  ['double_quotes', { value: compound('chars', []), allowed: ['chars', 'codes', 'atom'], changeable: true }],
@@ -1,4 +1,4 @@
1
- // ISO/IEC 13211-2 module sources shipped with EyeProlog.
1
+ // Prolog library modules shipped with EyeProlog and loaded through its documented module compatibility surface.
2
2
  // The sources are registered here so library(Name) works in Node and browsers.
3
3
  // Modules are loaded on demand by explicit use_module/1-2 or by the conservative
4
4
  // source-level interop autoloader declared below.
@@ -32,13 +32,13 @@ error-ordering alternative to an individual executable assertion.
32
32
  | --- | --- | --- |
33
33
  | Clause 6 — tokens, terms, lists, operators, quoted text | audit | Complete vendored WG17 syntax matrix, `lexical_and_curly_terms`, `scryer_lexical_terms`, operator suites, syntax-error cases, quoted-layout/escape error cases, and writer/read-back regressions. |
34
34
  | 7.1-7.3 — term types, term order, unification | audit | Standard-order, identity, finite-tree and occurs-check suites, Corrigendum 2 term predicates. |
35
- | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Cross-text `multifile/1` and ordering constraints require explicit shall-by-shall audit. |
35
+ | 7.4 — Prolog text and directives | audit | All Part 1 directive indicators are parsed; include/ensure-loaded/operator/flag/character-conversion behavior has executable coverage. Preparation-time `char_conversion/2` now affects later unquoted source text and respects `char_conversion=off`. Cross-text `multifile/1` and ordering constraints still require explicit shall-by-shall audit. |
36
36
  | 7.5-7.6 — database and term/clause conversion | audit | Dynamic database and logical-update-view suites. Strict mode restores Part 1 private-static/public-dynamic `clause/2` access. Public/private and multi-text requirements still need complete mapping. |
37
37
  | 7.7 — execution and backtracking | audit | Control/search suites. Strict mode disables EyeProlog automatic tabling, cycle guards, and recursive numeric shortcuts so core execution uses ordinary clause selection/backtracking. |
38
38
  | 7.8 — control constructs and exceptions | audit | call, cut, conjunction, disjunction (including failed branches after callee-local cuts), if-then, catch/throw, renamed-copy tests. |
39
39
  | 7.9 — expression evaluation | audit | Arithmetic/evaluation/error suites, including Corrigenda. Exceptional-value/error-precedence matrix remains to be exhaustively enumerated. |
40
40
  | 7.10 — input/output concepts | audit | Stream, character/byte I/O, read/write options, operator-sensitive write-back, Corrigendum 3 writer cases. Full option cross-product remains open. |
41
- | 7.11 — flags | audit | Required Part 1 flags implemented. Normal and strict modes use the ISO `unknown=error` default; strict mode excludes the EyeProlog `occurs_check` extension. |
41
+ | 7.11 — flags | audit | Required Part 1 flags implemented. Normal and strict modes use the ISO `unknown=error` default; strict mode excludes the EyeProlog `occurs_check` extension. With `bounded=false`, `max_integer` and `min_integer` correctly have no current value. |
42
42
  | 7.12 — errors | audit | ISO `error(Error, Context)` envelope, type/domain/permission/representation/evaluation/syntax families and focused error cases. Complete prescribed-error ordering remains open. |
43
43
  | 8.2-8.17 — built-in predicates | audit | Predicate-family coverage is mapped in ISO-MATRIX.md; Corrigendum 2 additions (`subsumes_term/2`, `term_variables/2`, `call/2..8`, `false/0`) are in the strict registry. Mode/error matrix is not yet one-row-per-standard-row. |
44
44
  | Clause 9 — evaluable functors | audit | Integer/float/rounding/transcendental/bitwise suites and corrigendum cases. Host floating-point representation choices remain documented implementation-defined behavior. |
@@ -46,6 +46,24 @@ error-ordering alternative to an individual executable assertion.
46
46
  | Corrigendum 2 | covered | Added predicates/functors, catch corrections, bar/operator and uninstantiation corrections have dedicated cases. |
47
47
  | Corrigendum 3 | covered | Writer options, `variable_names/1`, canonical list output and negative-power corrections have dedicated cases. |
48
48
 
49
+ ## Issue #65 conformance corrections
50
+
51
+ The issue #65 audit against the licensed Part 1 text and Corrigenda closed two
52
+ concrete mismatches without changing the remaining audit rows into blanket
53
+ conformance claims:
54
+
55
+ - `bounded=false` no longer exposes implementation-specific `unbounded` values
56
+ for `max_integer` or `min_integer`; the corresponding
57
+ `current_prolog_flag/2` queries fail as specified by 7.11.1.1;
58
+ - preparation-time `char_conversion/2` now converts later unquoted source text
59
+ when the `char_conversion` flag is `on`, leaves quoted characters unchanged,
60
+ and feeds the same mapping into execution-time term input.
61
+
62
+ The processor-character-set/collation rows in
63
+ [ISO-IMPLEMENTATION-DEFINED.md](ISO-IMPLEMENTATION-DEFINED.md) remain explicit
64
+ `audit gap`s. They were not papered over by narrowing strict mode without full
65
+ WG17 and read/write/error validation.
66
+
49
67
  ## Strict-core boundary
50
68
 
51
69
  `isoStrict: true` is intentionally a **Part 1 + Corrigenda 1-3** mode. It does
@@ -64,9 +82,11 @@ remain ordinary operator syntax in strict core mode. A conforming `op/3`
64
82
  directive may still add an infix `?-` definition; strict mode reads that as an
65
83
  ordinary term rather than as a quad.
66
84
 
67
- Part 2 modules and Part 3 grammar rules remain supported and tested in the
85
+ Module and DCG compatibility features remain supported and tested in the
68
86
  normal EyeProlog profile. They are tracked separately in ISO-MATRIX.md rather
69
- than being silently folded into the Part 1 strict-core claim.
87
+ than being silently folded into the Part 1 strict-core claim. The project does
88
+ not currently assert that this evidence closes every requirement of ISO/IEC
89
+ 13211-2:2000 or ISO/IEC TS 13211-3.
70
90
 
71
91
  ## Release gate
72
92
 
@@ -35,7 +35,7 @@ Status values are:
35
35
  | 7.1.2.2 | Mapping between a character code and bytes | Text file streams decode and encode UTF-8. Binary streams expose bytes 0..255 directly. | **defined** — `src/io.js`. |
36
36
  | 7.1.4.1 | Set `C` of characters represented by one-char atoms | Character predicates accept Unicode scalar values U+0000..U+10FFFF excluding surrogate code points. | **defined** — `src/iso.js` character-code validation. |
37
37
  | 7.4.2.4 | Whether `op/3` directives affect other Prolog texts or execution | An `op/3` directive changes parsing of subsequent text loaded into the same `Program`; the resulting operator table is also used by execution-time term I/O. Separately created `Program` objects are independent. | **defined** — `src/parser.js`, `src/program.js`, `src/iso.js`. |
38
- | 7.4.2.5 | Whether directive-created `Convc` affects other text/execution | Directive mappings are copied into the solver's execution-time `charConversions` map. The source tokenizer itself does not currently apply `char_conversion/2` to later unquoted source characters. | **audit gap** — `src/program.js`, `src/solver.js`; source-preparation conversion remains to be aligned with 7.11.2.1. |
38
+ | 7.4.2.5 | Whether directive-created `Convc` affects other text/execution | Yes. A `char_conversion/2` directive updates preparation-time conversion for later unquoted source characters and the recorded mapping initializes execution-time term input. Quoted characters are not converted; `char_conversion=off` disables following preparation-time conversion. | **defined** — `src/parser.js`, `src/program.js`, `src/solver.js`; strict-core regression coverage. |
39
39
  | 7.4.2.6 | Order of `initialization/1` goals | Initialization goals run once, in source/inclusion order, before requested goals; each must obtain a first solution. | **defined** — `Program.initializations`, `Solver.runInitializations()`. |
40
40
  | 7.4.2.7 | Ground term designating a Prolog text for `include/1` | A source designation is an atom interpreted as a file path; relative paths resolve from the including file's directory (or the current working directory for unanchored input). | **defined** — `src/program.js` (`readIncludedSource`). |
41
41
  | 7.4.2.8 | Ground term designating a Prolog text for `ensure_loaded/1` | Same atom/file-path designation as `include/1`. | **defined** — `src/program.js`. |
@@ -59,17 +59,17 @@ Status values are:
59
59
  | 7.10.2.13 | `eof_action(Action)` property when the stream uses its default action | Reports the same selected action: `error` for ordinary opened files and `reset` for the predefined standard streams. | **defined** — `streamProperties()` in `src/iso.js`, defaults in `src/io.js`. |
60
60
  | 7.10.2.11 | Whether `reposition(false)` streams may nevertheless be repositioned | No. `set_stream_position/2` raises a permission error unless the stream was created with `reposition(true)`. | **defined** — `src/iso.js`. |
61
61
  | 7.11.1.1 | Default `bounded` flag | `false`: EyeProlog's Prolog integer model uses arbitrary-precision `BigInt`, subject to host resources. | **defined** — `defaultPrologFlags()` in `src/solver.js`. |
62
- | 7.11.1.2 | Default `max_integer` when `bounded=true` | The bounded-profile default is not applicable because EyeProlog selects `bounded=false`. However, EyeProlog currently exposes the sentinel atom `unbounded` through `current_prolog_flag/2` instead of making the `max_integer` query fail as 7.11.1.1 prescribes for an unbounded processor. | **audit gap** — `src/solver.js`, `current_prolog_flag/2`. |
63
- | 7.11.1.3 | Default `min_integer` when `bounded=true` | The bounded-profile default is not applicable because EyeProlog selects `bounded=false`. However, EyeProlog currently exposes the sentinel atom `unbounded` through `current_prolog_flag/2` instead of making the `min_integer` query fail as 7.11.1.1 prescribes for an unbounded processor. | **audit gap** — `src/solver.js`, `current_prolog_flag/2`. |
62
+ | 7.11.1.2 | Default `max_integer` when `bounded=true` | Not applicable because EyeProlog selects `bounded=false`; consequently `current_prolog_flag(max_integer, _)` fails. | **not applicable** — `src/solver.js`, `current_prolog_flag/2`; strict-core regression coverage. |
63
+ | 7.11.1.3 | Default `min_integer` when `bounded=true` | Not applicable because EyeProlog selects `bounded=false`; consequently `current_prolog_flag(min_integer, _)` fails. | **not applicable** — `src/solver.js`, `current_prolog_flag/2`; strict-core regression coverage. |
64
64
  | 7.11.1.4 | Default `integer_rounding_function` flag | `toward_zero`; `//` uses truncation toward zero. `div` is provided separately with downward/floor division semantics. | **defined** — `src/solver.js`, `src/iso-arithmetic.js`. |
65
65
  | 9.1.3.1 | Integer division rounding function `rndI` | Truncation toward zero, matching the `integer_rounding_function=toward_zero` flag. | **defined** — BigInt division for `//` in `src/iso-arithmetic.js`. |
66
- | 7.11.2.1 | Whether preparation-time `Convc` affects execution-time `Convc` | Yes: mappings recorded by `char_conversion/2` directives initialize the solver map. Source-token conversion itself has the audit gap noted at 7.4.2.5. | **defined / audit note** — `src/solver.js`. |
66
+ | 7.11.2.1 | Whether preparation-time `Convc` affects execution-time `Convc` | Yes: mappings created while Prolog text is prepared are retained and initialize the solver's execution-time conversion map. | **defined** — `src/parser.js`, `src/program.js`, `src/solver.js`. |
67
67
  | 7.11.2.2 | Effect when `debug=on` | The flag is accepted and stored; it does not change goal semantics or enable a debugger. | **defined** — `src/solver.js`; no semantic branch depends on `debug`. |
68
68
  | 7.11.2.3 | Default `max_arity` | `unbounded` in the Prolog model, subject to host memory and practical JavaScript array/index limits. | **defined** — `src/solver.js`; relevant guards report representation/resource errors. |
69
69
  | 7.11.2.5 | Default `double_quotes` | `chars`. | **defined** — `src/solver.js`, parser flag state. |
70
70
  | 7.12.1 | Second argument of `error/2` | The default context term is the atom `eyeprolog`. A few implementation-specific diagnostics may deliberately supply a more specific context term. | **defined** — `formalErrorTerm()` in `src/iso.js`. |
71
71
  | 7.12.2(f) | Implementation-defined representation limits | Character and character-code operations use Unicode scalar limits; arity/integer values are modeled as unbounded but may hit host/resource limits. Float input overflow uses the implementation-specific `max_float`/`min_float` representation names documented by the STC-oriented tests. | **defined** — parser/ISO numeric and character guards. |
72
- | 8.17.1 | Implementation-defined flag value ranges | Strict mode exposes only Part 1 core flags and their standard value sets. Normal mode additionally exposes EyeProlog's `occurs_check` flag; `max_integer`/`min_integer` use the `unbounded` sentinel described above. | **defined** — strict registry/flag filtering in `src/solver.js`. |
72
+ | 8.17.1 | Implementation-defined flag value ranges | Strict mode exposes only Part 1 core flags and their standard value sets. Normal mode additionally exposes EyeProlog's `occurs_check` flag. With `bounded=false`, `max_integer` and `min_integer` have no current value and their `current_prolog_flag/2` queries fail. | **defined** — strict registry/flag filtering in `src/solver.js`. |
73
73
  | 8.17.3 | Other effects of `halt/0` | Terminates EyeProlog execution and returns host/process status `0`; it produces no Prolog solution. | **defined** — `HaltSignal`, `haltBuiltin()`, CLI/runner handling. |
74
74
  | 8.17.4 | Meaning/effects of `halt(Status)` | Integer `Status` is converted to the host process/runner halt code; it produces no Prolog solution. | **defined** — `haltBuiltin()`, `src/execute.js`, `src/cli.js`. |
75
75
  | 9.1.4.1 | Floating-point rounding function `rndF` | Floating values and operations use ECMAScript `Number` (IEEE-754 binary64) and the host's specified binary64 arithmetic/conversions. | **defined** — `src/iso-arithmetic.js`, `src/number-value.js`. |
@@ -118,9 +118,10 @@ families; `--iso-strict` is intended to remove their Part 1 interpretation.
118
118
  | 5.5.11 Reserved atoms | None | None. |
119
119
  | Cor.3 5.5.12 Options | Extra library/host options may exist outside core option lists | Strict core accepts the standard option names implemented for Part 1; invalid options follow Corrigendum 3 validation. |
120
120
 
121
- Part 2 and Part 3 are separately standardized profiles when EyeProlog is run in
122
- its normal mode; the table above describes them as extensions only relative to
123
- the Part 1 strict-core boundary.
121
+ Normal mode provides documented module and DCG compatibility profiles whose
122
+ features overlap standardized Part 2 and Part 3 facilities. They are extensions
123
+ relative to the Part 1 strict-core boundary and are tested separately; this
124
+ ledger does not assert complete Part 2 or Part 3 conformance.
124
125
 
125
126
  ## Important implementation-dependent behavior (not the 5.4 mandatory table)
126
127
 
@@ -19,8 +19,8 @@ compliance audit and the remaining work before a full conformance claim.
19
19
  | 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, parenthesized-number rejection, Logtalk-derived error cases |
20
20
  | 8.17 flags and hooks | required flags, mutation permissions, halt and character conversion | `exceptions_and_flags`, `remaining_builtins_and_directives`, flag error cases |
21
21
  | Clause 9 evaluable functors | integer, float, rounding, transcendental and bitwise operations | `arithmetic`, `corrigenda_arithmetic`, `corrigenda_atan2_zero`, `corrigenda_integer_negative_power` |
22
- | ISO/IEC 13211-2 modules | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
23
- | ISO/IEC TS 13211-3 grammar rules | `-->`, terminal and partial sequences, grammar control constructs, semicontexts, nonterminal indicators, modules, `phrase/2-3`, steadfastness and errors | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_partial_sequences`, `dcg_phrase_steadfastness`, `dcg_dynamic_nonterminal_indicator`, `logtalk_dcg_phrase_identity`, `logtalk_dcg_semicontexts`, DCG error and precedence cases |
22
+ | Module compatibility profile (related to ISO/IEC 13211-2 and later WG17 amendment work) | module declarations, exports, imports, qualification, meta-predicate context | `modules/qualified_call`, `modules/selective_library_import`, `dcg_module_nonterminal_indicator` |
23
+ | Part 3-oriented DCG compatibility profile | `-->`, terminal and partial sequences, grammar control constructs, semicontexts, nonterminal indicators, modules, `phrase/2-3`, steadfastness and errors | `dcg_terminals_and_remainder`, `dcg_control_constructs`, `dcg_partial_sequences`, `dcg_phrase_steadfastness`, `dcg_dynamic_nonterminal_indicator`, `logtalk_dcg_phrase_identity`, `logtalk_dcg_semicontexts`, DCG error and precedence cases |
24
24
 
25
25
  Corrigendum-specific coverage includes double-quoted atom operator priority
26
26
  (Cor.1); the added predicates and evaluable functors, bar-operator rules,
@@ -36,7 +36,9 @@ particular, the index records the unbounded integer model, `double_quotes=chars`
36
36
  9.1.4.2 choice to round tiny arithmetic results rather than raise underflow),
37
37
  stream/character decisions, and the normal-profile extension boundary.
38
38
 
39
- This is an executable conformance matrix, not a certification issued by an
40
- independent standards body. Release gating runs the ISO cases and the dedicated
39
+ This is an executable conformance/compatibility matrix, not a certification issued by an
40
+ independent standards body. The Part 2/Part 3-related rows describe tested
41
+ normal-mode compatibility surfaces rather than a claim that every requirement
42
+ of those standards has been closed. Release gating runs the ISO cases and the dedicated
41
43
  Part 1 strict-core suite together with the full regression, API, documentation,
42
44
  example, and browser suites.
@@ -8,7 +8,7 @@ supported ISO Prolog profile, built-ins, extensions, and reasoner behavior.
8
8
  The suite is intentionally file-based. Exact standard output, errors, warnings,
9
9
  and proof output test the behavior of the JavaScript implementation.
10
10
  [ISO-MATRIX.md](ISO-MATRIX.md) maps Part 1 normative clause families, all three
11
- corrigenda, Part 2 modules, and Part 3 definite clause grammars to representative
11
+ corrigenda, plus EyeProlog's module and DCG compatibility profiles, to representative
12
12
  executable cases. [WG17-SYNTAX-STATUS.md](WG17-SYNTAX-STATUS.md) records the
13
13
  complete one-to-one trace for the vendored active upstream WG17 syntax cases.
14
14
  [STC-DRAFT-STATUS.md](STC-DRAFT-STATUS.md) separately tracks executable
@@ -17,9 +17,8 @@ review evidence, not normative ISO claims.
17
17
 
18
18
  “Conformance” here means conformance to EyeProlog's documented ISO compatibility
19
19
  profile and implementation extensions. The default registry covers the exact
20
- predicate indicators listed in Appendix B of the book across the supported
21
- ISO/IEC 13211-1:1995, ISO/IEC 13211-2:2000, and ISO/IEC TS 13211-3:2025
22
- standard families. [ISO-COMPLIANCE.md](ISO-COMPLIANCE.md) is the explicit
20
+ predicate indicators listed in Appendix B of the book across the Part 1 strict-core
21
+ target and the normal-mode module/DCG compatibility families. [ISO-COMPLIANCE.md](ISO-COMPLIANCE.md) is the explicit
23
22
  processor-requirement ledger for the Part 1 strict-core audit. This suite is
24
23
  still not an independent certification of every processor requirement, lexical
25
24
  edge, option combination, or prescribed error precedence. Cases under `iso/`
@@ -1,4 +1,4 @@
1
- % Part 2 module exports combined with a Part 3 nonterminal indicator.
1
+ % Module compatibility exports combined with a Part 3 nonterminal indicator.
2
2
  :- module(dcg_vocab, [word//1, answer/1]).
3
3
 
4
4
  word(X) --> [X].
@@ -1,3 +1,3 @@
1
1
  answer(7, red, caught(eyeprolog), first, caught(eyeprolog)).
2
2
  answer(7, red, caught(eyeprolog), second, caught(eyeprolog)).
3
- flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_integer, unbounded), pair(min_integer, unbounded), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
3
+ flags(off, on, [pair(bounded, false), pair(integer_rounding_function, toward_zero), pair(char_conversion, on), pair(debug, off), pair(max_arity, unbounded), pair(unknown, fail), pair(double_quotes, chars), pair(occurs_check, true)]).
@@ -21,3 +21,10 @@ The corpus snapshot was retrieved on 2026-08-11.
21
21
  `prologue_quad_runner.pl` loads EyeProlog's `library(prologue)` and includes
22
22
  the unmodified corpus, mirroring the draft's requirement that a Prologue be
23
23
  included before its examples are run.
24
+
25
+ The upstream Prologue snapshot contains one `max_integer` quad that accepts an
26
+ implementation-specific `Max = unbounded` result. EyeProlog deliberately does
27
+ not patch that vendored fixture: with its ISO `bounded=false` choice, Part 1
28
+ 7.11.1.1 requires `current_prolog_flag(max_integer, _)` to fail. The regression
29
+ gate therefore records this single standards-driven divergence explicitly while
30
+ requiring the other 32 quads to pass.
@@ -36,6 +36,26 @@ export function runIsoStrict(reporter = new TestReporter()) {
36
36
  equal(solver.prologFlags.get('unknown')?.value?.name, 'error', 'unknown default');
37
37
  });
38
38
 
39
+ reporter.test('unbounded integer profile does not expose max_integer or min_integer values', () => {
40
+ const program = Program.parse('', { isoStrict: true });
41
+ const solver = new Solver(program, { isoStrict: true });
42
+ const answers = (text) => [...solver.solve([parseGoalText(text, { isoStrict: true })], new Env(), 0)].length;
43
+ equal(answers('current_prolog_flag(bounded,false)'), 1, 'bounded=false');
44
+ equal(answers('current_prolog_flag(max_integer,_)'), 0, 'max_integer unavailable');
45
+ equal(answers('current_prolog_flag(min_integer,_)'), 0, 'min_integer unavailable');
46
+ });
47
+
48
+
49
+ reporter.test('preparation-time char_conversion affects later unquoted source only', () => {
50
+ const program = Program.parse(
51
+ ":- char_conversion(x,y).\np(x).\nquoted('x').\n:- set_prolog_flag(char_conversion,off).\nraw(x).\n",
52
+ { isoStrict: true },
53
+ );
54
+ equal(Boolean(program.findGroup('p', 1)?.clauses.some((clause) => clause.head.args[0]?.name === 'y')), true, 'converted p/1');
55
+ equal(Boolean(program.findGroup('quoted', 1)?.clauses.some((clause) => clause.head.args[0]?.name === 'x')), true, 'quoted atom unchanged');
56
+ equal(Boolean(program.findGroup('raw', 1)?.clauses.some((clause) => clause.head.args[0]?.name === 'x')), true, 'conversion disabled');
57
+ });
58
+
39
59
  reporter.test('uses the Part 1 predefined operator table', () => {
40
60
  const program = Program.parse('', { isoStrict: true });
41
61
  equal(program.operators.has('fx\u0000?-'), true, 'fx ?-');
@@ -1218,7 +1218,7 @@ c4 ?- call((!;1)).
1218
1218
  },
1219
1219
  },
1220
1220
  {
1221
- name: 'runQuads passes the complete vendored Prolog Prologue quad corpus',
1221
+ name: 'vendored Prolog Prologue corpus records the bounded=false max_integer divergence',
1222
1222
  run: () => {
1223
1223
  const filename = path.join(testRoot, 'fixtures', 'prologue_quad_runner.pl');
1224
1224
  const source = fs.readFileSync(filename, 'utf8');
@@ -1228,8 +1228,15 @@ c4 ?- call((!;1)).
1228
1228
  baseDir: path.dirname(filename),
1229
1229
  }]));
1230
1230
  assertEqual(result.total, 33, 'quad total');
1231
- assertEqual(result.passed, 33, 'quad passed');
1232
- assertEqual(result.stdout, 'quads: 33 run, 33 passed, 0 failed.\n', 'quad report');
1231
+ // The upstream working-draft quad accepts either integer overflow or
1232
+ // Max=unbounded. ISO/IEC 13211-1 7.11.1.1 instead says that when
1233
+ // bounded=false, current_prolog_flag(max_integer, N) fails. Preserve
1234
+ // the upstream fixture unchanged and make that one deliberate
1235
+ // standards-driven divergence explicit in the regression gate.
1236
+ assertEqual(result.passed, 32, 'quad passed');
1237
+ assertEqual(result.failed, 1, 'quad failed');
1238
+ assertIncludes(result.stdout, 'current_prolog_flag(max_integer, Max)', 'max_integer divergence');
1239
+ assertIncludes(result.stdout, 'quads: 33 run, 32 passed, 1 failed.', 'quad report');
1233
1240
  },
1234
1241
  },
1235
1242
  {
@@ -4613,7 +4620,7 @@ answer(ok) :-
4613
4620
  },
4614
4621
  },
4615
4622
  {
4616
- name: 'ISO Part 2 library modules load Prolog clauses explicitly',
4623
+ name: 'module compatibility libraries load Prolog clauses explicitly',
4617
4624
  run: () => {
4618
4625
  const program = Program.parse(':- use_module(library(lists)).\n:- use_module(library(random)).\nanswer(X) :- append([a], [b], X).');
4619
4626
  const solver = new Solver(program);
@@ -4749,7 +4756,7 @@ repeated(X) :- X in 1..3, all_distinct([X, X]).
4749
4756
  },
4750
4757
  },
4751
4758
  {
4752
- name: 'Part 2 modules isolate predicates and support selective imports',
4759
+ name: 'module compatibility profile isolates predicates and supports selective imports',
4753
4760
  run: () => {
4754
4761
  const directory = path.join(tmp, `modules-${++tmpCounter}`);
4755
4762
  fs.mkdirSync(directory);
@@ -4774,7 +4781,7 @@ repeated(X) :- X in 1..3, all_distinct([X, X]).
4774
4781
  },
4775
4782
  },
4776
4783
  {
4777
- name: 'Part 3 nonterminal indicators import through Part 2 modules',
4784
+ name: 'Part 3 nonterminal indicators import through module compatibility profile',
4778
4785
  run: () => {
4779
4786
  const directory = path.join(tmp, `dcg-modules-${++tmpCounter}`);
4780
4787
  fs.mkdirSync(directory);
@@ -1943,7 +1943,7 @@ The source layout mirrors the language boundary while keeping the JavaScript
1943
1943
  runtime flat under `src/`. `src/iso.js` remains the stable ISO facade and
1944
1944
  built-in registry; arithmetic evaluation lives in `src/iso-arithmetic.js`, and
1945
1945
  processor control/error classes live in `src/errors.js`. `src/dcg.js`
1946
- implements the shared ISO Part 3 grammar-rule and dynamic-body expansion
1946
+ implements the shared Part 3-oriented grammar-rule and dynamic-body expansion
1947
1947
  without depending back on the ISO registry. This keeps the low-level syntax and
1948
1948
  error layers acyclic while preserving the existing `src/iso.js` exports.
1949
1949
 
@@ -1964,8 +1964,8 @@ being split through extra strategy objects or callbacks. Architectural cleanup
1964
1964
  is required to preserve benchmark performance as well as semantics.
1965
1965
 
1966
1966
  Focused files under `src/lib/` contain the portable extensions, with
1967
- `src/lib/lists.pl` supplying common list relations. They are ordinary ISO/IEC
1968
- 13211-2 modules, organized like Trealla's `library/` and registered for
1967
+ `src/lib/lists.pl` supplying common list relations. They are ordinary Prolog modules using EyeProlog's documented module compatibility
1968
+ surface, organized like Trealla's `library/` and registered for
1969
1969
  `library(Name)` by `src/standard-library.js` in Node and the browser. The
1970
1970
  browser entry point `src/playground-worker.js` uses that same program and
1971
1971
  module-loading path in a dedicated worker. `src/ARCHITECTURE.md` records the
@@ -2587,8 +2587,10 @@ correct program without losing its meaning, and organize a decision system
2587
2587
  whose conclusions remain auditable.
2588
2588
 
2589
2589
  EyeProlog supplies the Part 1 control, dynamic-database, operator, and I/O
2590
- facilities together with the Part 2 module forms `module/2`, `use_module/1`,
2591
- `use_module/2`, and `Module:Goal`. Definite-clause grammar notation remains
2590
+ facilities together with its normal-profile module forms `module/2`,
2591
+ `use_module/1`, `use_module/2`, and `Module:Goal`. These forms are treated as a
2592
+ module compatibility surface, not as a claim of complete ISO/IEC 13211-2:2000
2593
+ conformance. Definite-clause grammar notation remains
2592
2594
  outside this profile. The examples still prefer explicit domain
2593
2595
  relations, state, and syntax trees where that makes assumptions easier to
2594
2596
  inspect.
@@ -5485,11 +5487,13 @@ The long catalogs are meant to be entered locally, not memorized linearly.
5485
5487
 
5486
5488
  ## 38. Language and ISO profile
5487
5489
 
5488
- The standards baseline is ISO/IEC 13211-1:1995, as corrected by Technical
5489
- Corrigenda 1:2007, 2:2012, and 3:2017, together with the module facilities of
5490
- ISO/IEC 13211-2 and definite clause grammars from ISO/IEC TS 13211-3:2025.
5491
- EyeProlog implements the compatibility profile documented
5492
- here; it does not claim certification as a complete conforming processor.
5490
+ The normative strict-core baseline is ISO/IEC 13211-1:1995, as corrected by
5491
+ Technical Corrigenda 1:2007, 2:2012, and 3:2017. Normal EyeProlog additionally
5492
+ provides a practical module interface aligned with later WG17 module amendment
5493
+ work and a definite-clause-grammar profile following ISO/IEC TS 13211-3. Those
5494
+ normal-mode profiles are documented and tested compatibility surfaces; they are
5495
+ not currently claimed as complete clause-by-clause certifications of Part 2 or
5496
+ Part 3.
5493
5497
 
5494
5498
  Prolog source accepted by EyeProlog is UTF-8. `%` starts a line comment and
5495
5499
  `/* ... */` delimits a block comment. Plain atoms begin with a
@@ -5618,10 +5622,11 @@ EyeProlog supports cut, operator declarations, dynamic database updates, grouped
5618
5622
  solutions, exceptions, flags, initialization and inclusion directives, and
5619
5623
  standard stream and term I/O. Normal mode additionally provides lifecycle-aware
5620
5624
  `call_cleanup/2` and `setup_call_cleanup/3`; these cleanup controls are
5621
- EyeProlog extensions and are excluded by `--iso-strict`. ISO Part 2 modules and
5622
- Part 3 definite clause grammars complement this Part 1 core.
5625
+ EyeProlog extensions and are excluded by `--iso-strict`. Normal mode also
5626
+ adds the documented module compatibility surface and a Part 3-oriented DCG
5627
+ profile.
5623
5628
 
5624
- ### ISO Part 2 modules
5629
+ ### Module compatibility profile
5625
5630
 
5626
5631
  A module gives predicate identity one more component: module name, predicate
5627
5632
  name, and arity. The first directive in a module source names the module and
@@ -5657,7 +5662,7 @@ an empty list when only qualified calls are wanted. `Module:Goal` selects a
5657
5662
  module explicitly. Repeated module loads are idempotent, while conflicting
5658
5663
  imports and requests for predicates that a module does not export are errors.
5659
5664
 
5660
- ### ISO Part 3 definite clause grammars
5665
+ ### Part 3-oriented definite clause grammars
5661
5666
 
5662
5667
  A grammar rule `Head --> Body.` is prepared as an ordinary predicate with two
5663
5668
  additional difference-list arguments. Parameterized nonterminals retain their
@@ -5670,7 +5675,7 @@ Nonterminal indicators can be exported and imported through modules:
5670
5675
  word(noun) --> [robot] | [scientist].
5671
5676
  ```
5672
5677
 
5673
- The required grammar constructs are terminal lists, `[]`, sequencing with
5678
+ The supported grammar constructs include terminal lists, `[]`, sequencing with
5674
5679
  comma, alternatives with `;` or `|`, if-then-else, embedded goals `{Goal}`,
5675
5680
  `call//1`, `phrase//1`, and `!//0`. Semicontexts provide look-ahead by restoring
5676
5681
  terminals to the remaining sequence:
@@ -5973,7 +5978,7 @@ silently changing a static program.
5973
5978
  | --- | --- |
5974
5979
  | `op(+Priority,+Specifier,+NameOrNames)` | Defines or removes operators in the current program. Priority is `0..1200`; specifiers are `fx`, `fy`, `xf`, `yf`, `xfx`, `xfy`, or `yfx`; names may be one atom or a proper list. Priority zero removes the definition. `,` and `\|` cannot be modified. |
5975
5980
  | `current_op(?Priority,?Specifier,?Name)` | Enumerates active operator definitions and filters supplied arguments. |
5976
- | `char_conversion(+Input,+Output)` | Installs a one-character atom conversion for subsequent term input. Mapping a character to itself removes its custom mapping. |
5981
+ | `char_conversion(+Input,+Output)` | Installs a one-character conversion. In prepared Prolog text, later **unquoted** characters are converted when `char_conversion=on`; quoted characters are unchanged. The same mapping initializes execution-time term input. Mapping a character to itself removes its custom mapping. |
5977
5982
  | `current_char_conversion(?Input,?Output)` | Enumerates installed nonidentity conversions. |
5978
5983
  | `current_prolog_flag(?Flag,?Value)` | Enumerates flags or retrieves one named flag. An unknown bound flag raises *domain_error(prolog_flag)*. |
5979
5984
  | `set_prolog_flag(+Flag,+Value)` | Changes a supported mutable flag after validating its allowed atom value. Read-only flags raise a permission error. |
@@ -5984,13 +5989,21 @@ silently changing a static program.
5984
5989
  | `integer_rounding_function` | `toward_zero` | `toward_zero` | no |
5985
5990
  | `char_conversion` | `on` | `on`, `off` | yes |
5986
5991
  | `debug` | `off` | `on`, `off` | yes |
5987
- | `max_integer` | `unbounded` | `unbounded` | no |
5988
- | `min_integer` | `unbounded` | `unbounded` | no |
5992
+ | `max_integer` | no current value because `bounded=false` | not applicable | no |
5993
+ | `min_integer` | no current value because `bounded=false` | not applicable | no |
5989
5994
  | `max_arity` | `unbounded` | `unbounded` | no |
5990
5995
  | `unknown` | `error` | `error`, `fail`, `warning` | yes |
5991
5996
  | `double_quotes` | `chars` | `chars`, `codes`, `atom` | yes |
5992
5997
  | `occurs_check` | `true` | `true`, `error` | yes |
5993
5998
 
5999
+ Because `bounded=false`, `current_prolog_flag(max_integer, _)` and
6000
+ `current_prolog_flag(min_integer, _)` fail as required by ISO 7.11.1.1;
6001
+ EyeProlog does not expose an `unbounded` sentinel as either flag value.
6002
+ Preparation-time `char_conversion/2` mappings affect later unquoted source text
6003
+ and also initialize the execution-time conversion mapping; setting the
6004
+ `char_conversion` flag to `off` disables conversion for following source text.
6005
+ Quoted source characters are not converted.
6006
+
5994
6007
  Both normal EyeProlog and strict ISO core mode use the ISO `unknown=error`
5995
6008
  default. Interactive `set_prolog_flag/2` changes are retained when the REPL
5996
6009
  consults another file or imports a module instead of being reset by the host
@@ -7599,14 +7612,15 @@ npm run test:playground
7599
7612
 
7600
7613
  ### Supported ISO Prolog implementation
7601
7614
 
7602
- EyeProlog executes a documented and tested ISO Prolog implementation based on
7603
- ISO/IEC 13211-1:1995 and its three technical corrigenda, ISO/IEC 13211-2:2000,
7604
- and ISO/IEC TS 13211-3:2025. The exact supported predicate indicators are
7615
+ EyeProlog executes a documented and tested ISO-oriented Prolog profile. Its
7616
+ strict-core target is ISO/IEC 13211-1:1995 with Technical Corrigenda 1-3. Normal
7617
+ mode additionally provides the documented module compatibility surface and a
7618
+ Part 3-oriented definite-clause-grammar implementation. The exact supported predicate indicators are
7605
7619
  listed in Chapter 39. The normal profile includes control and exceptions, term
7606
7620
  operations, arithmetic, grouped solutions, dynamic clauses, operators,
7607
7621
  atomic-term processing, flags, character conversion, streams, character/byte
7608
- and term I/O, initialization, source inclusion, Part 2 modules, Part 3 grammar
7609
- rules, and EyeProlog extensions.
7622
+ and term I/O, initialization, source inclusion, module compatibility forms,
7623
+ definite-clause grammar rules, and EyeProlog extensions.
7610
7624
 
7611
7625
  For a Part 1 conformance boundary, `--iso-strict` (or API option
7612
7626
  `isoStrict: true`) limits the processor to ISO/IEC 13211-1:1995 plus Technical
@@ -7627,7 +7641,7 @@ remaining qualifications are:
7627
7641
 
7628
7642
  - zero-arity compound syntax such as `ready()` is represented by the atom
7629
7643
  `ready`;
7630
- - ISO Part 2 modules and ISO/IEC TS 13211-3 definite clause grammars are supported;
7644
+ - module and ISO/IEC TS 13211-3-oriented DCG compatibility profiles are supported in normal mode, without a complete Part 2/Part 3 certification claim;
7631
7645
  - variables cannot occupy functor or predicate position;
7632
7646
  - double-quoted text follows `double_quotes` exactly; the default `chars` value
7633
7647
  matches Trealla and Scryer and may be changed to `codes` or `atom`;
package/why-eyeprolog.md CHANGED
@@ -6,8 +6,8 @@ answer.
6
6
 
7
7
  Its design rule is simple:
8
8
 
9
- > **Keep the ISO language core complete, keep extensions small and explicit,
10
- > and implement portable conveniences as ordinary Prolog whenever possible.**
9
+ > **Keep the ISO language core explicit and auditable, keep extensions small and
10
+ > explicit, and implement portable conveniences as ordinary Prolog whenever possible.**
11
11
 
12
12
  ## Why ISO Prolog?
13
13
 
@@ -17,12 +17,13 @@ arithmetic, control, streams, errors, and processor behavior. Reusing that
17
17
  language gives programs recognizable semantics without inventing another rule
18
18
  syntax.
19
19
 
20
- EyeProlog implements the Part 1 core together with Technical Corrigenda 1, 2,
21
- and 3, Part 2 modules, and the Part 3 definite clause grammar specification.
22
- Its executable conformance matrix and tests document the supported
23
- behavior, including an executable trace of the vendored active WG17 syntax
24
- cases. This is extensive implementation evidence, not certification by an
25
- independent standards body.
20
+ EyeProlog targets the Part 1 core together with Technical Corrigenda 1, 2,
21
+ and 3, and provides documented module and definite-clause-grammar compatibility
22
+ profiles for normal-mode programs. Its executable conformance matrix and tests
23
+ document the supported behavior, including an executable trace of the vendored
24
+ active WG17 syntax cases. This is extensive implementation evidence, not a
25
+ claim that every Part 1, Part 2, or Part 3 normative requirement has already
26
+ been certified or closed.
26
27
 
27
28
  ## Why a small implementation?
28
29
 
@@ -34,8 +35,8 @@ keeps a narrow architecture:
34
35
  - explicit `tnot/1` with well-founded semantics for finite, range-restricted,
35
36
  function-free Datalog components;
36
37
  - the ISO built-in registry;
37
- - lean portable ISO Part 2 library modules;
38
- - ISO Part 3 definite clause grammars and `phrase/2-3`;
38
+ - lean portable library modules using the documented module compatibility profile;
39
+ - ISO Part 3-oriented definite clause grammars and `phrase/2-3`;
39
40
  - lifecycle-aware `call_cleanup/2` and `setup_call_cleanup/3` in normal mode;
40
41
  - optional proof explanations; and
41
42
  - the same implementation in Node.js and the browser.
@@ -72,8 +73,8 @@ positive Datalog closures with a shared relation-wide table, but the admission
72
73
  heuristics are implementation details. Programs should rely on the documented
73
74
  semantics and finiteness conditions, not on a particular internal threshold.
74
75
 
75
- DCGs follow the same rule. Their meaning remains the ISO Part 3 difference-list
76
- model, while finite sequence scans and proven zero-width hand-offs may use
76
+ DCGs follow the same rule. Their supported semantics follow the ISO Part 3
77
+ difference-list model, while finite sequence scans and proven zero-width hand-offs may use
77
78
  lighter internal control paths so deep grammars do not pay one general solver
78
79
  frame per token. Relational remainder-producing modes are preserved. The
79
80
  checked `examples/dcg-expression-language.pl` program shows the declarative side
@@ -122,7 +123,7 @@ and integrations that can live outside the reasoning engine.
122
123
  ## The durable idea
123
124
 
124
125
  EyeProlog demonstrates that a useful proof-producing reasoner can be built from
125
- a complete standard core, small portable modules, and an ordinary JavaScript
126
+ a carefully audited standard-oriented core, small portable modules, and an ordinary JavaScript
126
127
  API. Its value is not feature count; it is that the language boundary stays
127
128
  visible enough to understand.
128
129