git » fjorth.git » master » tree

[master] / README.md

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.

Features

Quick Start

Include Fjorth in a page:

<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)

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):

{
  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:

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

Debugging and Introspection

Enable tracing and breakpoints when needed. This layer is opt-in and incurs minimal overhead when disabled.

Enable:

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):

{
  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:

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:

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