144 lines
5 KiB
JavaScript
144 lines
5 KiB
JavaScript
'use strict';
|
||
/* Interpreter tests — run with: node --test "js/test/*.test.mjs"
|
||
* Asserts EXACT outputs for every program mpl.codes ships, so the site can
|
||
* never claim an example works that doesn't. Migrated from the mpl_codes
|
||
* test suite (test/mpl.test.js @ 5b50cc0) with every assertion's input and
|
||
* expected value preserved exactly; only the runner mechanics changed.
|
||
*/
|
||
import test from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
import { runMPL, ESCAPES } from './load.mjs';
|
||
import { SYMBOLS, EXERCISES, DEFAULT_PROGRAM, HERO_PROGRAM } from './fixtures.mjs';
|
||
|
||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||
|
||
function run(src) {
|
||
const out = [];
|
||
runMPL(src, s => out.push(s));
|
||
return out;
|
||
}
|
||
|
||
test('hero program computes factorial', () => {
|
||
assert.deepEqual(run(HERO_PROGRAM), ['5! = 120']);
|
||
});
|
||
|
||
test('default playground program', () => {
|
||
assert.deepEqual(run(DEFAULT_PROGRAM), ['Salaam, Fatima!', '5! = 120']);
|
||
});
|
||
|
||
const EXPECTED_EXERCISES = [
|
||
['Hello, World!', 'Jambo!', '你好!', 'مرحبا!'],
|
||
['Area = 15'],
|
||
['5! = 120'],
|
||
['Σ = 55'],
|
||
['2', '4', '6', '8'],
|
||
['42'],
|
||
];
|
||
|
||
test('all six exercises produce their documented output', () => {
|
||
assert.equal(EXERCISES.length, EXPECTED_EXERCISES.length);
|
||
EXERCISES.forEach((ex, i) => {
|
||
assert.deepEqual(run(ex.code), EXPECTED_EXERCISES[i], `exercise ${i + 1}`);
|
||
});
|
||
});
|
||
|
||
test('all six type symbols lex as inert ∈-constraint atoms', () => {
|
||
// ℕ ℤ ℚ ℝ ℂ 𝔹 parse in constraint position and are discarded unevaluated
|
||
// (§5: "parsed today, not yet enforced"). 𝔹 is supplementary-plane: the
|
||
// lexer must treat the surrogate pair as one code point.
|
||
for (const T of ['ℕ', 'ℤ', 'ℚ', 'ℝ', 'ℂ', '𝔹']) {
|
||
assert.deepEqual(run(`f ≜ λx∈${T}: (x ⟹ 1) | 0;\n✎ f(true);`), ['1'],
|
||
`type symbol ${T}`);
|
||
}
|
||
});
|
||
|
||
test('𝔹 (U+1D539) lexes as one code point, not two surrogate halves', () => {
|
||
// The exact gate case:
|
||
assert.deepEqual(run('f ≜ λx∈𝔹: (x ⟹ 1) | 0; ✎ f(true);'), ['1']);
|
||
// And no half-pair matching: 𝕊 (U+1D54A) shares 𝔹's high surrogate
|
||
// \uD835 but is NOT a type symbol — it must be rejected whole.
|
||
try {
|
||
run('✎ 𝕊;');
|
||
assert.fail('𝕊 should not lex');
|
||
} catch (e) {
|
||
assert.equal(e.key, 'err_char');
|
||
}
|
||
});
|
||
|
||
test('vendored examples 01 and 02 run in the M0 browser core', () => {
|
||
// 03–10 use constructs beyond the browser core (modules, resources,
|
||
// channels, metaprogramming) — running them is deliberately NOT claimed.
|
||
const ex = n => fs.readFileSync(path.join(__dirname, '..', '..', 'examples', n), 'utf8');
|
||
assert.deepEqual(run(ex('01_hello_world.mpl')), ['Hello, World!']);
|
||
assert.deepEqual(run(ex('02_factorial.mpl')), ['120']);
|
||
});
|
||
|
||
test('undefined name reports err_undef at 1:1', () => {
|
||
try {
|
||
run('x + 1;');
|
||
assert.fail('should have thrown');
|
||
} catch (e) {
|
||
assert.equal(e.key, 'err_undef');
|
||
assert.equal(e.line, 1);
|
||
assert.equal(e.col, 1);
|
||
}
|
||
});
|
||
|
||
test('division by zero reports err_div0', () => {
|
||
try {
|
||
run('✎ 1 ÷ 0;');
|
||
assert.fail('should have thrown');
|
||
} catch (e) {
|
||
assert.equal(e.key, 'err_div0');
|
||
}
|
||
});
|
||
|
||
test('SYMBOLS table: one escape per glyph, all escapes known to the lexer', () => {
|
||
const glyphs = new Set();
|
||
const escapes = new Set();
|
||
for (const [glyph, esc] of SYMBOLS) {
|
||
assert.ok(!glyphs.has(glyph), `duplicate glyph ${glyph}`);
|
||
assert.ok(!escapes.has(esc), `duplicate escape ${esc}`);
|
||
glyphs.add(glyph);
|
||
escapes.add(esc);
|
||
if (esc.startsWith('\\')) {
|
||
// the palette's escape must be exactly what the lexer expands
|
||
assert.equal(ESCAPES[esc.slice(1)], glyph,
|
||
`lexer ESCAPES['${esc.slice(1)}'] must map to ${glyph}`);
|
||
} else {
|
||
// ASCII symbols (like |) are their own spelling
|
||
assert.equal(esc, glyph);
|
||
}
|
||
}
|
||
});
|
||
|
||
test('every palette escape lexes identically to its glyph', () => {
|
||
// One program that uses all fifteen backslash-escaped glyphs.
|
||
const glyphProgram = [
|
||
'check ≜ λa, b: ((a ≤ b) ∧ (b ≥ a) ∧ (a ≠ b) ⟹ ✎ "cmp") | ⊥;',
|
||
'check(1, 2);',
|
||
't ← 0;',
|
||
'∀ n ∈ [1, 2, 3]: t ← t + n;',
|
||
'✎(t × 2);',
|
||
'✎(t ÷ 2);',
|
||
'✎((true ∨ false) ⟹ "or");',
|
||
].join('\n');
|
||
// Verify coverage: every escaped SYMBOLS glyph appears in the program.
|
||
for (const [glyph, esc] of SYMBOLS) {
|
||
if (esc.startsWith('\\')) {
|
||
assert.ok(glyphProgram.includes(glyph), `probe program must use ${glyph}`);
|
||
}
|
||
}
|
||
// Build the escape-spelled variant (a space terminates each escape word).
|
||
let escProgram = glyphProgram;
|
||
for (const [glyph, esc] of SYMBOLS) {
|
||
if (esc.startsWith('\\')) escProgram = escProgram.split(glyph).join(esc + ' ');
|
||
}
|
||
const expected = ['cmp', '12', '3', 'or'];
|
||
assert.deepEqual(run(glyphProgram), expected);
|
||
assert.deepEqual(run(escProgram), expected, 'escape spelling must behave identically');
|
||
});
|