(function(){
function startTests(){
// Helper to feed exact Forth source while avoiding JS escape confusion.
// Usage: runCode(R(String.raw`2 3 \ line\n +`))
// R() converts the two characters "\\n" into a real newline so the interpreter sees backslash-to-EOL then the next line.
const R = (raw) => raw.replace(/\\n/g, '\n');
function runCode(code) {
return new Promise((resolve, reject) => {
const out = [];
const vm = new Fjorth({
print: s => { out.push(s); },
println: s => { out.push(s + '\n'); },
stepBudget: 2000,
sliceMs: 0
});
try {
vm.reset();
vm.load(code);
vm.run((err) => {
if (err) reject(err);
else resolve({ vm, out: out.join('') });
});
} catch (e) { reject(e); }
});
}
QUnit.module('Fjorth');
QUnit.test('add', async assert => {
const { vm } = await runCode('2 3 +');
assert.strictEqual(vm.D.length, 1, 'stack has one item');
assert.strictEqual(vm.D[0], 5, '2 3 + => 5');
});
QUnit.test('square via colon + call + *', async assert => {
const { vm } = await runCode(': SQUARE DUP * ; 7 SQUARE');
assert.strictEqual(vm.D.length, 1, 'stack has one item');
assert.strictEqual(vm.D[0], 49, '7 SQUARE => 49');
});
QUnit.test('paren comment', async assert => {
const { vm } = await runCode('( hi ) 2 3 +');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 5);
});
QUnit.test('backslash to EOL comment', async assert => {
const { vm } = await runCode(R(String.raw`2 3 \\ line\n +`));
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 5);
});
QUnit.test('backslash at end of line only', async assert => {
const { vm } = await runCode(R(String.raw`2 \n 3 +`));
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 5);
});
QUnit.test('1- then 1+', async assert => {
const { vm } = await runCode('10 1- 1+');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 10);
});
QUnit.test('return stack accessors', async assert => {
const { vm } = await runCode(': SAVE >R R@ R> ; 123 SAVE');
assert.strictEqual(vm.D.length, 2, 'two values on stack');
assert.strictEqual(vm.D[0], 123);
assert.strictEqual(vm.D[1], 123);
});
QUnit.test('return stack roundtrip', async assert => {
const { vm } = await runCode(': PASS >R R> ; 7 PASS');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 7);
});
QUnit.test('nested calls', async assert => {
const { vm } = await runCode(': B 1+ ; : A 2 + B ; 39 A');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 42);
});
QUnit.test('IF/ELSE/THEN true branch', async assert => {
const { vm } = await runCode(': T 1 IF 42 ELSE 99 THEN ; T');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 42);
});
QUnit.test('IF/ELSE/THEN false branch', async assert => {
const { vm } = await runCode(': F 0 IF 42 ELSE 99 THEN ; F');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 99);
});
QUnit.test('BEGIN/UNTIL countdown with 0=', async assert => {
const { vm } = await runCode(': CD ( n -- ) BEGIN 1- DUP 0= UNTIL DROP ; 3 CD');
assert.strictEqual(vm.D.length, 0, 'stack empty after loop');
});
// Additional negative tests to lock semantics
QUnit.test('ELSE outside compile errors', async assert => {
await assert.rejects(runCode('ELSE'), /ELSE outside compile/);
});
QUnit.test('BEGIN outside compile errors', async assert => {
await assert.rejects(runCode('BEGIN'), /BEGIN outside compile/);
});
QUnit.test('unbalanced control flow at ;', async assert => {
await assert.rejects(runCode(': X IF 1 ;'), /unbalanced control flow/);
});
// ===== Additional Coverage =====
QUnit.module('Stack & Arithmetic');
QUnit.test('DUP, DROP, SWAP, OVER', async assert => {
const { vm } = await runCode('1 2 DUP SWAP OVER DROP');
assert.deepEqual(vm.D, [1,2,2]);
});
QUnit.test('add/sub/mul with negatives', async assert => {
const { vm: vm1 } = await runCode('10 3 -');
assert.strictEqual(vm1.D[0], 7);
const { vm: vm2 } = await runCode('3 10 -');
assert.strictEqual(vm2.D[0], -7);
const { vm: vm3 } = await runCode('6 7 *');
assert.strictEqual(vm3.D[0], 42);
const { vm: vm4 } = await runCode('-7 3 +');
assert.strictEqual(vm4.D[0], -4);
});
QUnit.module('Memory & Dictionary');
QUnit.test('HERE , @ roundtrip', async assert => {
const { vm } = await runCode('HERE 0 , DUP 99 SWAP ! @');
assert.strictEqual(vm.D.length, 1);
assert.strictEqual(vm.D[0], 99);
});
QUnit.test("' tick returns execution token", async assert => {
const { vm } = await runCode(': SQUARE DUP * ; \' SQUARE');
const xt = vm.D[0];
assert.strictEqual(xt, vm.find('SQUARE').xt, 'xt on stack matches dictionary entry');
});
QUnit.test('IMMEDIATE marks last word immediate', async assert => {
const { vm } = await runCode(': I 123 ; IMMEDIATE');
const w = vm.find('I');
assert.ok(w && w.immediate === true, 'I is immediate');
});
QUnit.module('Tokenizer & Comments');
QUnit.test('mixed comments (paren + backslash)', async assert => {
const { vm } = await runCode(R(String.raw`(foo) 1 \\ bar\n 2 +`));
assert.strictEqual(vm.D[0], 3);
});
QUnit.module('Output');
QUnit.test('EMIT + CR output', async assert => {
const { out } = await runCode('65 EMIT 66 EMIT CR');
assert.strictEqual(out, 'AB\n');
});
QUnit.test('. prints with a trailing space', async assert => {
const { out } = await runCode('42 .');
assert.strictEqual(out, '42 ');
});
QUnit.test('.S prints stack contents', async assert => {
const { out } = await runCode('1 2 3 .S');
assert.strictEqual(out, '<3> 1 2 3\n');
});
QUnit.module('Errors');
QUnit.test('undefined word throws', async assert => {
await assert.rejects(runCode('NOPE'), /undefined word: NOPE/);
});
QUnit.test('data stack underflow', async assert => {
await assert.rejects(runCode('DUP'), /stack underflow/);
});
QUnit.test('return stack underflow', async assert => {
await assert.rejects(runCode('R>'), /rstack underflow/);
});
QUnit.test('colon with no name', async assert => {
await assert.rejects(runCode(':'), /expected name after/);
});
QUnit.module('API');
function runWithVM(setup, code){
return new Promise((resolve, reject)=>{
const out=[];
const vm=new Fjorth({ print:s=>out.push(s), println:s=>out.push(s+'\n'), stepBudget:200, sliceMs:0 });
try{ vm.reset(); if(setup) setup(vm); vm.load(code); vm.run(err=>{ if(err) reject(err); else resolve({vm, out:out.join('')}); }); }
catch(e){ reject(e); }
});
}
QUnit.test('defineJS adds external word', async assert => {
const { vm } = await runWithVM(vm=>{
vm.defineJS('TWICE', function(){ const a=this.pop(); this.push(a*2); });
}, '21 TWICE');
assert.strictEqual(vm.D[0], 42);
});
QUnit.test('defineJS immediate flag is recorded', async assert => {
const { vm } = await runWithVM(vm=>{
vm.defineJS('IWORD', function(){ this.push(9); }, true);
}, ': T IWORD ;');
const w = vm.find('IWORD');
assert.ok(w && w.immediate===true);
});
QUnit.test('pause/resume on long program still completes', async assert => {
// Build a long program: 0 then N times (1 +)
const N = 1000;
const code = '0' + ' 1 +'.repeat(N);
const { vm } = await new Promise((resolve, reject)=>{
const out=[]; const vm=new Fjorth({ print:s=>out.push(s), println:s=>out.push(s+'\n'), stepBudget:10, sliceMs:0 });
try{ vm.reset(); vm.load(code); vm.run(err=>{ if(err) reject(err); else resolve({vm, out:out.join('')}); }); setTimeout(()=>{ vm.pause(); setTimeout(()=> vm.resume(), 0); }, 0); }
catch(e){ reject(e); }
});
assert.strictEqual(vm.D[0], 1000, 'result equals N after resume');
});
}
// Start after QUnit loads (local or CDN)
if (window.QUnit) {
startTests();
} else {
const s = document.getElementById('qunit-js');
if (s) s.addEventListener('load', () => { if (window.QUnit) startTests(); });
window.addEventListener('load', () => { if (window.QUnit) startTests(); });
}
})();