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.
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.
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)
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).
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 */ });
stepBudget
and sliceMs
.vm.pause()
to stop at a safe boundary. Call vm.resume()
or vm.run()
to continue.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.
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);
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.
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.
: 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.
print
and/or println
so users see output.onError
to surface structured errors in your IDE (decorate gutter, highlight token, etc.).vm.debug
only when you need it (breakpoints, tracing).stepBudget
moderate (e.g., 1000 - 5000) to preserve UI responsiveness.