eyeprolog 1.2.25 → 1.2.27

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.25",
6
+ "version": "1.2.27",
7
7
  "description": "EyeProlog turns facts and rules into answers and proofs.",
8
8
  "type": "module",
9
9
  "main": "./index.js",
@@ -50,9 +50,11 @@
50
50
  "test:eyeprolog": "node test/run-all.mjs",
51
51
  "test:conformance": "node test/run-conformance.mjs",
52
52
  "test:iso-strict": "node test/run-iso-strict.mjs",
53
+ "test:wg17-syntax": "node test/run-wg17-syntax.mjs",
53
54
  "test:examples": "node test/run-examples.mjs",
54
55
  "test:regression": "node test/run-regression.mjs",
55
56
  "test:playground": "node test/run-playground.mjs",
57
+ "report:wg17-syntax": "node tools/report-wg17-syntax-coverage.mjs",
56
58
  "preversion": "npm test && node test/run-conformance-report.mjs conformance-report.md",
57
59
  "postversion": "git push origin HEAD --follow-tags"
58
60
  }
package/src/iso.js CHANGED
@@ -1072,6 +1072,11 @@ function isTerminatingFullStop(source, index) {
1072
1072
  return true;
1073
1073
  }
1074
1074
 
1075
+ const termGraphicCharacters = new Set('#$&*+-./<=>@^~\\:');
1076
+ function continuesGraphicToken(source, index) {
1077
+ return index > 0 && termGraphicCharacters.has(source[index - 1]);
1078
+ }
1079
+
1075
1080
  function* termTextCandidates(stream) {
1076
1081
  const source = String(stream.content);
1077
1082
  let quote = null, lineComment = false, blockComment = false;
@@ -1095,13 +1100,24 @@ function* termTextCandidates(stream) {
1095
1100
  const characterCodeEnd = characterCodeConstantEnd(source, i);
1096
1101
  if (characterCodeEnd != null) { i = characterCodeEnd; continue; }
1097
1102
  if (ch === '%') { lineComment = true; continue; }
1098
- if (ch === '/' && next === '*') { blockComment = true; i++; continue; }
1103
+ // Comment openers are recognized between tokens. Within a maximal
1104
+ // graphic token, as in `//*`, the slash and star remain atom characters.
1105
+ if (ch === '/' && next === '*' && !continuesGraphicToken(source, i)) {
1106
+ blockComment = true;
1107
+ i++;
1108
+ continue;
1109
+ }
1099
1110
  if (ch === "'" || ch === '"') { quote = ch; continue; }
1100
1111
  if (ch === '.' && isTerminatingFullStop(source, i)) {
1101
1112
  yield { text: source.slice(stream.position, i + 1), end: i + 1 };
1102
1113
  }
1103
1114
  }
1104
1115
  }
