git » fjorth.git » commit 36f3c7b

errors

author Alan Dipert
2025-09-16 22:10:33 UTC
committer Alan Dipert
2025-09-16 22:10:33 UTC
parent e21a23ac8ba29de827602b85a79f31e6a3b6af19

errors

fjorth.html +81 -1

diff --git a/fjorth.html b/fjorth.html
index 2da4837..8632daf 100644
--- a/fjorth.html
+++ b/fjorth.html
@@ -203,7 +203,7 @@
           case OP.RET: if(this.R.length){ ip = this.R.pop(); } else { this.currentIp=null; } break;
           case OP.JUMP: { const dest = this.H[ip++]; if(dest==null || dest<0 || dest>=this.here || this.H[dest]===undefined){ this.fail({...errBase, code:'bad_jump', message:'invalid jump '+dest}); return; } ip = dest; } break;
           case OP.JZ: { const dest=this.H[ip++]; const flag=this.pop(); if(this.lastError) return; if(flag===0){ if(dest==null || dest<0 || dest>=this.here || this.H[dest]===undefined){ this.fail({...errBase, code:'bad_jump', message:'invalid jump '+dest}); return; } ip=dest; } } break;
-          case OP.DUP: this.push(this.peek()); break;
+          case OP.DUP: { const v=this.peek(); if(this.lastError) return; this.push(v); } break;
           case OP.DROP: this.pop(); break;
           case OP.SWAP: { const a=this.pop(), b=this.pop(); if(this.lastError) return; this.push(a); this.push(b); } break;
           case OP.OVER: { const n=this.D.length; if(n<2){ this.fail({...errBase, code:'stack_underflow', message:'stack underflow'}); return; } this.push(this.D[n-2]); } break;
@@ -539,5 +539,85 @@
   <!-- If local qunit.js isn't available in your environment, replace the line above with:
   <script id="qunit-js" src="https://app.unpkg.com/qunit@2.24.1/files/qunit/qunit.js"></script>
   -->
+  <!-- Additional tests: Structured Error Handling -->
+  <script>
+  (function(){
+    function runCodeExpectErr(code, vmOpts={}) {
+      return new Promise((resolve) => {
+        const seen=[];
+        const vm = new Forth(Object.assign({ onError: e => seen.push(e), stepBudget: 2000, sliceMs: 0 }, vmOpts));
+        try {
+          vm.reset();
+          vm.load(code);
+          vm.run((err) => {
+            resolve({ vm, err, seen });
+          });
+        } catch (e) {
+          resolve({ vm, err: e, seen: [...seen, e] });
+        }
+      });
+    }
+
+    function startErrTests(){
+      QUnit.module('Structured Error Handling (callback API)');
+
+      QUnit.test('undefined word: onError + idle get same Error with rich fields', async assert => {
+        const { vm, err, seen } = await runCodeExpectErr('NOPE');
+        assert.ok(err instanceof Error, 'idle callback receives Error');
+        assert.strictEqual(seen.length, 1, 'onError called once');
+        assert.strictEqual(seen[0], err, 'onError error === idle error');
+        assert.strictEqual(err.code, 'undefined_word');
+        assert.strictEqual(err.phase, 'interpret');
+        assert.strictEqual(err.token, 'NOPE');
+        assert.strictEqual(vm.lastError, err, 'vm.lastError points to same error');
+        assert.strictEqual(vm.running, false, 'vm halted');
+      });
+
+      QUnit.test('stack underflow: structured exec error and halt, no further tokens executed', async assert => {
+        const { vm, err } = await runCodeExpectErr('DUP 1 2 +');
+        assert.strictEqual(err.code, 'stack_underflow');
+        assert.strictEqual(err.phase, 'exec');
+        assert.ok(typeof err.stackDepth === 'number', 'err.stackDepth is number');
+        assert.ok(typeof err.rstackDepth === 'number', 'err.rstackDepth is number');
+        assert.ok(typeof err.ip === 'number' || typeof err.ip === 'undefined', 'err.ip present or undefined');
+        assert.strictEqual(vm.D.length, 0, 'no subsequent execution after error');
+      });
+
+      QUnit.test('rstack underflow: structured exec error', async assert => {
+        const { err } = await runCodeExpectErr('R>');
+        assert.strictEqual(err.code, 'rstack_underflow');
+        assert.strictEqual(err.phase, 'exec');
+      });
+
+      QUnit.test('compile-time misuse: ELSE outside compile', async assert => {
+        const { err } = await runCodeExpectErr('ELSE');
+        assert.strictEqual(err.code, 'else_outside_compile');
+        assert.strictEqual(err.phase, 'compile');
+      });
+
+      QUnit.test('compile-time: ":" with no name', async assert => {
+        const { err } = await runCodeExpectErr(':');
+        assert.strictEqual(err.code, 'colon_no_name');
+        assert.strictEqual(err.phase, 'compile');
+      });
+
+      QUnit.test('errors do not throw synchronously from run()', assert => {
+        assert.expect(1);
+        const vm = new Forth({});
+        vm.reset();
+        vm.load('NOPE');
+        let threw = false;
+        try { vm.run(() => {}); } catch (e) { threw = true; }
+        assert.strictEqual(threw, false, 'no synchronous throw');
+      });
+    }
+
+    if (window.QUnit) {
+      startErrTests();
+    } else {
+      window.addEventListener('load', () => { if (window.QUnit) startErrTests(); });
+    }
+  })();
+  </script>
 </body>
 </html>