author | Alan Dipert
<alan@dipert.org> 2025-10-06 19:23:38 UTC |
committer | Alan Dipert
<alan@dipert.org> 2025-10-06 19:23:38 UTC |
parent | 90a3d9625312363aa8b2b99f0cc824c79e2096b3 |
README.md | +354 | -0 |
diff --git a/README.md b/README.md new file mode 100644 index 0000000..d2e0625 --- /dev/null +++ b/README.md @@ -0,0 +1,354 @@ +# Fjorth + +A hyper-minimal, embeddable Forth for the browser, written in JavaScript. +Design goals: tiny core, clear semantics, strong introspection and debugging hooks for pedagogical IDEs. + +This README is plain-text ASCII. Copy freely. + +--- + +## Features + +- Minimal stack-based VM with numeric bytecodes. +- Colon definitions, immediate words, basic control flow: + - IF ELSE THEN, BEGIN UNTIL, BEGIN AGAIN +- Data stack (D) and return stack (R) primitives: + - DUP DROP SWAP OVER, >R R> R@ +- Memory words and basic arithmetic: + - @ ! + - * 1+ 1- +- Output words: . EMIT CR .S +- Structured error reporting via callback (no throws on normal faults). +- Preemptible run loop (time-sliced) with pause/resume. +- Introspection and debugging layer: + - Source maps: ip -> token (line, col) + - Readable word call stack + - Tracing events (step, wordEnter, wordExit, break) + - Breakpoints by ip, by word+token, or by predicate + - Disassembler and memory map +- QUnit test suite (browser-based). + +--- + +## Quick Start + +Include Fjorth in a page: + +```html +<script src="fjorth.js"></script> +<script> + const vm = new Fjorth({ + print: s => { /* write to your console/output */ }, + println: s => { /* optional line printer */ }, + onError: err => console.warn("Fjorth error:", err) + }); + + vm.reset(); + vm.load(": SQUARE DUP * ; 7 SQUARE . CR"); + vm.run(err => { + if (err) { + console.log("Finished with error:", err); + } else { + console.log("Program completed."); + } + }); +</script> +``` + +Console should show: `49` followed by newline. + +--- + +## Public API (runtime) + +```js +const vm = new Fjorth(options) +``` + +Options: +- print(string): called by . and EMIT (required for visible output). +- println(string): optional line printer. +- onError(error): structured error callback (see Errors below). +- sliceMs: number, time between scheduler slices (default 0). +- stepBudget: number, max VM steps per slice (default 2000). + +Methods: +- vm.reset(): reset VM, stacks, dictionary, memory. +- vm.load(source: string): tokenize and stage Forth source. +- vm.run(onIdle?: (err?: Error) => void): start/resume execution; calls onIdle when idle or error. +- vm.pause(): pause time-sliced execution at next boundary. +- vm.resume(): resume if paused. +- vm.defineJS(name, fn, immediate=false): define a word implemented in JS. + - Inside fn, `this` is the VM. You can this.pop(), this.push(), etc. +- vm.find(name) -> { name, xt, immediate } | null: lookup dictionary word. + +Stacks and memory helpers (JS-level, if you extend VM): +- vm.push(n), vm.pop(), vm.peek() +- vm.H (memory cells), vm.here (next free cell) + +--- + +## Words provided by the core + +Stack (data): +- DUP DROP SWAP OVER + +Return stack: +- >R R> R@ + +Memory: +- @ ! , HERE + +Arithmetic / logic: +- + - * 1+ 1- 0= + +Output: +- . EMIT CR .S + +Control flow: +- IF ELSE THEN +- BEGIN UNTIL +- BEGIN AGAIN + +Compiler metawords: +- : ; IMMEDIATE ' + - `' NAME` (tick) pushes NAME's execution token (xt) onto the stack. + +Comments: +- Backslash to EOL: `\ comment to end of line` +- Paren comments: `( comment until )` (single-line or across token stream, not nested) + +Numbers: +- Only base-10 integers are recognized by default (e.g., -42, 0, 123). + +--- + +## Structured Error Handling + +Fjorth does not throw for normal program faults. Instead, it: +1) Calls `options.onError(err)` if provided. +2) Halts the scheduler and invokes the `run(onIdle)` callback with the same `err` object. + +Error shape (Error with extra fields): +```js +{ + message: "undefined word: FOO", + code: "undefined_word", // stack_underflow, rstack_underflow, bad_jump, bad_call, bad_opcode, colon_no_name, ... + phase: "interpret" | "compile" | "exec" | "schedule", + token: "FOO", // when available + line: 12, col: 5, // when available + ip: 123, // instruction pointer for exec faults + stackDepth: 0, + rstackDepth: 0 +} +``` + +Example: +```js +const vm = new Fjorth({ onError: e => console.log(e.code, e.message) }); +vm.reset(); +vm.load("NOPE"); +vm.run(err => { /* err === same Error object */ }); +``` + +--- + +## Preemption and Scheduling + +- Time-sliced loop with configurable `stepBudget` and `sliceMs`. +- Call `vm.pause()` to stop at a safe boundary. Call `vm.resume()` or `vm.run()` to continue. +- Errors halt the scheduler and call your callbacks. + +--- + +## Debugging and Introspection + +Enable tracing and breakpoints when needed. This layer is opt-in and incurs minimal overhead when disabled. + +Enable: +```js +vm.debug.enable({ step:true, word:true, stack:true }); +vm.debug.onTrace = e => console.log(e); +``` + +Common operations: +- Break by word+token index: + ```js + vm.debug.breakAtWordToken("MYWORD", 2); // pause before 3rd token + ``` +- Break by ip: + ```js + vm.debug.break({ ip: 42 }); + ``` +- Conditional break: + ```js + vm.debug.break({ cond: e => e.op === '!' && e.word === 'STOREIT' }); + ``` +- Step and continue: + ```js + vm.debug.stepInto(); // single-instruction step + vm.debug.continue(); // resume + ``` +- View current state for IDE: + ```js + const v = vm.debug.view(); // { D, R, ip, word, tokenIndex, line, col } + ``` +- Call stack: + ```js + const frames = vm.debug.getCallStack(); // [{name, xt, ret, baseSp}, ...] + ``` +- Disassembly: + ```js + console.log(vm.debug.disassemble("MYWORD")); + ``` +- Memory map: + ```js + console.table(vm.debug.memoryMap()); + ``` + +Trace event example (when enabled): +```js +{ + type: 'step' | 'wordEnter' | 'wordExit' | 'break', + when: 'before' | 'after', // for step + ip: 123, + op: 'LIT' | 'CALL' | ..., + word: 'CURRENTWORD', + tokenIndex: 5, + line: 10, col: 3, + D: [...], R: [...] +} +``` + +Notes: +- Source maps: each emitted cell records the token index that produced it; symbolication finds line/col. +- Frames: calls to colon words push a readable frame; returning pops it. + +--- + +## Extending with JS + +Define a JS-backed word: + +```js +vm.defineJS("TWICE", function () { + const a = this.pop(); + if (this.lastError) return; // guard after pop + this.push(a * 2); +}); + +vm.reset(); +vm.load("21 TWICE ."); +vm.run(); +``` + +Immediate word example: + +```js +vm.defineJS("IWORD", function() { + // runs during compile if used while STATE=1 + this.push(9); +}, true); +``` + +--- + +## Testing with QUnit + +The project includes a browser QUnit suite. You can run tests by loading: +- fjorth.js +- qunit.js and qunit.css +- the test harness script + +If you cannot or do not want to rely on a network CDN, keep local copies of QUnit assets and reference them directly. + +--- + +## Design Notes + +Why numeric bytecodes? +- Compact memory representation, easy disassembly, consistent stepping. +- Great for building source maps and teaching the fetch-decode-execute cycle. + +Error model: +- Fail fast, report richly, avoid throwing across the run loop. +- Both onError callback and the run(onIdle) callback receive the same Error object. + +Debug-first: +- All features (breaks, stepping, disasm, call stack) are additive and do not alter core semantics. +- Tracing and symbolication are opt-in. + +Future work: +- Optional function-threaded tier or superinstruction compiler that fuses straight-line code into a single JS function while keeping source maps for stepping and breakpoints. +- Watchpoints and time-travel snapshots (ring buffer) for reverse stepping. +- A REPL UI with SEE/WORDS and live stack graphs. + +--- + +## Minimal Cheat Sheet + +: + Define with colon and semicolon: + : NAME ... ; + +IMMEDIATE: + Mark last defined word immediate. + +Tick: + ' NAME (push NAME execution token) + +Memory: + @ ! , HERE + +Stack ops: + DUP DROP SWAP OVER + +Return stack: + >R R> R@ + +Arithmetic / flags: + + - * 1+ 1- 0= + +Output: + . EMIT CR .S + +Control flow: + IF ... ELSE ... THEN + BEGIN ... UNTIL + BEGIN ... AGAIN + +Comments: + \ comment to end of line + ( comment until ) (not nested) + +Numbers: + Base-10 integers only by default. + +--- + +## Embedding Tips + +- Provide `print` and/or `println` so users see output. +- Use `onError` to surface structured errors in your IDE (decorate gutter, highlight token, etc.). +- Enable `vm.debug` only when you need it (breakpoints, tracing). +- For long-running student code, keep `stepBudget` moderate (e.g., 1000 - 5000) to preserve UI responsiveness. + +--- + +## Contributing + +- Keep the interpreter tiny and readable. +- Add tests for every public API and new word. +- Do not change existing tests unless they are clearly wrong. +- Prefer structured errors over thrown exceptions. + +--- + +## License + +MIT (or your preferred permissive license). Replace this line if you adopt something else. + +--- + +## Acknowledgments + +Inspired by classic Forth systems and educational interpreters. Fjorth focuses on clarity, debuggability, and embeddability for teaching environments.