forthling
Run Forth-2012 in a sandbox: definitions stay between calls, each result shows the stacks.
Que pouvez-vous faire avec forthling ?
- A REPL and file execution (nested with
INCLUDE,INCLUDEDandREQUIRE) - File access:
OPEN-FILE CREATE-FILE READ-LINE READ-FILE WRITE-LINE WRITE-FILE, - Colon definitions compiled to threaded code, with
RECURSE,IMMEDIATE, IF ELSE THEN,BEGIN UNTIL/AGAIN/WHILE REPEAT,DO ?DO LOOP +LOOP LEAVE,- Locals:
{: a b | c -- out :},LOCALS|and(LOCAL), withTOfor assigning VARIABLE,CONSTANT,VALUE,DEFER,BUFFER:,MARKER,:NONAME,CREATE … DOES>- Byte-addressed memory with 64-bit cells, and a heap:
ALLOCATE FREE RESIZE - Double-cell numbers:
123.is a double, withD+ D- D. D.R D< DNEGATE DABS M+ M*/,
Installation
Ajoutez ceci à la configuration MCP de votre client (Claude Desktop : claude_desktop_config.json ; Cursor : .cursor/mcp.json ; VS Code et les autres utilisent la même forme « mcpServers »).
forthling-mcphttps://github.com/DeSchaker/forthling/releases/download/v0.2.3/forthling.mcpbDocumentation
Tirée du README du projet sur GitHub, actualisée chaque semaine. Les liens ouvrent GitHub.
forthling
forthling (noun)
- A diminutive creature thriving on a two-stack diet.
- A lean, extensible Forth system built from the ground up.
Forth is a tiny language with a big idea: you build your program out of small words, and you can try each one the moment you write it. forthling brings that to today's world: a complete Forth-2012 system in Rust that you can run on the command line, build into your own program, hand to an AI agent, or try right now in your browser, with nothing to install.
- The whole standard. All word sets of Forth-2012, from CORE to FLOATING, pass the Forth-2012 test suite, so standard code from books and tutorials runs as written. See Standard compliance.
- Safe to play with. A sandbox puts limits on steps, time and output, and keeps the
disk out of reach: files live in memory there, so a program can still write, read and
INCLUDEthem. A runaway loop stops with an error instead of hanging, and gives the same result on every machine. See Sandbox. - Small and clean. The core has no dependencies and no
unsafe, and its memory is bounded. - Talks to the world. JSON words in the core; SQLite as an extension.
- Made for AI agents. An MCP server gives an agent a Forth
session that remembers its definitions and shows the stacks after every call. When
something goes wrong, it also says where, which words were running and what the stacks
held, so an agent can often fix its code in one round.
llms.txttells a language model how to use it. - Learn as you go.
ALSO TUTORIALgives 20 short lessons, ten for beginners and ten beyond, andCHECKtests your answer. See Tutorial. - Runs anywhere. As a program, embedded in Rust or Python, or in the browser through WebAssembly.
The extensions (Python, SQLite, MCP, WebAssembly) live in crates of their own. MIT licence.
Install the program from crates.io with Rust 1.85 or
newer, and run forthling:
cargo install forthling
From a clone of the repository, cargo run --release does the same:
$ cargo run --release
forthling — WORDS shows the dictionary, BYE quits.
: SQUARE DUP * ;
ok
7 SQUARE .
49 ok
Run a file: cargo run --release -- examples/demo.fs
What to install
forthling comes in parts, called crates in Rust: the core (forthling) and extensions
such as forthling-sqlite and forthling-mcp. Which ones you need depends on how you use
it.
- To use forthling, install one program; its extensions are built in.
- In the browser: nothing to install, open the playground.
cargo install forthling: the program, the core alone, without SQLite.cargo install forthling-sqlite: the same program with the SQLite words.cargo install forthling-mcp, or the bundleforthling.mcpb(see For AI agents): the MCP server, with the core and SQLite.
- To embed forthling in a Rust program, name the core and the extensions you want in
your
Cargo.toml. An extension brings the core along, and Cargo builds it once, shared by all extensions, so they can work in the same system. An extension you do not name, such as SQLite with its C library, stays out of your program.[dependencies] forthling = "0.1" forthling-sqlite = "0.1"let mut forth = forthling::Forth::new(); forth.install(&forthling_sqlite::Sqlite);
Options
forthling [options] [file ...]
-m, --memory SIZE memory for data space and heap, such as 4M (default 1M)
-b, --blocks FILE the block file (default blocks.fb in the current directory)
--sandbox files only in memory (the files given here and the block
file are copied in; what Forth writes is gone at the end),
and the limits below: 10000000 steps, 5s and 1M of output
per line of the REPL or per file, and 4M of space for
definitions and the like in all
--max-steps N at most N steps (instructions and words) per evaluation
--max-time T at most T per evaluation, such as 5s or 500ms
--max-output SIZE at most SIZE bytes of output per evaluation
--max-space SIZE at most SIZE bytes outside Forth memory for definitions,
code, word lists and the like
-h, --help show this help
A limit of none lifts it, also one of --sandbox. See Sandbox.
SIZE is a number of bytes with an optional K, M or G (binary units), from 64K up
to 4G. A relative block file name is relative to the current directory.
To change the defaults, put lines like these in the config file
forthling/forthling.conf in your config directory: %APPDATA% on Windows, otherwise
$XDG_CONFIG_HOME or ~/.config (--help shows the exact path):
# forthling settings
memory = 16M
blocks = C:/Users/me/forth/blocks.fb
Lines starting with # are comments. The command line wins over the config file.
A program that embeds forthling picks the size with Forth::with_memory(size) and the
block file with Forth::set_block_file(path).
Features
- A REPL and file execution (nested with
INCLUDE,INCLUDEDandREQUIRE) - File access:
OPEN-FILE CREATE-FILE READ-LINE READ-FILE WRITE-LINE WRITE-FILE,FILE-POSITION REPOSITION-FILE FILE-SIZE RESIZE-FILE,DELETE-FILE RENAME-FILEand more - Colon definitions compiled to threaded code, with
RECURSE,IMMEDIATE,POSTPONE,LITERAL,[ ],' EXECUTEandSEE IF ELSE THEN,BEGIN UNTIL/AGAIN/WHILE REPEAT,DO ?DO LOOP +LOOP LEAVE,CASE OF ENDOF ENDCASE- Locals:
{: a b | c -- out :},LOCALS|and(LOCAL), withTOfor assigning VARIABLE,CONSTANT,VALUE,DEFER,BUFFER:,MARKER,:NONAME,CREATE … DOES>- Byte-addressed memory with 64-bit cells, and a heap:
ALLOCATE FREE RESIZE - Double-cell numbers:
123.is a double, withD+ D- D. D.R D< DNEGATE DABS M+ M*/,2VALUE,2LITERALand more - Structures (
BEGIN-STRUCTURE FIELD: CFIELD: +FIELD END-STRUCTURE),MS,TIME&DATE,KEY?, andAT-XY/PAGEfor the screen SOURCE,>IN,STATE,WORD,PARSE,REFILL,SAVE-INPUT,FINDandEVALUATE, working on input buffers in Forth memory- JSON: read it in place (
JSON@with a path such asusers[0].name) and write it to the output or to a string (see JSON) - A sandbox for code you do not trust: limits on steps, time and output, and no file access (see Sandbox)
- Errors as standard THROW codes, e.g.
demo.fs:3: error -13: undefined word: FOO, whichCATCHcan catch - Strings:
COMPARE SEARCH /STRING -TRAILING BLANK CMOVE CMOVE> SLITERAL, and text substitution withREPLACES SUBSTITUTE UNESCAPE - Blocks:
BLOCK BUFFER UPDATE FLUSH SAVE-BUFFERS EMPTY-BUFFERS LOAD THRU LIST, kept in a block file - Floating point:
1.5E3is a float, on a stack of its own, withF+ F* F/ F. FS. FE.,FSQRT FSIN FEXP FLN F** FATAN2and more,FVARIABLE FCONSTANT FVALUE,>FLOATandREPRESENT - Extended characters in UTF-8:
XC@+ XC!+ XCHAR+ XEMIT XKEY X-SIZE X-WIDTH XHOLDand more;CHAR,[CHAR],'€'andPARSEtake whole characters - Word lists and the search order:
WORDLIST GET-ORDER SET-ORDER ALSO ONLY PREVIOUS DEFINITIONS ORDER SEARCH-WORDLIST, andTRAVERSE-WORDLIST NAME>STRING - Programming tools:
.S ? DUMP SEE WORDS,[IF] [ELSE] [THEN],[DEFINED] [UNDEFINED],AHEAD CS-PICK CS-ROLL,N>R NR>andSYNONYM
Part of the dictionary is written in Forth itself (src/prelude.fs).
The interpreter is a library crate (src/lib.rs, with one module per part: stacks, memory,
parsing, the inner and outer interpreter, and the primitives in src/words/); src/main.rs
only holds the REPL. Another Rust program can embed it (cargo add forthling):
let mut forth = forthling::Forth::new();
forth.interpret(": SQUARE DUP * ; 7 SQUARE .")?;
assert_eq!(forth.output(), b"49 ");
Such a program can also add words of its own, written in Rust. A word is a closure that
works on the stacks (push, pop, fpush, fpop, pop_bytes, push_bytes); it can
keep state in Forth::state. An Extension bundles words in a word list of its own and
makes its name known to ENVIRONMENT?, so standard programs never meet them by accident
(see Extensions):
let mut forth = forthling::Forth::new();
let wid = forth.wordlist("MATHS"); // also defines MATHS, for ALSO MATHS
forth.add_word(wid, "CUBE", |f| {
let n = f.pop()?;
f.push(n * n * n);
Ok(())
})?;
forth.interpret("ALSO MATHS 3 CUBE .")?;
assert_eq!(forth.output(), b"27 ");
The macro forth_word! writes the pop and push for you: the word is a Rust function
with typed arguments, taken in stack order (the last is the top), and a typed result:
use forthling::{forth_word, Forth, Res, Throw};
let mut forth = Forth::new();
let wid = forth.wordlist("MATHS");
forth_word!(forth, wid, "CUBE", (n: i64) -> i64 { n * n * n })?;
forth_word!(forth, wid, "HYPOT", (a: f64, b: f64) -> f64 { a.hypot(b) })?;
forth_word!(forth, wid, "SHOUT", (s: String) -> String { s.to_uppercase() })?;
forth_word!(forth, wid, "DIVMOD", (a: i64, b: i64) -> Res<(i64, i64)> {
if b == 0 {
return Err(Throw::new(-10));
}
Ok((a % b, a / b))
})?;
forth.interpret("ALSO MATHS 3 CUBE . 3E0 4E0 HYPOT F. S\" hi\" SHOUT TYPE 7 2 DIVMOD . .")?;
assert_eq!(forth.output(), b"27 5. HI3 1 ");
i64 and bool (a flag) come from the data stack, f64 from the float stack, String
and Vec<u8> from a string ( c-addr u ). A result of those types (or &str) goes back
the same way, a string in ALLOCATEd memory that the program frees; a tuple gives
several, Res<T> lets the word throw, and without -> T the word gives nothing back.
A word that gets too few arguments takes none and throws -4 (or -45 for floats). For
types of your own, implement FromForth and IntoForth.
JSON
The word list JSON (ALSO JSON) is built in; the forthling program and the Python
module install it, a Rust program with forth.install(&forthling::Json). A JSON value is
its text, a string ( c-addr u ): the reading words give back pieces of it, so nothing is
copied. A missing key or a value of the wrong kind gives the flag 0.
ALSO JSON
: DOC S\" {\"users\": [{\"name\": \"Ada\", \"born\": 1815}]}" ;
DOC S" users[0].name" JSON@ DROP JSON>STRING DROP TYPE \ Ada (ALLOCATEd: FREE it)
DOC S" users[0].born" JSON@ DROP JSON>NUMBER DROP . \ 1815
JSON{ S" sum" JSON-KEY 6 JSON-NUMBER S" ok" JSON-KEY -1 JSON-FLAG JSON}
\ {"sum":6,"ok":true}
| Reading | Stack |
|---|---|
JSON-VALID? | ( json -- flag ) |
JSON-TYPE | ( json -- n ): 0 null, 1 boolean, 2 number, 3 string, 4 array, 5 object, -1 no JSON |
JSON-GET | ( json c-addr u -- json' flag ): the value of a key |
JSON-NTH | ( json n -- json' flag ): element n (from 0) of an array or object |
JSON-NAME | ( json n -- c-addr u flag ): key n of an object, ALLOCATEd |
JSON-LENGTH | ( json -- n ): elements of an array or object, else -1 |
JSON@ | ( json c-addr u -- json' flag ): a path, such as a.b[2] or ["a.b"] |
JSON>NUMBER | ( json -- n flag ): an integer that fits in a cell |
JSON>FLOAT | ( json -- flag ) ( F: -- r ) |
JSON>FLAG | ( json -- flag ok ) |
JSON>STRING | ( json -- c-addr u flag ): without its escapes, ALLOCATEd |
Writing goes to the output, or between <JSON and JSON> (which gives ( c-addr u ),
ALLOCATEd) to a string; the commas come by themselves. JSON{ JSON} JSON[ JSON],
JSON-KEY ( c-addr u -- ), JSON-STRING ( c-addr u -- ), JSON-NUMBER ( n -- ),
JSON-FLOAT ( F: r -- ), JSON-FLAG ( flag -- ), JSON-NULL and JSON-RAW ( json -- ).
A structure that does not fit, such as a value in an object without its key, throws -22.
Sandbox
Code that is not trusted, from a user or an AI agent, runs in a sandbox:
let mut forth = forthling::Forth::sandboxed();
let error = forth.interpret(": FOREVER BEGIN AGAIN ; FOREVER").unwrap_err();
assert_eq!(error.to_string(), "error -258: limit exceeded: steps");
Forth::sandboxed() has files only in memory (see Files), no terminal input, and limits each evaluation (each call of
interpret or include) to 10 million steps, 5 seconds and 1 MiB of output. A step is an
instruction or a word, and words that handle much data at once (MOVE, SEARCH, the JSON
words and the like) count one step per 64 bytes, so the time limit holds for them too.
Forth memory has its fixed size; what a program makes outside it (definitions and their
code, word lists, REPLACES texts, JSON strings) is limited to 4 MiB in all, its
space. set_limits(Limits { … }) and permit(Permission::Files, …) set them one by
one. A limit that is reached throws -258, and every step after it throws again, so
CATCH cannot go on. permit(Permission::Files, false) takes files away altogether (the
FILE words give the ior -259, INCLUDE and the block words throw -259). The sandbox restricts the Forth program, not the host: include
called by the host still reads its file. Extensions follow the permissions too; SQLite
opens only :memory: unless the program may use the disk (Forth::disk_files()).
Files
The FILE words, INCLUDE and the like, the block file and the host's include go
through a file system that the host chooses with set_file_system: NativeFs, the disk
(the default), MemoryFs, files in memory, or a FileSystem of its own, such as files
in a database or the browser's storage. A MemoryFs and its clones share their files, so
the host can put files in and read back what the program wrote:
use forthling::{Forth, MemoryFs};
let files = MemoryFs::new();
files.insert("square.fs", ": SQUARE DUP * ;").unwrap();
let mut forth = Forth::sandboxed();
forth.set_file_system(Box::new(files.clone())).unwrap();
forth.interpret("INCLUDE square.fs S\" out.txt\" W/O CREATE-FILE THROW VALUE OUT").unwrap();
forth.interpret("7 SQUARE 0 <# #S #> OUT WRITE-FILE THROW OUT CLOSE-FILE THROW").unwrap();
assert_eq!(files.get("out.txt").unwrap(), b"49");
In a MemoryFs a name is a path in which / and \ both separate the parts, so
lib/../square.fs is square.fs; MemoryFs::with_limit(bytes) caps the files in all,
and a write past it fails with the word's ior. Forth::sandboxed() starts with an empty
one of 4 MiB (MemoryFs::SANDBOX_LIMIT), and so does each session of the MCP server and
the playground, which keeps its files in the browser. With --sandbox the program puts
copies of the files on its command line (and of the --blocks file) in one, and Python's
sandbox=True has one too (write_file, read_file, delete_file, files); what the
Forth code writes there never reaches the disk.
Error reports
An error empties the stacks, but the Throw that interpret or include returns keeps
what the system looked like at that moment, so a person or an agent sees what went wrong
without running the code again:
let mut forth = forthling::Forth::new();
forth.interpret(": STEP 0 / ;\n: MAIN 1 2 STEP ;").unwrap();
let error = forth.interpret("5 MAIN").unwrap_err();
let c = error.context().unwrap();
assert_eq!(c.words, ["MAIN", "STEP", "/"]); // the words that were running
assert_eq!(c.stack, [5, 1]); // `/` has taken its two arguments
assert_eq!((c.line, c.column, c.token.as_str()), (1, 3, "MAIN"));
ErrorContext has the data and float stacks at the error, the words that were running
(from the outermost; the chain ends at a word of the system, such as /, which is
written in Forth itself), and where in the input: source (input, evaluate, the path
of a file or block n), line, column (in characters) and token, the last word read.
A word that takes its arguments before it fails has taken them; a stack underflow takes
nothing. steps_used() and max_depth() tell how many steps the last evaluation took and
how deep the data stack went, the words of the system it ran (such as ., written in
Forth) included.
An undefined word that is in a word list outside the search order gets a hint, which
Throw::hint() gives and the message ends with:
error -13: undefined word: JSON@ (it is in the word list JSON; ALSO JSON finds it). The
MCP server, the playground and the Python module pass all of this on.
Tracing
To see what code does word by word, switch on a trace. TRACE-ON and TRACE-OFF are in
the word list DEBUG (ALSO DEBUG), installed by the forthling program, the MCP server
and the playground. Each word that runs gets a line: indented by how deep it is nested,
then its name and the data stack when it starts, as .S shows it (at most the top 8
cells):
ALSO DEBUG
: SQ DUP * ;
: F 3 SQ 1+ ;
TRACE-ON F . TRACE-OFF
F <0>
SQ <1> 3
DUP <1> 3
* <2> 3 3
1+ <1> 9
. <1> 10
10
The insides of the system's own words (such as / and ., written in Forth) are not
traced, nor are the words that run while a definition is compiled, nor the system's words
that read the input, such as \, ( and [IF]. The lines are output,
so the output limit of the sandbox holds for them. A host records the same as data:
record_trace(Some(max)) and then take_trace(), a TraceEntry per word (word,
depth, stack, stack_depth); forth_eval of the MCP server does this with
trace: true. When no trace is on, it costs one test per word.
Debugging
BREAK, also in the word list DEBUG, stops the program where it stands. The forthling
program then shows the words that are running and the stack, and asks how to go on: s
(or an empty line) steps to the next word, n steps over the words this one calls, f
runs on until the word this one is in has finished, c continues to the next BREAK, and
q quits with -28 (user interrupt). A file debug.fs:
ALSO DEBUG
: SQ DUP * ;
: F 3 SQ 1+ ;
: MAIN BREAK F 2 SQ + ;
MAIN .
and forthling debug.fs in a terminal, with the answers typed after the prompts:
break: MAIN > BREAK <0>
s step, n over, f out, c continue, q quit> s
step: MAIN > F <0>
s step, n over, f out, c continue, q quit> s
step: MAIN > F > SQ <1> 3
s step, n over, f out, c continue, q quit> f
step: MAIN > SQ <2> 10 2
s step, n over, f out, c continue, q quit> c
14
A host decides what a pause does with set_debugger: a function that gets the system
and a Pause (why it stopped, the word, its depth, and the stacks, the running words and
the place in the input, as for an error) and returns a Resume (Step, Over, Out,
Continue) or an error to throw. The time spent in it does not count towards the time
limit. Without a debugger, BREAK does nothing.
The MCP server and the playground cannot wait in the middle of a run, as the program
waits for the terminal: CATCH, EVALUATE and INCLUDE run Forth inside Rust calls, so
a run can only stop while its host function runs. So they do it differently. In
forth_eval, BREAK does not stop: the result shows the stacks and the running words at
each BREAK. In the playground, Step through runs the code while it records each
word, then plays the run back: forwards and back, step, over, out and on to the next
BREAK, with the stack before and after each word. Since the sandbox makes a run come out
the same each time, the record is what happened.
Tutorial
New to Forth? The tutorial teaches it inside forthling, in 20 short lessons: ten for
beginners (the stack, arithmetic, defining words, decisions, loops, variables, strings)
and ten beyond (recursion, locals, tables, execution tokens, exceptions, floating point,
formatting numbers, JSON and finding a bug with a trace). Each lesson explains a little,
gives code to try and a task; CHECK tests your answer and says what is still wrong.
ALSO TUTORIAL
LESSONS \ the list, with the lessons you have done
4 LESSON \ lesson 4: Defining words
: CUBE DUP DUP * * ;
CHECK \ Well done! Lesson 4 is done. NEXT goes on to: Printing.
HINT NEXT \ a hint, and the next lesson
The program, the MCP server and the playground have it; in the playground the
Tutorial button opens the lessons beside the editor and keeps your progress in the
browser. Forth::new() leaves it out, so a standard program sees no extra words; a Rust
host installs it with forth.install(&forthling::Tutorial), and Forth::tutorial(word)
runs its words also without ALSO TUTORIAL, for buttons.
In the browser
web/ compiles forthling to WebAssembly (about 490 KB, no dependencies, no wasm-bindgen)
and has a playground page: Forth in the browser, on the visitor's own computer, in the
sandbox. Try it at **https://deschaker.github.io/forthling/**; see
web/README.md.
For AI agents (MCP)
mcp/ holds forthling-mcp, an MCP server: AI agents
run Forth through it, in a session that keeps their definitions and files, always in the
sandbox, with files only in memory. Install it with cargo install forthling-mcp, or
without Rust as the bundle forthling.mcpb from the
latest release, which Claude
Desktop opens; with Claude Code: claude mcp add forthling -- forthling-mcp. It is in the
MCP Registry as io.github.DeSchaker/forthling. Its
tools are forth_eval, forth_stack, forth_words and forth_reset; each result has
the output and the stacks as text and as JSON. See mcp/README.md, also
for what enforces the sandbox when a user asks the AI to get around it.
llms.txt (also at https://deschaker.github.io/forthling/llms.txt) sums up
for a language model how to write Forth for forthling: the idioms, locals rather than
ROT and PICK, the JSON words and the limits.
From Python
python/ holds a Python module, built with PyO3 and
maturin; see python/README.md.
import math, forthling
f = forthling.Forth()
f.add_word("HYPOT", math.hypot, inputs=2) # a word written in Python
print(f.interpret("3 4 HYPOT F.")) # 5.
SQLite
sqlite/ adds the word list SQLITE, in the style of the FILE words: handles as cells,
errors as an ior. Its program forthling-sqlite is forthling with these words; see
sqlite/README.md and the example.
$ cargo run --release --manifest-path sqlite/Cargo.toml
forthling — WORDS shows the dictionary, BYE quits.
Extensions: FORTHLING-SQLITE.
ALSO SQLITE S" :memory:" DB-OPEN THROW VALUE DB
ok
S" SELECT sqlite_version()" DB DB-PREPARE THROW VALUE V
ok
V DB-STEP THROW DROP 0 V DB-COLUMN TYPE
3.53.2 ok
Standard compliance
ROADMAP.md holds the roadmap to Forth-2012 and the
current status. The CORE, CORE EXT, BLOCK, DOUBLE, EXCEPTION, FACILITY, FILE, LOCALS,
MEMORY, SEARCH, STRING, TOOLS, XCHAR and FLOATING word sets are complete, and their tests in the
Forth-2012 test suite pass (core.fr,
coreplustest.fth, coreexttest.fth, blocktest.fth, doubletest.fth, exceptiontest.fth,
facilitytest.fth, filetest.fth, localstest.fth, memorytest.fth,
searchordertest.fth, stringtest.fth, toolstest.fth and all floating-point tests in
fp/, the paranoia test included). Of TOOLS EXT, everything is
present except ;CODE ASSEMBLER CODE EDITOR (there is no assembler or editor). The test
suite has no XCHAR tests, so forthling has its own, in the same style:
tests/xchartest.fth.
The system documentation that section 4.1 of the standard asks for (every
implementation-defined option, how forthling acts on each ambiguous condition, and the
other facts about the system) is in COMPLIANCE.md, with a Dutch
translation in VERKLARING.md.
Implementation-defined choices
The main ones; COMPLIANCE.md lists them all.
- Cells are 64 bits; an address unit and a character are 8 bits.
- Division is floored:
-7 2 /gives-4and-7 2 MODgives1./ MOD /MOD */ */MODare defined viaFM/MOD;SM/REMis available for symmetric division. - A number is a double cell only when it ends with a
.(123.,-5.,$FF.); a.elsewhere does not make a number.M*/multiplies through a 192-bit intermediate, so only its quotient can be out of range (-11); its result is floored too. - A double cell on the stack has its high cell on top. Division by zero throws
-10, a quotient that does not fit throws-11. - The pictured numeric output buffer (
<# … #>) holds 256 characters; overflowing it throws-17..andU.use the same buffer. ACCEPTandKEYread from standard input, which the terminal buffers per line:KEYreturns once a line is entered, and the line ending is a character too.ACCEPTkeeps at most the requested number of characters and drops the rest of the line. At the end of inputACCEPTreturns 0 andKEYthrows-39.KEY?never waits: it is true once a line has been entered and not yet read.EKEYreturns characters only, likeXKEY; the special keys (K-UP,K-F1, …) are numbered from$110000, above Unicode, and never occur.- Floats are IEEE 754 doubles (8 bytes, like a DFLOAT; an SFLOAT is 4 bytes), on a
separate stack of 1024 floats; over- and underflowing it throws
-44and-45. Arithmetic follows IEEE: dividing by zero gives an infinity or a NaN rather than an exception.F>DandF>Sthrow-11when the result does not fit. The text interpreter knows float literals only whileBASEis decimal; they need digits before the point and anE(1E,-1.5e-3).PRECISIONstarts at 15 andSET-PRECISIONkeeps it between 1 and 17.F.,FE.andFS.write1000.,333.33E-3and3.3333E0, with trailing zeros after the point dropped byF.only; infinities and NaNs come out asinf,-infandnan.CATCHrestores the depth of the float stack too. - Extended characters (xchars) are UTF-8, up to
$10FFFFin at most 4 bytes. A character stays one byte (MAX-CHARis 255), soKEY,EMITandC@work on bytes andEKEY>CHARis true for ASCII only. Malformed UTF-8 is read a byte at a time: a byte that does not start a complete xchar is one on its own, with the byte as its value. Writing an xchar above$10FFFFwrites U+FFFD.XC-WIDTHknows the common Unicode ranges (0 for control characters and combining marks, 2 for wide East Asian characters and emoji), not the full tables. TIME&DATEgives the time in UTC.AT-XYandPAGEwrite ANSI escape sequences, which current terminals, Windows Terminal included, understand.- Memory is 1 MiB unless set otherwise (see Options). The input buffers, the
S"andWORDbuffers, the pictured numeric output buffer andPADsit at fixed addresses at the bottom, followed by data space. The heap ofALLOCATEgrows down from the top towardsHERE, so data space and heap share the free space:UNUSEDis what lies between them, and allocating lowers it. Allocated blocks are cell-aligned and a whole number of cells. A failingALLOCATEreturns-59,FREEof an address thatALLOCATEdid not return gives-60(and changes nothing), and a failingRESIZEgives-61and leaves the block as it was. - Blocks are kept in the file
blocks.fbin the current directory, unless--blocksor the config file names another (see Options). The file is created the first time a block is read or written. Block numbers run from 1 to 1048576 (so the file stays below 1 GiB); any other number throws-35. Block u is at byte offset (u - 1) × 1024. A block beyond the end of the file reads as spaces, and writing one fills the gap with spaces. There are 8 block buffers in Forth memory; when all are in use, the least recently used one is written back (if updated) and reused. Read and write errors throw-33and-34. LOADcopies the block into an input buffer of its own, soSOURCEdoes not return the block buffer itself and changing the buffer duringLOADhas no effect on the text. In a block,\skips to the end of the current line of 64 characters,REFILLmoves on to the next block, andSAVE-INPUT/RESTORE-INPUTwork across blocks.SOURCE-IDis 0 while a block is loaded.LISTprintsScreen uand then the 16 lines of 64 characters, numbered 0 to 15, without trailing spaces.- A word list id (wid) is a small number;
FORTH-WORDLISTis 1 andWORDLISThands out 2, 3, …, without limit. The search order holds up to 16 word lists (WORDLISTS); more throws-49, andPREVIOUSorALSOon an empty order throws-50. An invalid wid throws-24.ONLYand-1 SET-ORDERleave justFORTH-WORDLIST, which holds all words of the system,SET-ORDERincluded.WORDSlists the first word list of the search order, andORDERshows unnamed word lists as#2,#3, ….MARKERalso restores the search order, the compilation word list and the word lists made since. FORGET namelooks for the name in the compilation word list only, deletes that word and every word defined after it, and putsHEREback to where it was before the word. It does not undo changes to the search order or word lists;MARKERdoes. Words of the system itself cannot be forgotten (-15).- A name token is the same number as the execution token.
NAME>STRINGcopies the name to the heap the first time, in upper case, since names are not case-sensitive.NAME>INTERPRETnever returns 0: every word can be executed. - Locals live on a stack of their own, not on the return stack, so
>RandR>can be mixed freely with them. A definition can have up to 256 locals (#LOCALS); more throws-21. It may declare them in more than one group. The locals' names are found before all other words, but only while compiling, and they are forgotten at;and atDOES>; theDOES>part can declare locals of its own. A{:declaration may continue over several lines. With{:the last argument gets the top of the stack, withLOCALS|and(LOCAL)the first one. A|or--out of place, or a missing:}, throws-22. - The data stack and the return stack hold 65536 cells each; overflowing them throws
-3and-5.ENVIRONMENT?reports these sizes and the other standard queries. ABORTthrows-1andABORT"throws-2; both empty the stacks and return to the terminal with a message.QUITkeeps the data stack and prints nothing. When running files,QUITends the run, since there is no terminal to return to.SOURCE-IDis 0 for the terminal, -1 forEVALUATEand the file id for an included file.RESTORE-INPUTcan return to any earlier line of a file, but at the terminal and inEVALUATEonly within the current line; otherwise it returns true.S\"translates\nto a single line feed (10). Running a word made byDEFERbefore an action is set withISthrows-21.- Every error is a THROW code that
CATCHcan catch, including errors the system itself detects (such as-13for an undefined word).QUITis not an exception and is not caught.CATCHcan be nested 1000 deep; one more throws-5. When aTHROWunwinds a definition that was started inside theCATCH, that definition is dropped. [IF]and[ELSE]skip words across lines, reading further lines from the file or the terminal; if the input ends before the matching[ELSE]or[THEN], skipping just stops.DUMPprints the address and 16 bytes per line in hexadecimal, plus their characters.COMPAREandSEARCHcompare characters exactly, so case matters. Substitution names (REPLACES,SUBSTITUTE) do not depend on case. A%name%that is not a substitution name is kept as it is, delimiters included.SUBSTITUTEreturns-78when the result does not fit, andREPLACESthrows-79for a name that contains%.- File ids are small positive numbers.
R/Ois 0,W/O1 andR/W2;BINadds 4 and changes nothing. A failing file word returns its own THROW code as ior (-62forCLOSE-FILEup to-76forWRITE-LINE).WRITE-LINEends a line with a line feed;READ-LINEaccepts both LF and CR LF.FILE-STATUSreturns 0 as its status. INCLUDEDandREQUIREDlook for a relative file name that does not exist in the current directory next to the file that is including it.REQUIREDrecognises a file by its full path. Inside a file, a(comment may continue over several lines.
Contributing
Bug reports, ideas and pull requests are welcome; see CONTRIBUTING.md and the code of conduct. Security problems go through SECURITY.md.
License
Testing
cargo test
The tests from the Forth-2012 test suite need the suite to be cloned first:
git clone https://github.com/gerryjackson/forth2012-test-suite.git tests/forth2012-test-suite
git -C tests/forth2012-test-suite checkout 4c87e56
forthling needs Rust 1.85 or newer. CI (.github/workflows/ci.yml) checks formatting and
clippy lints and runs all tests, the test suite included, on Rust 1.85 and on the latest
stable Rust on Linux and Windows. It also builds the Python module and runs its tests
(python -m unittest discover -s python/tests, after pip install ./python) and checks
and tests the SQLite words (cargo test --manifest-path sqlite/Cargo.toml) and the MCP
server (cargo test --manifest-path mcp/Cargo.toml, also with --no-default-features),
and builds the WebAssembly module and tests it with Node (node --test
web/tests/forthling.test.mjs).
Questions fréquentes
Qu'est-ce que forthling ?
Run Forth-2012 in a sandbox: definitions stay between calls, each result shows the stacks.
Comment installer forthling ?
Suivez les étapes d'installation de sa documentation, affichée sur cette page et dans son dépôt.
Faut-il une clé API pour forthling ?
Son entrée au registre n'indique aucune clé ni secret obligatoire. Certaines fonctions peuvent tout de même demander un compte auprès du service auquel il se connecte.
forthling est-il open source ?
Oui. Son code est sur GitHub sous licence MIT.
Qui maintient forthling ?
Il est publié par DeSchaker. AI.tn le référence sans le maintenir ni le recommander.
Issu du registre MCP officiel. Être listé n'est pas une recommandation : vérifiez le code et les permissions d'un serveur avant de lui donner accès à vos données.