author | Alan Dipert
<alan@dipert.org> 2025-09-16 22:10:33 UTC |
committer | Alan Dipert
<alan@dipert.org> 2025-09-16 22:10:33 UTC |
parent | e21a23ac8ba29de827602b85a79f31e6a3b6af19 |
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>