git » fjorth.git » master » tree

[master] / test.js

(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(); });
  }
})();