1116
+ function hasNonLayoutRemainder(source, start) {
1117
+ return source.slice(start)
1118
+ .replace(/[\u0009-\u000d\u0020]+|%[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\//g, '')
1119
+ .length > 0;
1120
+ }
1105
1121
  function convertedTermText(text, solver) {
1106
1122
  if (solver.prologFlags.get('char_conversion')?.value?.name !== 'on' || solver.charConversions.size === 0) return text;
1107
1123
  let result = '', quote = null;
@@ -1128,6 +1144,43 @@ function convertedTermText(text, solver) {
1128
1144
  }
1129
1145
  return result;
1130
1146
  }
1147
+
1148
+ function scopeReadTerm(term) {
1149
+ // A term read from a stream has its own variable set (ISO 7.10.3). Parser
1150
+ // variable names cannot be used as environment identities here: a caller
1151
+ // such as read(X) and an input term X=a would otherwise share the same `X`
1152
+ // and incorrectly attempt the cyclic unification X=(X=a). Use an internal
1153
+ // name containing NUL, which cannot occur in Prolog source, while retaining
1154
+ // the spelling and occurrence count required by read_term/3 metadata.
1155
+ const scope = ++isoFresh;
1156
+ const bySourceName = new Map();
1157
+ const variables = [];
1158
+
1159
+ const copy = (item) => {
1160
+ if (item.type === VAR) {
1161
+ let record = bySourceName.get(item.name);
1162
+ if (record == null) {
1163
+ const scoped = variable(`\u0000read:${scope}:${variables.length}`);
1164
+ scoped.displayName = item.name;
1165
+ record = {
1166
+ sourceName: item.name,
1167
+ term: scoped,
1168
+ count: 0,
1169
+ anonymous: item.name.startsWith('__anon'),
1170
+ };
1171
+ bySourceName.set(item.name, record);
1172
+ variables.push(record);
1173
+ }
1174
+ record.count++;
1175
+ return record.term;
1176
+ }
1177
+ if (item.type !== COMPOUND) return item;
1178
+ return compound(item.name, item.args.map(copy));
1179
+ };
1180
+
1181
+ return { term: copy(term), variables };
1182
+ }
1183
+
1131
1184
  function readTermFromStream(stream, solver) {
1132
1185
  let requestedInteractiveTerm = false;
1133
1186
  while (true) {
@@ -1143,11 +1196,12 @@ function readTermFromStream(stream, solver) {
1143
1196
  const clauses = parseClauses(convertedTermText(candidate.text, solver), {
1144
1197
  sourceMetadata: false,
1145
1198
  operatorState,
1199
+ isoStrict: solver.isoStrict,
1146
1200
  doubleQuotes: solver.prologFlags.get('double_quotes')?.value?.name ?? 'chars',
1147
1201
  });
1148
1202
  if (clauses.length !== 1 || clauses[0].body.length) throw new Error('bad term');
1149
1203
  stream.position = candidate.end;
1150
- return clauses[0].head;
1204
+ return scopeReadTerm(clauses[0].head);
1151
1205
  } catch (_) {
1152
1206
  // A dot inside a graphic operator, such as =.., is only a possible
1153
1207
  // terminator. Keep scanning until a complete term parses.
@@ -1170,8 +1224,13 @@ function readTermFromStream(stream, solver) {
1170
1224
  }
1171
1225
  }
1172
1226
 
1173
- stream.position = String(stream.content).length;
1174
- if (!sawCandidate) return atom('end_of_file');
1227
+ const source = String(stream.content);
1228
+ const remainderStart = stream.position;
1229
+ stream.position = source.length;
1230
+ if (!sawCandidate) {
1231
+ if (hasNonLayoutRemainder(source, remainderStart)) throw new PrologError('syntax_error(read_term)');
1232
+ return { term: atom('end_of_file'), variables: [] };
1233
+ }
1175
1234
  throw new PrologError('syntax_error(read_term)');
1176
1235
  }
1177
1236
  }
@@ -1180,39 +1239,31 @@ function* readBuiltin({ solver, goal, env }) {
1180
1239
  const stream = inputStreamFor(solver, goal, env);
1181
1240
  if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1182
1241
  const next = env.clone();
1183
- if (unify(goal.args[goal.arity - 1], readTermFromStream(stream, solver), next)) yield next;
1242
+ const { term } = readTermFromStream(stream, solver);
1243
+ if (unify(goal.args[goal.arity - 1], term, next)) yield next;
1184
1244
  }
1185
1245
  function* readTermBuiltin({ solver, goal, env }) {
1186
1246
  const stream = goal.arity === 2 ? solver.io.resolve(solver.io.currentInput) : requireStream(solver, goal.args[0], env, 'read');
1187
1247
  if (stream.type !== 'text') throw new PrologError('permission_error(input, binary_stream)', streamHandle(stream.id));
1188
1248
  const options = optionList(goal.args[goal.arity - 1], env);
1189
1249
  const target = goal.args[goal.arity - 2];
1190
- const term = readTermFromStream(stream, solver);
1250
+ const { term, variables } = readTermFromStream(stream, solver);
1191
1251
  const next = env.clone();
1192
1252
  if (!unify(target, term, next)) return;
1193
- const variables = [];
1194
- const counts = new Map();
1195
- const visit = (item) => {
1196
- if (item.type === VAR) {
1197
- counts.set(item.name, (counts.get(item.name) ?? 0) + 1);
1198
- if (!variables.some((entry) => entry.name === item.name)) variables.push(item);
1199
- } else for (const arg of item.args) visit(arg);
1200
- };
1201
- visit(term);
1202
1253
  for (const option of options) {
1203
1254
  if (option.type === VAR) throw new PrologError('instantiation_error');
1204
1255
  if (option.type !== COMPOUND || option.arity !== 1) throw new PrologError('domain_error(read_option)', option);
1205
1256
  let value;
1206
1257
  if (option.name === 'variables') {
1207
- value = listFromItems(variables);
1258
+ value = listFromItems(variables.map((item) => item.term));
1208
1259
  } else if (option.name === 'variable_names') {
1209
1260
  value = listFromItems(variables
1210
- .filter((item) => !item.name.startsWith('__anon'))
1211
- .map((item) => compound('=', [atom(item.name), item])));
1261
+ .filter((item) => !item.anonymous)
1262
+ .map((item) => compound('=', [atom(item.sourceName), item.term])));
1212
1263
  } else if (option.name === 'singletons') {
1213
1264
  value = listFromItems(variables
1214
- .filter((item) => !item.name.startsWith('__anon') && counts.get(item.name) === 1)
1215
- .map((item) => compound('=', [atom(item.name), item])));
1265
+ .filter((item) => !item.anonymous && item.count === 1)
1266
+ .map((item) => compound('=', [atom(item.sourceName), item.term])));
1216
1267
  } else {
1217
1268
  throw new PrologError('domain_error(read_option)', option);
1218
1269
  }
package/src/parser.js CHANGED
@@ -87,7 +87,6 @@ const PREFIX_OPERATORS = new Map([
87
87
 
88
88
  export const ISO_OPERATOR_DEFINITIONS = [
89
89
  [1200, 'xfx', ':-'], [1200, 'fx', ':-'], [1200, 'fx', '?-'], [1200, 'xfx', '-->'],
90
- [1105, 'xfy', '|'],
91
90
  [1100, 'xfy', ';'], [1050, 'xfy', '->'], [1000, 'xfy', ','],
92
91
  [900, 'fy', '\\+'],
93
92
  ...['=', '=..', '\\=', '==', '\\==', '@<', '@=<', '@>', '@>=', 'is',
@@ -99,6 +98,13 @@ export const ISO_OPERATOR_DEFINITIONS = [
99
98
  [200, 'fy', '+'], [200, 'fy', '-'], [200, 'fy', '\\'],
100
99
  ];
101
100
 
101
+ // The alternative operator belongs to the Part 3 grammar-rule profile. Part 1
102
+ // reserves `|` as list punctuation but permits a program to declare it as an
103
+ // infix operator at priority 1001 or greater (Corrigendum 2).
104
+ export const PART3_OPERATOR_DEFINITIONS = [
105
+ [1105, 'xfy', '|'],
106
+ ];
107
+
102
108
  // EyeProlog's embedded quad syntax permits an optional label before `?-`.
103
109
  // That makes `?-` an implementation-specific xfx operator in addition to its
104
110
  // ISO 1200 fx definition.
@@ -118,6 +124,10 @@ function operatorStrength(priority) {
118
124
  return 1201 - priority;
119
125
  }
120
126
 
127
+ // ISO 6.3.3 arguments have maximum priority 999. Parenthesized terms and
128
+ // curly-bracket contents may contain a full priority-1200 term instead.
129
+ const ARG_MIN_PRECEDENCE = operatorStrength(999);
130
+
121
131
  function isGraphicAtomCode(code) {
122
132
  return graphicAtomChars.includes(String.fromCharCode(code));
123
133
  }
@@ -148,7 +158,10 @@ export function createParserOperatorState(definitions = [], includeDefaults = tr
148
158
  // The infix ?-/2 form is an EyeProlog quad extension. ISO 13211-1
149
159
  // predefines only the 1200 fx ?- operator; strict core mode starts from that
150
160
  // table and still permits an explicit op/3 directive to add an infix form.
151
- if (options.isoStrict === true) state.infixOperators.delete('?-');
161
+ if (options.isoStrict === true) {
162
+ state.infixOperators.delete('?-');
163
+ state.infixOperators.delete('|');
164
+ }
152
165
  for (const definition of definitions) {
153
166
  const [priority, specifier, name] = Array.isArray(definition)
154
167
  ? definition
@@ -422,16 +435,22 @@ class Parser {
422
435
  const previousEndsTerm = this.previousToken && (
423
436
  [TOK.VAR, TOK.NUMBER, TOK.STRING, TOK.RPAREN, TOK.RBRACKET, TOK.RBRACE].includes(this.previousToken.type) ||
424
437
  (this.previousToken.type === TOK.ATOM &&
425
- !this.infixOperators.has(this.previousToken.text) &&
426
- !this.prefixOperators.has(this.previousToken.text) &&
427
- !this.postfixOperators.has(this.previousToken.text))
438
+ (this.postfixOperators.has(this.previousToken.text) ||
439
+ (!this.infixOperators.has(this.previousToken.text) &&
440
+ !this.prefixOperators.has(this.previousToken.text))))
428
441
  );
429
442
  if (isDigitCode(ch.charCodeAt(0)) ||
430
443
  (ch === '-' && isDigitCode(this.peek(1).charCodeAt(0)) && !previousEndsTerm)) {
431
444
  const start = this.pos;
432
445
  const negative = this.peek() === '-';
433
446
  if (negative) this.take();
434
- if (this.peek() === '0' && this.peek(1) === "'") {
447
+ const startsQuotedCharacter = this.peek() === '0' && this.peek(1) === "'" &&
448
+ // `0''` is the integer 0 followed by the empty atom, whereas `0'''`
449
+ // is the character-code constant for an apostrophe. A continuation
450
+ // after the integer likewise belongs to the following quoted atom.
451
+ (this.peek(2) !== "'" || this.peek(3) === "'") &&
452
+ !(this.peek(2) === '\\' && this.peek(3) === '\n');
453
+ if (startsQuotedCharacter) {
435
454
  this.take();
436
455
  this.take();
437
456
  let value = this.take();
@@ -463,7 +482,12 @@ class Parser {
463
482
  const code = value.codePointAt(0);
464
483
  return { type: TOK.NUMBER, text: String(negative ? -code : code), line };
465
484
  }
466
- if (this.peek() === '0' && ['b', 'o', 'x'].includes(this.peek(1))) {
485
+ const radixKind = this.peek() === '0' ? this.peek(1) : '';
486
+ const radixHasDigit = radixKind === 'b' ? /^[01]$/.test(this.peek(2))
487
+ : radixKind === 'o' ? /^[0-7]$/.test(this.peek(2))
488
+ : radixKind === 'x' ? /^[0-9A-Fa-f]$/.test(this.peek(2))
489
+ : false;
490
+ if (radixHasDigit) {
467
491
  this.take();
468
492
  const kind = this.take();
469
493
  const radix = kind === 'b' ? 2 : kind === 'o' ? 8 : 16;
@@ -477,11 +501,16 @@ class Parser {
477
501
  return { type: TOK.NUMBER, text: integer.toString(), line };
478
502
  }
479
503
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
504
+ let hasFraction = false;
480
505
  if (this.peek() === '.' && isDigitCode(this.peek(1).charCodeAt(0))) {
506
+ hasFraction = true;
481
507
  this.take();
482
508
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
483
509
  }
484
- if ((this.peek() === 'e' || this.peek() === 'E')) {
510
+ // ISO floating-point syntax requires a fractional part before an
511
+ // exponent. Thus 1.0e9 is one number token, while 1E9 is the integer 1
512
+ // followed by the name E9 and is not a valid term without an operator.
513
+ if (hasFraction && (this.peek() === 'e' || this.peek() === 'E')) {
485
514
  let idx = this.pos + 1;
486
515
  if (this.source[idx] === '+' || this.source[idx] === '-') idx++;
487
516
  if (isDigitCode((this.source[idx] ?? '').charCodeAt(0))) {
@@ -490,7 +519,10 @@ class Parser {
490
519
  while (isDigitCode(this.peek().charCodeAt(0))) this.take();
491
520
  }
492
521
  }
493
- return { type: TOK.NUMBER, text: this.source.slice(start, this.pos), line };
522
+ let text = this.source.slice(start, this.pos);
523
+ if (!hasFraction) text = BigInt(text).toString();
524
+ else if (Object.is(Number(text), -0)) text = '0.0';
525
+ return { type: TOK.NUMBER, text, line };
494
526
  }
495
527
 
496
528
  if (isVariableStartCode(ch.charCodeAt(0))) {
@@ -527,7 +559,9 @@ class Parser {
527
559
  parseParenthesizedTerm() {
528
560
  this.expect(TOK.LPAREN, '(');
529
561
  this.advance();
530
- const term = this.parseTerm(0, true);
562
+ // A current operator atom may be the complete parenthesized term, e.g.
563
+ // (+), but it cannot silently become an operand in a larger expression.
564
+ const term = this.parseTerm(0, true, true, true);
531
565
  this.expect(TOK.RPAREN, ')');
532
566
  this.advance();
533
567
  return term;
@@ -544,14 +578,14 @@ class Parser {
544
578
  const items = [];
545
579
  let tail = null;
546
580
  while (true) {
547
- items.push(this.parseTerm(0, false, false));
581
+ items.push(this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true));
548
582
  if (this.token.type === TOK.COMMA) {
549
583
  this.advance();
550
584
  continue;
551
585
  }
552
586
  if (this.token.type === TOK.BAR) {
553
587
  this.advance();
554
- tail = this.parseTerm(0, false, false);
588
+ tail = this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true);
555
589
  this.expect(TOK.RBRACKET, ']');
556
590
  this.advance();
557
591
  break;
@@ -576,8 +610,33 @@ class Parser {
576
610
  this.advance();
577
611
  return compound('{}', [term]);
578
612
  }
579
- parseTerm(minPrecedence = 0, allowComma = false, allowBar = true) {
580
- let left = this.parsePrefixTerm(minPrecedence, allowBar);
613
+ parseFunctionalNotation(name) {
614
+ this.expect(TOK.LPAREN, '(');
615
+ this.advance();
616
+ const args = [];
617
+ if (this.token.type === TOK.RPAREN) {
618
+ throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(name)} for arity zero data`);
619
+ }
620
+ while (true) {
621
+ args.push(this.parseTerm(ARG_MIN_PRECEDENCE, false, false, true));
622
+ if (this.token.type !== TOK.COMMA) break;
623
+ this.advance();
624
+ }
625
+ this.expect(TOK.RPAREN, ')');
626
+ this.advance();
627
+ return compound(name, args);
628
+ }
629
+ parseTerm(minPrecedence = 0, allowComma = false, allowBar = true, allowOperatorAtom = false) {
630
+ const initialOperatorName = this.operatorTokenName();
631
+ // An atom whose only operator declaration is postfix can still seed a
632
+ // postfix expression: after `op(100,xf,a), op(200,xf,b)`, `a b` denotes
633
+ // b(a). Prefix and infix operator atoms cannot be bare operands.
634
+ const initialWasCurrentOperator = initialOperatorName != null &&
635
+ (this.infixOperators.has(initialOperatorName) ||
636
+ this.prefixOperators.has(initialOperatorName));
637
+ let left = this.parsePrefixTerm(minPrecedence, allowBar, allowOperatorAtom);
638
+ const leftIsBareOperatorAtom = initialWasCurrentOperator &&
639
+ left.type === ATOM && left.name === initialOperatorName;
581
640
  let strictPostfixPrecedence = null;
582
641
  while (true) {
583
642
  const op = this.token.type === TOK.COMMA && allowComma
@@ -591,15 +650,26 @@ class Parser {
591
650
  const postfix = postfixName == null ? null : this.postfixOperators.get(postfixName);
592
651
  if (!postfix || postfix.precedence < minPrecedence ||
593
652
  (strictPostfixPrecedence === postfix.precedence)) break;
653
+ if (this.strictIso && leftIsBareOperatorAtom) {
654
+ throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
655
+ }
594
656
  const name = postfixName;
595
657
  this.advance();
596
658
  left = compound(name, [left]);
597
659
  strictPostfixPrecedence = postfix.strict ? postfix.precedence : null;
598
660
  continue;
599
661
  }
662
+ if (this.strictIso && leftIsBareOperatorAtom) {
663
+ throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
664
+ }
600
665
  strictPostfixPrecedence = null;
601
666
  this.advance();
602
- const right = this.parseTerm(info.associativity === 'right' ? info.precedence : info.precedence + 1, allowComma, allowBar);
667
+ const right = this.parseTerm(
668
+ info.associativity === 'right' ? info.precedence : info.precedence + 1,
669
+ allowComma,
670
+ allowBar,
671
+ false,
672
+ );
603
673
  left = compound(op, [left, right]);
604
674
  if (info.associativity === 'none') {
605
675
  const nextOp = this.token.type === TOK.COMMA && allowComma
@@ -612,18 +682,46 @@ class Parser {
612
682
  }
613
683
  }
614
684
  }
685
+ if (this.strictIso && leftIsBareOperatorAtom && left.type === ATOM && !allowOperatorAtom) {
686
+ throw new Error(`parse line ${this.token.line}: operator atom ${left.name} requires parentheses as an operand`);
687
+ }
615
688
  return left;
616
689
  }
617
- parsePrefixTerm(minPrecedence = 0, allowBar = true) {
690
+ parsePrefixTerm(minPrecedence = 0, allowBar = true, allowOperatorAtom = false) {
618
691
  // `:-` is tokenized specially so the program grammar can recognize clause
619
692
  // and directive markers. In term argument position, however, ISO 6.3.3.1
620
693
  // permits an operator atom directly as an `arg`; a leading `:-` cannot be
621
694
  // prefix operator notation at argument priority, so it denotes the atom.
622
695
  if (this.token.type === TOK.IF) {
696
+ if (this.strictIso && !allowOperatorAtom) {
697
+ throw new Error(`parse line ${this.token.line}: operator atom :- requires argument context or parentheses`);
698
+ }
623
699
  this.advance();
624
700
  return atom(':-');
625
701
  }
626
702
  const operatorName = this.operatorTokenName();
703
+ // A negative number consists of a minus name token followed by a numeric
704
+ // token, with layout permitted between them. It is lexical number syntax,
705
+ // not an application of the current prefix `-` operator, and therefore
706
+ // remains valid even after op(0, fy, -).
707
+ if (operatorName === '-' && this.token.type === TOK.ATOM) {
708
+ const state = {
709
+ pos: this.pos,
710
+ line: this.line,
711
+ previousToken: this.previousToken,
712
+ token: this.token,
713
+ };
714
+ this.advance();
715
+ if (this.token.type === TOK.NUMBER && !this.token.text.startsWith('-')) {
716
+ const value = this.token.text;
717
+ this.advance();
718
+ return numberTerm(`-${value}`);
719
+ }
720
+ this.pos = state.pos;
721
+ this.line = state.line;
722
+ this.previousToken = state.previousToken;
723
+ this.token = state.token;
724
+ }
627
725
  if (operatorName != null && this.prefixOperators.get(operatorName)?.precedence >= minPrecedence) {
628
726
  const op = operatorName;
629
727
  const info = this.prefixOperators.get(op);
@@ -633,26 +731,30 @@ class Parser {
633
731
  // an argument delimiter there is no operand for prefix syntax, so keep
634
732
  // the operator as an atom instead of reporting a misleading bad-term
635
733
  // error.
636
- if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.BAR].includes(this.token.type)) {
734
+ if ([TOK.COMMA, TOK.RPAREN, TOK.RBRACKET, TOK.BAR, TOK.DOT].includes(this.token.type)) {
735
+ if (this.strictIso && !allowOperatorAtom && this.token.type !== TOK.DOT) {
736
+ throw new Error(`parse line ${this.token.line}: operator atom ${op} requires argument context or parentheses`);
737
+ }
637
738
  return atom(op);
638
739
  }
639
740
  if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) {
640
- this.advance();
641
- const args = [];
642
- while (true) {
643
- args.push(this.parseTerm(0, false, false));
644
- if (this.token.type !== TOK.COMMA) break;
645
- this.advance();
646
- }
647
- this.expect(TOK.RPAREN, ')');
648
- this.advance();
649
- return compound(op, args);
741
+ return this.parseFunctionalNotation(op);
650
742
  }
651
- return compound(op, [this.parseTerm(info.precedence + (info.strict ? 1 : 0), false, allowBar)]);
743
+ return compound(op, [this.parseTerm(info.precedence + (info.strict ? 1 : 0), false, allowBar, false)]);
652
744
  }
653
745
  if (this.token.type === TOK.LPAREN) return this.parseParenthesizedTerm();
654
- if (this.token.type === TOK.LBRACKET) return this.parseList();
655
- if (this.token.type === TOK.LBRACE) return this.parseCurly();
746
+ if (this.token.type === TOK.LBRACKET) {
747
+ const list = this.parseList();
748
+ if (list.type === ATOM && list.name === '[]' && this.token.type === TOK.LPAREN &&
749
+ this.token.precededByLayout !== true) return this.parseFunctionalNotation('[]');
750
+ return list;
751
+ }
752
+ if (this.token.type === TOK.LBRACE) {
753
+ const curly = this.parseCurly();
754
+ if (curly.type === ATOM && curly.name === '{}' && this.token.type === TOK.LPAREN &&
755
+ this.token.precededByLayout !== true) return this.parseFunctionalNotation('{}');
756
+ return curly;
757
+ }
656
758
  if (this.token.type === TOK.VAR) {
657
759
  const name = this.token.text;
658
760
  this.advance();
@@ -668,21 +770,7 @@ class Parser {
668
770
  const value = this.token.text;
669
771
  this.advance();
670
772
  if (this.parserFlagState.doubleQuotes === 'atom') {
671
- if (this.token.type === TOK.LPAREN) {
672
- this.advance();
673
- const args = [];
674
- if (this.token.type === TOK.RPAREN) {
675
- throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(value)} for arity zero data`);
676
- }
677
- while (true) {
678
- args.push(this.parseTerm(0, false, false));
679
- if (this.token.type !== TOK.COMMA) break;
680
- this.advance();
681
- }
682
- this.expect(TOK.RPAREN, ')');
683
- this.advance();
684
- return compound(value, args);
685
- }
773
+ if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) return this.parseFunctionalNotation(value);
686
774
  return atom(value);
687
775
  }
688
776
  const items = Array.from(value, (character) =>
@@ -701,23 +789,12 @@ class Parser {
701
789
  if (this.token.type === TOK.ATOM) {
702
790
  const name = this.token.text;
703
791
  this.advance();
704
- if (this.token.type === TOK.LPAREN) {
705
- this.advance();
706
- const args = [];
707
- if (this.token.type === TOK.RPAREN) {
708
- throw new Error(`parse line ${this.token.line}: zero-arity compound syntax is not supported; use atom ${JSON.stringify(name)} for arity zero data`);
709
- }
710
- while (true) {
711
- args.push(this.parseTerm(0, false, false));
712
- if (this.token.type === TOK.COMMA) {
713
- this.advance();
714
- continue;
715
- }
716
- break;
717
- }
718
- this.expect(TOK.RPAREN, ')');
719
- this.advance();
720
- return compound(name, args);
792
+ if (this.token.type === TOK.LPAREN && this.token.precededByLayout !== true) {
793
+ return this.parseFunctionalNotation(name);
794
+ }
795
+ if (this.strictIso && !allowOperatorAtom &&
796
+ (this.infixOperators.has(name) || this.prefixOperators.has(name) || this.postfixOperators.has(name))) {
797
+ throw new Error(`parse line ${this.token.line}: operator atom ${name} requires argument context or parentheses`);
721
798
  }
722
799
  return atom(name);
723
800
  }
@@ -842,7 +919,7 @@ class Parser {
842
919
  this.token = headState.token;
843
920
  };
844
921
 
845
- let head = this.parseTerm(3);
922
+ let head = this.parseTerm(3, false, true, true);
846
923
  if (this.token.type === TOK.COMMA && !this.strictIso) {
847
924
  restoreHeadState();
848
925
  const quadId = this.parseTerm(3, true);
@@ -852,7 +929,7 @@ class Parser {
852
929
  continue;
853
930
  }
854
931
  restoreHeadState();
855
- head = this.parseTerm(3);
932
+ head = this.parseTerm(3, false, true, true);
856
933
  }
857
934
 
858
935
  // Outside quad syntax, preserve the existing program-level comma rule,
@@ -1387,11 +1464,13 @@ export function parseNumberTokenText(text) {
1387
1464
  const digitsStart = position;
1388
1465
  while (isDigitCode(source.charCodeAt(position))) position++;
1389
1466
  if (position === digitsStart) throw new Error('not exactly one number token');
1467
+ let hasFraction = false;
1390
1468
  if (source[position] === '.' && isDigitCode(source.charCodeAt(position + 1))) {
1469
+ hasFraction = true;
1391
1470
  position++;
1392
1471
  while (isDigitCode(source.charCodeAt(position))) position++;
1393
1472
  }
1394
- if (source[position] === 'e' || source[position] === 'E') {
1473
+ if (hasFraction && (source[position] === 'e' || source[position] === 'E')) {
1395
1474
  position++;
1396
1475
  if (source[position] === '+' || source[position] === '-') position++;
1397
1476
  const exponentStart = position;
@@ -1399,6 +1478,8 @@ export function parseNumberTokenText(text) {
1399
1478
  if (position === exponentStart) throw new Error('not exactly one number token');
1400
1479
  }
1401
1480
  if (position !== source.length) throw new Error('not exactly one number token');
1481
+ if (/^-?\d+$/.test(source)) return numberTerm(BigInt(source).toString());
1482
+ if (Object.is(Number(source), -0)) return numberTerm('0.0');
1402
1483
  return numberTerm(source);
1403
1484
  }
1404
1485
 
package/src/program.js CHANGED
@@ -5,6 +5,7 @@ import { numberValueKey } from './number-value.js';
5
5
  import { formatTermForWrite } from './write.js';
6
6
  import {
7
7
  ISO_OPERATOR_DEFINITIONS,
8
+ PART3_OPERATOR_DEFINITIONS,
8
9
  QUAD_OPERATOR_DEFINITIONS,
9
10
  createParserOperatorState,
10
11
  parseClauses,
@@ -106,7 +107,7 @@ export class Program {
106
107
  this.operators = new Map();
107
108
  const predefinedOperatorSets = this.strictIso
108
109
  ? [ISO_OPERATOR_DEFINITIONS]
109
- : [ISO_OPERATOR_DEFINITIONS, QUAD_OPERATOR_DEFINITIONS];
110
+ : [ISO_OPERATOR_DEFINITIONS, PART3_OPERATOR_DEFINITIONS, QUAD_OPERATOR_DEFINITIONS];
110
111
  for (const definitions of predefinedOperatorSets) {
111
112
  for (const [priority, specifier, name] of definitions) {
112
113
  this.defineOperator(priority, specifier, name);
@@ -34,6 +34,11 @@ export function characterCodeConstantEnd(source, apostropheIndex) {
34
34
  const characterIndex = apostropheIndex + 1;
35
35
  const character = source[characterIndex] ?? '';
36
36
  if (!character) return apostropheIndex;
37
+ // `0''` is two tokens (0 and the empty atom), and `0'\\\n...'
38
+ // likewise starts a quoted atom containing a continuation. Only `0'''`
39
+ // denotes the character-code constant for an apostrophe.
40
+ if (character === "'" && source[characterIndex + 1] !== "'") return null;
41
+ if (character === '\\' && ['\n', '\r'].includes(source[characterIndex + 1])) return null;
37
42
  if (character === '\\') return quotedEscapeEnd(source, characterIndex);
38
43
 
39
44
  // An apostrophe character is doubled in 0''' exactly as it is in a quoted
package/src/term.js CHANGED
@@ -413,9 +413,26 @@ function writeList(term, env, options) {
413
413
  }
414
414
 
415
415
  export function termToString(term, env = new Env(), quoteStrings = true, options = {}) {
416
- options = { doubleQuotes: options.doubleQuotes ?? 'chars' };
416
+ options = {
417
+ ...options,
418
+ doubleQuotes: options.doubleQuotes ?? 'chars',
419
+ readVariableNames: options.readVariableNames instanceof Map ? options.readVariableNames : new Map(),
420
+ usedReadVariableNames: options.usedReadVariableNames instanceof Set ? options.usedReadVariableNames : new Set(),
421
+ };
417
422
  const resolved = deref(term, env);
418
- if (resolved.type === VAR) return writeVariable(resolved.name);
423
+ if (resolved.type === VAR) {
424
+ if (resolved.displayName == null) return writeVariable(resolved.name);
425
+ let printed = options.readVariableNames.get(resolved.name);
426
+ if (printed == null) {
427
+ const base = writeVariable(resolved.displayName);
428
+ printed = base;
429
+ let suffix = 1;
430
+ while (options.usedReadVariableNames.has(printed)) printed = `${base}_${suffix++}`;
431
+ options.readVariableNames.set(resolved.name, printed);
432
+ options.usedReadVariableNames.add(printed);
433
+ }
434
+ return printed;
435
+ }
419
436
  if (isCons(resolved)) return writeList(resolved, env, options);
420
437
  if (resolved.type === STRING) return writeString(resolved.name, quoteStrings);
421
438
  if (resolved.type === ATOM) return writeAtom(resolved.name);