eyeprolog 1.2.9 → 1.2.11
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/conformance-report.md +2 -2
- package/package.json +1 -1
- package/src/explain.js +3 -2
- package/src/iso.js +7 -0
- package/src/parser.js +7 -1
- package/src/solver.js +22 -5
- package/src/term.js +22 -9
- package/test/conformance/ISO-MATRIX.md +1 -1
- package/test/conformance/README.md +1 -1
- package/test/conformance/errors/iso/number_chars_parenthesized.pl +2 -0
- package/test/conformance/errors/iso/number_codes_parenthesized.pl +2 -0
- package/test/conformance/expected-errors/iso/number_chars_parenthesized.txt +1 -0
- package/test/conformance/expected-errors/iso/number_codes_parenthesized.txt +1 -0
- package/test/fixtures/number_chars_cont_quad.pl +3 -0
- package/test/run-regression.mjs +65 -3
- package/the-art-of-eyeprolog.md +20 -6
package/conformance-report.md
CHANGED
|
@@ -11,7 +11,7 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
11
11
|
| builtins | 11 | 0 | 0 | 0 | 11 |
|
|
12
12
|
| context | 11 | 0 | 0 | 0 | 11 |
|
|
13
13
|
| control | 15 | 0 | 0 | 0 | 15 |
|
|
14
|
-
| iso | 167 |
|
|
14
|
+
| iso | 167 | 218 | 0 | 0 | 385 |
|
|
15
15
|
| lists | 52 | 3 | 0 | 0 | 55 |
|
|
16
16
|
| modules | 2 | 0 | 0 | 0 | 2 |
|
|
17
17
|
| negation | 8 | 0 | 19 | 0 | 27 |
|
|
@@ -23,4 +23,4 @@ This report summarizes the file-based conformance corpus under `test/conformance
|
|
|
23
23
|
| terms | 26 | 3 | 0 | 0 | 29 |
|
|
24
24
|
| unification | 18 | 0 | 0 | 0 | 18 |
|
|
25
25
|
| variables | 16 | 9 | 0 | 0 | 25 |
|
|
26
|
-
| **Total** | **482** | **
|
|
26
|
+
| **Total** | **482** | **269** | **19** | **21** | **791** |
|
package/package.json
CHANGED
package/src/explain.js
CHANGED
|
@@ -101,8 +101,9 @@ function* proveGoalAll(program, goal, env, depth, maxDepth, registry, active) {
|
|
|
101
101
|
for (let candidateIndex = 0; candidateIndex < clauseCandidateLength(pass); candidateIndex++) {
|
|
102
102
|
const clause = clauseCandidateAt(pass, candidateIndex);
|
|
103
103
|
const id = nextFreshId();
|
|
104
|
-
const
|
|
105
|
-
const
|
|
104
|
+
const freshVariables = new Map();
|
|
105
|
+
const freshHead = freshTerm(clause.head, id, freshVariables);
|
|
106
|
+
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
106
107
|
const next = env.clone();
|
|
107
108
|
if (!unify(goal, freshHead, next)) continue;
|
|
108
109
|
|
package/src/iso.js
CHANGED
|
@@ -1564,6 +1564,13 @@ function parseIsoNumber(text) {
|
|
|
1564
1564
|
}
|
|
1565
1565
|
|
|
1566
1566
|
const numericText = `${sign}${text.slice(position)}`;
|
|
1567
|
+
// 8.16.7/8.16.8 parse the character sequence according to the syntax rules
|
|
1568
|
+
// for numbers and negative numbers (6.3.1.1/6.3.1.2), not as an arbitrary
|
|
1569
|
+
// term whose value happens to be numeric. Every such number starts with a
|
|
1570
|
+
// decimal digit after an optional negative sign, so parenthesized terms such
|
|
1571
|
+
// as `(0)` or `-(0)` must be rejected before the general term parser sees
|
|
1572
|
+
// them. This keeps the parser reuse below from admitting grouping syntax.
|
|
1573
|
+
if (!/^-?\d/.test(numericText)) return null;
|
|
1567
1574
|
// ISO floating-point syntax requires a decimal fraction before an exponent.
|
|
1568
1575
|
if (/^-?\d+[eE][+-]?\d+$/.test(numericText)) return null;
|
|
1569
1576
|
try {
|
package/src/parser.js
CHANGED
|
@@ -165,6 +165,7 @@ class Parser {
|
|
|
165
165
|
this.pos = 0;
|
|
166
166
|
this.line = 1;
|
|
167
167
|
this.anonymous = 0;
|
|
168
|
+
this.variables = new Map();
|
|
168
169
|
this.sourceMetadata = options.sourceMetadata !== false;
|
|
169
170
|
this.strictIso = options.isoStrict === true;
|
|
170
171
|
this.parserFlagState = options.parserFlagState ?? {
|
|
@@ -633,7 +634,12 @@ class Parser {
|
|
|
633
634
|
const name = this.token.text;
|
|
634
635
|
this.advance();
|
|
635
636
|
if (name === '_') return variable(`__anon${this.anonymous++}`);
|
|
636
|
-
|
|
637
|
+
let term = this.variables.get(name);
|
|
638
|
+
if (term == null) {
|
|
639
|
+
term = variable(name);
|
|
640
|
+
this.variables.set(name, term);
|
|
641
|
+
}
|
|
642
|
+
return term;
|
|
637
643
|
}
|
|
638
644
|
if (this.token.type === TOK.STRING) {
|
|
639
645
|
const value = this.token.text;
|
package/src/solver.js
CHANGED
|
@@ -398,6 +398,8 @@ export class Solver {
|
|
|
398
398
|
break;
|
|
399
399
|
}
|
|
400
400
|
}
|
|
401
|
+
} catch (error) {
|
|
402
|
+
throw normalizeHostResourceError(error);
|
|
401
403
|
} finally {
|
|
402
404
|
const stackIndex = this.solveStacks.indexOf(registeredStack);
|
|
403
405
|
if (stackIndex >= 0) this.solveStacks.splice(stackIndex, 1);
|
|
@@ -450,8 +452,9 @@ export class Solver {
|
|
|
450
452
|
}
|
|
451
453
|
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
452
454
|
const id = nextFreshId();
|
|
453
|
-
const
|
|
454
|
-
const
|
|
455
|
+
const freshVariables = new Map();
|
|
456
|
+
const freshHead = freshTerm(clause.head, id, freshVariables);
|
|
457
|
+
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
455
458
|
const next = env.clone();
|
|
456
459
|
this.stats.unify_calls++;
|
|
457
460
|
if (!unify(goal, freshHead, next)) continue;
|
|
@@ -482,6 +485,19 @@ export class Solver {
|
|
|
482
485
|
|
|
483
486
|
}
|
|
484
487
|
|
|
488
|
+
function normalizeHostResourceError(error) {
|
|
489
|
+
if (error?.name !== 'RangeError') return error;
|
|
490
|
+
const message = String(error?.message ?? '');
|
|
491
|
+
// V8 reports exhausted Map/Set capacity as a host RangeError. ISO 7.12.2 h
|
|
492
|
+
// requires processor resource exhaustion to surface as resource_error/1,
|
|
493
|
+
// with the resource atom implementation dependent. EyeProlog uses the
|
|
494
|
+
// finite_memory spelling already accepted by its ISO conformance corpus.
|
|
495
|
+
if (/^(?:Map|Set) maximum size exceeded$/.test(message)) {
|
|
496
|
+
return new PrologError('resource_error(finite_memory)');
|
|
497
|
+
}
|
|
498
|
+
return error;
|
|
499
|
+
}
|
|
500
|
+
|
|
485
501
|
function qualifyMetaArguments(goal, group) {
|
|
486
502
|
const callerModule = goal.module ?? 'user';
|
|
487
503
|
for (const index of group.metaArgumentPositions ?? []) {
|
|
@@ -601,8 +617,9 @@ function pushUserGoalUncachedFrames(stack, solver, group, goal, rest, env, depth
|
|
|
601
617
|
}
|
|
602
618
|
if (headCannotMatch(goal, clause.head, env)) continue;
|
|
603
619
|
const id = nextFreshId();
|
|
604
|
-
const
|
|
605
|
-
const
|
|
620
|
+
const freshVariables = new Map();
|
|
621
|
+
const freshHead = freshTerm(clause.head, id, freshVariables);
|
|
622
|
+
const freshBody = clause.body.map((term) => freshTerm(term, id, freshVariables));
|
|
606
623
|
const next = env.clone();
|
|
607
624
|
solver.stats.unify_calls++;
|
|
608
625
|
if (!unify(goal, freshHead, next)) continue;
|
|
@@ -1233,7 +1250,7 @@ function copyResolvedWithKey(term, env, variables) {
|
|
|
1233
1250
|
id = variables.size;
|
|
1234
1251
|
variables.set(value.name, id);
|
|
1235
1252
|
}
|
|
1236
|
-
return { term: termModuleCache.variable(value.name), key: `var:${id}` };
|
|
1253
|
+
return { term: termModuleCache.variable(value.name, value.order), key: `var:${id}` };
|
|
1237
1254
|
}
|
|
1238
1255
|
if (!value.args?.length) {
|
|
1239
1256
|
return {
|
package/src/term.js
CHANGED
|
@@ -6,8 +6,10 @@ export const STRING = 'string';
|
|
|
6
6
|
export const NUMBER = 'number';
|
|
7
7
|
export const COMPOUND = 'compound';
|
|
8
8
|
const EMPTY_ARGS = Object.freeze([]);
|
|
9
|
+
// Variable term order is carried by the variable objects themselves. Do not
|
|
10
|
+
// retain a process-global name registry: runtime clause freshening can create
|
|
11
|
+
// an unbounded sequence of names in an otherwise constant-space computation.
|
|
9
12
|
let variableOrder = 0;
|
|
10
|
-
const variableOrders = new Map();
|
|
11
13
|
|
|
12
14
|
export class Term {
|
|
13
15
|
constructor(type, name, args = []) {
|
|
@@ -20,10 +22,9 @@ export class Term {
|
|
|
20
22
|
}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
export const variable = (name) => {
|
|
25
|
+
export const variable = (name, order = null) => {
|
|
24
26
|
const term = new Term(VAR, name, EMPTY_ARGS);
|
|
25
|
-
|
|
26
|
-
term.order = variableOrders.get(term.name);
|
|
27
|
+
term.order = order ?? ++variableOrder;
|
|
27
28
|
return term;
|
|
28
29
|
};
|
|
29
30
|
export const atom = (name) => new Term(ATOM, name, EMPTY_ARGS);
|
|
@@ -270,6 +271,7 @@ export function unify(left, right, env, options = {}) {
|
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
export function cloneTerm(term) {
|
|
274
|
+
if (term.type === VAR) return variable(term.name, term.order);
|
|
273
275
|
const cloned = term.type === COMPOUND && term.arity === 0
|
|
274
276
|
? atom(term.name)
|
|
275
277
|
: new Term(term.type, term.name, term.args.map(cloneTerm));
|
|
@@ -277,18 +279,25 @@ export function cloneTerm(term) {
|
|
|
277
279
|
return cloned;
|
|
278
280
|
}
|
|
279
281
|
|
|
280
|
-
export function freshTerm(term, suffix) {
|
|
281
|
-
if (term.type === VAR)
|
|
282
|
+
export function freshTerm(term, suffix, variables = new Map()) {
|
|
283
|
+
if (term.type === VAR) {
|
|
284
|
+
let fresh = variables.get(term.name);
|
|
285
|
+
if (fresh == null) {
|
|
286
|
+
fresh = variable(`${term.name}#${suffix}`);
|
|
287
|
+
variables.set(term.name, fresh);
|
|
288
|
+
}
|
|
289
|
+
return fresh;
|
|
290
|
+
}
|
|
282
291
|
const fresh = term.type === COMPOUND && term.arity === 0
|
|
283
292
|
? atom(term.name)
|
|
284
|
-
: new Term(term.type, term.name, term.args.map((arg) => freshTerm(arg, suffix)));
|
|
293
|
+
: new Term(term.type, term.name, term.args.map((arg) => freshTerm(arg, suffix, variables)));
|
|
285
294
|
if (term.module != null) fresh.module = term.module;
|
|
286
295
|
return fresh;
|
|
287
296
|
}
|
|
288
297
|
|
|
289
298
|
export function copyResolved(term, env) {
|
|
290
299
|
const resolved = deref(term, env);
|
|
291
|
-
if (resolved.type === VAR) return variable(resolved.name);
|
|
300
|
+
if (resolved.type === VAR) return variable(resolved.name, resolved.order);
|
|
292
301
|
const copied = resolved.type === COMPOUND && resolved.arity === 0
|
|
293
302
|
? atom(resolved.name)
|
|
294
303
|
: new Term(resolved.type, resolved.name, resolved.args.map((arg) => copyResolved(arg, env)));
|
|
@@ -517,9 +526,13 @@ export function compareTerms(left, right) {
|
|
|
517
526
|
return compareNumberText(left.name, right.name);
|
|
518
527
|
}
|
|
519
528
|
if (left.type === VAR) {
|
|
529
|
+
if (left.name === right.name) return 0;
|
|
520
530
|
const leftOrder = left.order ?? 0;
|
|
521
531
|
const rightOrder = right.order ?? 0;
|
|
522
|
-
|
|
532
|
+
if (leftOrder !== rightOrder) return leftOrder < rightOrder ? -1 : 1;
|
|
533
|
+
// Explicitly copied order values are permitted internally; keep the
|
|
534
|
+
// implementation-defined variable order total even in that case.
|
|
535
|
+
return left.name < right.name ? -1 : 1;
|
|
523
536
|
}
|
|
524
537
|
if (left.type === ATOM || left.type === STRING) return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
|
|
525
538
|
if (left.arity !== right.arity) return left.arity < right.arity ? -1 : 1;
|
|
@@ -16,7 +16,7 @@ compliance audit and the remaining work before a full conformance claim.
|
|
|
16
16
|
| 8.8-8.10 database and solutions | logical update view, dynamic mutation, all-solutions grouping | `dynamic_database`, `trealla_logical_update_view`, `corrigenda_retractall`, `grouped_solutions_and_clauses` |
|
|
17
17
|
| 8.11-8.14 streams and term I/O | text/binary streams, properties, units, read/write options and operators | `streams_and_term_io`, `operators`, Corrigendum 3 option cases, stream error cases |
|
|
18
18
|
| 8.15 logic and control | negation, once, repeat, `call/2` through `call/8`, `false/0` | `logtalk_once`, `corrigenda_call_closure`, `false_builtin` |
|
|
19
|
-
| 8.16 atomic processing | atoms, characters, codes and number conversion with prescribed errors | `atomic_term_processing`, focused forward/reverse cases, Logtalk-derived error cases |
|
|
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
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` |
|
|
@@ -102,7 +102,7 @@ Selected cases are adapted from the ISO and standard-core suites of Logtalk,
|
|
|
102
102
|
Scryer Prolog, Trealla Prolog, and SWI-Prolog. Their upstream identifiers and licenses
|
|
103
103
|
are recorded in [THIRD_PARTY.md](THIRD_PARTY.md).
|
|
104
104
|
|
|
105
|
-
The corpus has
|
|
105
|
+
The corpus has 385 cases in `iso/` and 791 file-based conformance cases in
|
|
106
106
|
total. The generated `conformance-report.md` is the authoritative source for
|
|
107
107
|
current category totals. Together with regression, documentation-sync, API,
|
|
108
108
|
example, and book-example checks, `npm test` is the release gate.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(syntax_error(number))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
error(syntax_error(number))
|
package/test/run-regression.mjs
CHANGED
|
@@ -396,9 +396,24 @@ c4 ?- call((!;1)).
|
|
|
396
396
|
text: source,
|
|
397
397
|
filename,
|
|
398
398
|
}]));
|
|
399
|
-
assertEqual(result.total,
|
|
400
|
-
assertEqual(result.passed,
|
|
401
|
-
assertEqual(result.stdout, 'quads:
|
|
399
|
+
assertEqual(result.total, 73, 'quad total');
|
|
400
|
+
assertEqual(result.passed, 73, 'quad passed');
|
|
401
|
+
assertEqual(result.stdout, 'quads: 73 run, 73 passed, 0 failed.\n', 'quad report');
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
name: 'number conversion rejects parenthesized numeric terms',
|
|
406
|
+
run: () => {
|
|
407
|
+
for (const goal of ['number_chars(N,"(0)")', 'number_codes(N,[40,48,41])']) {
|
|
408
|
+
let caught = null;
|
|
409
|
+
try {
|
|
410
|
+
publicApi.run('', { goal });
|
|
411
|
+
} catch (error) {
|
|
412
|
+
caught = error;
|
|
413
|
+
}
|
|
414
|
+
if (caught == null) throw new Error(`${goal} should throw`);
|
|
415
|
+
assertIncludes(String(caught?.message ?? caught), 'syntax_error(number)', goal);
|
|
416
|
+
}
|
|
402
417
|
},
|
|
403
418
|
},
|
|
404
419
|
{
|
|
@@ -2078,6 +2093,53 @@ open(X) :- candidate(X), \\+ closed(X).
|
|
|
2078
2093
|
assertEqual(answers.join('\n'), 'p(a)', 'answer beyond former default ceiling');
|
|
2079
2094
|
},
|
|
2080
2095
|
},
|
|
2096
|
+
{
|
|
2097
|
+
name: 'fresh-variable generation stays bounded under a small host heap',
|
|
2098
|
+
run: () => {
|
|
2099
|
+
const engineUrl = new URL('../src/index.js', import.meta.url).href;
|
|
2100
|
+
const programText = 'p(X) :- repeat, q(X).\nq(_).\n';
|
|
2101
|
+
const script = `
|
|
2102
|
+
import { Program, Solver, Env, parseGoalText, getEyePrologRegistry } from ${JSON.stringify(engineUrl)};
|
|
2103
|
+
const program = Program.parse(${JSON.stringify(programText)});
|
|
2104
|
+
const solver = new Solver(program, { registry: getEyePrologRegistry() });
|
|
2105
|
+
const goal = parseGoalText('p(X)');
|
|
2106
|
+
let count = 0;
|
|
2107
|
+
for (const _ of solver.solve([goal], new Env(), 0)) {
|
|
2108
|
+
if (++count === 300000) break;
|
|
2109
|
+
}
|
|
2110
|
+
if (count !== 300000) throw new Error('unexpected answer count: ' + count);
|
|
2111
|
+
process.stdout.write(String(count));
|
|
2112
|
+
`;
|
|
2113
|
+
const result = spawnSync(process.execPath, [
|
|
2114
|
+
'--max-old-space-size=32',
|
|
2115
|
+
'--input-type=module',
|
|
2116
|
+
'--eval',
|
|
2117
|
+
script,
|
|
2118
|
+
], { cwd: packageRoot, encoding: 'utf8', timeout: 30000 });
|
|
2119
|
+
if (result.error) throw result.error;
|
|
2120
|
+
assertEqual(result.status, 0, `bounded-heap child status; stderr=${result.stderr}`);
|
|
2121
|
+
assertEqual(result.stdout, '300000', 'fresh-variable answer count');
|
|
2122
|
+
},
|
|
2123
|
+
},
|
|
2124
|
+
{
|
|
2125
|
+
name: 'host Map capacity errors become ISO resource errors',
|
|
2126
|
+
run: () => {
|
|
2127
|
+
const registry = new BuiltinRegistry();
|
|
2128
|
+
registry.add('exhaust_map', 0, function* () {
|
|
2129
|
+
throw new RangeError('Map maximum size exceeded');
|
|
2130
|
+
});
|
|
2131
|
+
const solver = new Solver(Program.parse(''), { registry });
|
|
2132
|
+
const goal = parseGoalText('exhaust_map');
|
|
2133
|
+
let caught = null;
|
|
2134
|
+
try {
|
|
2135
|
+
[...solver.solve([goal], new Env(), 0)];
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
caught = error;
|
|
2138
|
+
}
|
|
2139
|
+
assertEqual(caught?.name, 'PrologError', 'normalized error type');
|
|
2140
|
+
assertEqual(caught?.formal, 'resource_error(finite_memory)', 'normalized resource error');
|
|
2141
|
+
},
|
|
2142
|
+
},
|
|
2081
2143
|
{
|
|
2082
2144
|
name: 'solver honors solution limits',
|
|
2083
2145
|
run: () => {
|
package/the-art-of-eyeprolog.md
CHANGED
|
@@ -1811,7 +1811,10 @@ child searches that inherit the solver limit do not stop after a fixed number of
|
|
|
1811
1811
|
solutions. This matters for re-executable goals such as `repeat/0` and for
|
|
1812
1812
|
library relations such as `call_nth/2`; an implementation safety threshold must
|
|
1813
1813
|
not turn a still re-executable search into logical failure. Embedders that need
|
|
1814
|
-
a finite answer budget should pass `solutionLimit` explicitly.
|
|
1814
|
+
a finite answer budget should pass `solutionLimit` explicitly. Host capacity
|
|
1815
|
+
failures that V8 reports as `Map maximum size exceeded` or `Set maximum size
|
|
1816
|
+
exceeded` are normalized at the solver boundary to the ISO error
|
|
1817
|
+
`resource_error(finite_memory)` instead of leaking a JavaScript `RangeError`.
|
|
1815
1818
|
|
|
1816
1819
|
### Implementation boundary
|
|
1817
1820
|
|
|
@@ -5611,8 +5614,16 @@ nonterminals.
|
|
|
5611
5614
|
For ISO terms, the standard term order is variables, numbers, atoms, then compounds;
|
|
5612
5615
|
compound terms compare by arity, functor, and arguments. Within the numeric
|
|
5613
5616
|
category, floats precede integers; floats compare by finite numeric value and
|
|
5614
|
-
integers compare exactly.
|
|
5615
|
-
|
|
5617
|
+
integers compare exactly. ISO leaves the ordering of two distinct variables
|
|
5618
|
+
implementation dependent, subject to stability while a sorted list is being
|
|
5619
|
+
created. EyeProlog assigns a creation ordinal to each logical variable and
|
|
5620
|
+
carries that ordinal on the variable term itself; repeated occurrences share it
|
|
5621
|
+
within parsing or clause-freshening scope. It therefore does not keep a
|
|
5622
|
+
process-global table of every fresh variable name ever created, so long-running
|
|
5623
|
+
generators can create and discard fresh variables without growing an unrelated
|
|
5624
|
+
host `Map`. Double-quoted Prolog source
|
|
5625
|
+
follows the `double_quotes` flag and never creates an extra host-only scalar
|
|
5626
|
+
category.
|
|
5616
5627
|
|
|
5617
5628
|
### Term construction and inspection
|
|
5618
5629
|
|
|
@@ -5724,13 +5735,16 @@ quoted_atom("ab"). % quoted_atom(ab)
|
|
|
5724
5735
|
| `sub_atom(+Atom,?Before,?Length,?After,?SubAtom)` | Enumerates substrings and their Unicode-code-point offsets. Supplied counts must be nonnegative integers. |
|
|
5725
5736
|
| `atom_chars(?Atom,?Chars)`, `atom_codes(?Atom,?Codes)` | Convert between an atom and a proper list of one-character atoms or Unicode scalar codes. At least one side must be instantiated. |
|
|
5726
5737
|
| `char_code(?Character,?Code)` | Converts one character atom and one Unicode scalar code. Surrogates and values outside `0..0x10ffff` raise a representation error. |
|
|
5727
|
-
| `number_chars(?Number,?Chars)`, `number_codes(?Number,?Codes)` | Convert finite numbers to canonical text or parse a proper character/code list using ISO
|
|
5738
|
+
| `number_chars(?Number,?Chars)`, `number_codes(?Number,?Codes)` | Convert finite numbers to canonical text or parse a proper character/code list using ISO number and negative-number syntax, including radix integers, character-code constants, and leading layout. The input is not parsed as a general term: grouping such as `(0)` is a syntax error. At least one side must be instantiated; malformed numeric input raises *syntax_error(number)*. |
|
|
5728
5739
|
|
|
5729
5740
|
Conversions accept partial output lists when the atomic input is known, but
|
|
5730
5741
|
constructing an atom or number requires a complete proper list with no unbound
|
|
5731
5742
|
elements. Numeric parsing accepts leading ISO layout characters, an optional
|
|
5732
5743
|
sign, decimal fractions, and decimal exponents; it rejects trailing material
|
|
5733
|
-
and non-finite values.
|
|
5744
|
+
and non-finite values. The regression gate vendors all 73 numbered cases from
|
|
5745
|
+
Ulrich Neumerkel's contemporary `number_chars/2` comparison, including the
|
|
5746
|
+
Cor.2 error-precedence cases; `number_codes/2` shares the same numeric parser
|
|
5747
|
+
and has mirrored coverage for the parenthesized-number regression.
|
|
5734
5748
|
|
|
5735
5749
|
### Streams and unit I/O
|
|
5736
5750
|
|
|
@@ -6975,7 +6989,7 @@ precedence still need one-by-one closure. `test/conformance/ISO-MATRIX.md`
|
|
|
6975
6989
|
maps language families to representative executable cases.
|
|
6976
6990
|
|
|
6977
6991
|
The complete suite must pass before release. The file-based conformance corpus
|
|
6978
|
-
contains
|
|
6992
|
+
contains 791 cases, including 385 focused ISO
|
|
6979
6993
|
cases derived from the success, failure, mode, and error behavior in
|
|
6980
6994
|
ISO/IEC 13211-1 clauses 7 and 8, Part 2 modules, and Part 3 grammar rules.
|
|
6981
6995
|
Separate exact-output suites check 189 normal
|