ccforth is a Forth to C compiler written in Go. It interprets compile-time Forth (immediate words, meta-programming) and emits flattened C11 code that is compiled with gcc or clang to produce standalone executables.
ccforth was written to explore the idea of having an interpreter and a compiler running simultaneously to enable meta-programming heavy Forth programs to be compiled into C. This is harder than it sounds because Forth cannot be ahead-of-time compiled by parsing alone; programs routinely execute code at compile time. The project was also an experiment in how far an AI agent could progress a compiler project - most of the initial implementation was done by Claude Opus 4.6. Go was used as the implementation language as it is low enough level to express Forth properly but easier to write than C. C was used as a compiler backend as C compilers optimize well and are available everywhere.
- Fast compiled output — generates optimised C11, compiled with
gcc/clang at
-O3. Typically 5–373x faster than Gforth on benchmarks. - Single binary, zero dependencies — download one executable and go. No runtime library, no install step, no Forth image files.
- Self-contained executables — compiled programs are standalone native binaries with no runtime dependencies (the C runtime is embedded).
- Interactive REPL —
ccforth -igives you a full interpreter with tab completion and line editing for prototyping and debugging. - Forth 2012 standard — passes the official Forth 2012 test suite with 0 errors across all implemented word sets.
- Gforth compatible — implements
{ }locals,SLURP-FILE,S+,SYSTEM,ARGC/ARG, and other Gforth extensions so existing code ports easily. - Inline C escape hatch —
CODE/;CODElets you drop into raw C for performance-critical words or direct library calls. - Cross-platform — runs on Linux, macOS, and Windows.
- 32-bit and 64-bit — 64-bit cells by default; the
-32flag generates 32-bit cell code for cross-compiling to ARM32, embedded targets, or 32-bit x86 (via-Cflags "-m32"). - Readable generated C —
ccforth -cemits human-readable C with#linedirectives mapping back to your.fthsource, so you can debug with gdb. - Floating-point — full Forth 2012 FLOATING and FLOATING EXT word sets including transcendentals.
- Exception handling —
CATCH/THROWand theTRY/ENDTRYextension, compiled viasetjmp/longjmp. - Locals —
{: ... :}and Gforth{ ... }syntax with typed specifiers, compiled to C local variables.
.fthsource is read by the Scanner (lexer), which tokenises it.- The Interpreter executes immediate words and metaprogramming, building a dictionary of word bodies.
- The C emitter walks the finished dictionary and generates flattened C from the word bodies.
- The result is a self-contained
output.cwith the runtime inlined. - gcc / clang compiles that C11 source into a standalone executable.
Forth source is processed by the interpreter, which executes
immediate words (like IF, DO, :, ;) and allows the normal
metaprogramming tricks Forth programs do. These build up an
intermediate representation — a list of body items containing word
references, literals, and branch instructions — stored in the
dictionary. The C emitter then walks each colon definition's
body and produces inlined C code using goto-based control flow. A
small C runtime is embedded into the ccforth binary and inlined
at the top of every generated C file, providing the data stack, return
stack, floating-point stack, and file table. This means ccforth -c
output is fully self-contained — you can compile it with just cc -std=c11 -O3 -march=native -flto -fwrapv -fno-strict-aliasing -lm output.c (-fwrapv and -fno-strict-aliasing are required for
correctness).
A Forth compiler cannot just parse — it must execute. Forth
programs routinely run code at compile time: immediate words like IF
and DO build control-flow structures by manipulating the compiler
state, CREATE/DOES> defines new defining words, [CHAR] and [']
evaluate at compile time and embed the result as a literal, and
[IF]/[THEN] conditionally includes or excludes source code.
Programs use these facilities to build lookup tables, define new
control structures (e.g. CASE/ENDCASE), compute constants, and
generate families of words from templates — all before the program
"runs". A static compiler that only translates syntax would miss all
of this. ccforth solves this by running a full Forth interpreter first:
every line of source is executed, immediate words fire, the dictionary
and memory fill up with the results, and only then does the C emitter
walk the finished dictionary to produce output. The interpreter is
the compiler's front end.
- 64-bit cells (
int64_t) by default — a cell must be big enough to hold a pointer and most platforms are 64-bit. A-32flag generates 32-bit cell code for embedded and cross-compilation targets. - Parameterised C output — words with statically known stack
effects are emitted as proper C functions with parameters and return
values (e.g.
cell_t word_FACT(cell_t n)), so the C compiler can register-allocate stack values. Words that cannot be analysed fall back tovoid(void)functions with global stack operations. Both forms arestatic inline, letting the C compiler optimise across word boundaries. goto-based control flow — direct translation of Forth branch instructions. No structuredif/whilereconstruction is performed.- Real-pointer memory model — a single
unsigned char[]array holds the Forth memory image (variables, strings, allotted data), initialised from a snapshot taken after interpretation. All addresses are real C pointers, so@and!work uniformly on both dictionary resident data andmalloc'd buffers fromALLOCATE. - Peephole optimiser — operates on the body before C emission. Folds constants, eliminates identity operations and redundant stack shuffles. See the Performance section for details.
#linedirectives — generated C includes#linedirectives that map back to the original.fthsource files with absolute paths. This means gdb can be used to debug crashes, though you'll want to pass-Cflags -g. Use-no-line-directivesto disable this (useful when debugging the compiler itself).- Bootstrap from Forth —
core.fthdefines many standard words in Forth itself, as is traditional, and providesCODEwords for I/O, file access, and diagnostics, keeping both the Go interpreter and the C runtime minimal. It is embedded in the binary for distribution.
For detailed per-word documentation including stack effects, see WORDS.md.
Stack: DUP DROP SWAP OVER ROT -ROT NIP TUCK
?DUP PICK DEPTH 2DUP 2DROP 2SWAP 2OVER
Arithmetic: + - * / MOD /MOD */ */MOD NEGATE
ABS MIN MAX 1+ 1- 2* 2/
Double-cell arithmetic: D+ D- DNEGATE DABS D2* D2/
D< D0< D0= D= D>S DMAX DMIN M+ M* M*/ S>D
UM* UM/MOD SM/REM FM/MOD
Bitwise: AND OR XOR INVERT LSHIFT RSHIFT
Comparison: = <> < > <= >= 0= 0< 0> 0<>
U< U> TRUE FALSE
Memory: @ ! C@ C! +! 2@ 2! CELL+ CELLS
CHAR+ CHARS ALIGN ALIGNED ALLOT FILL ERASE MOVE
CMOVE CMOVE> PAD UNUSED
I/O: . U. U.R D. D.R .R CR EMIT SPACE SPACES
TYPE ." .( KEY ACCEPT SOURCE >IN WORD PARSE
PARSE-NAME REFILL
Pictured numeric output: <# # #S #> HOLD SIGN BASE
DECIMAL
Return stack: >R R> R@ 2>R 2R> 2R@ N>R NR>
Control flow: IF ELSE THEN DO ?DO LOOP +LOOP I J
LEAVE UNLOOP BEGIN AGAIN UNTIL WHILE REPEAT RECURSE
EXIT CASE OF ENDOF ENDCASE AHEAD ?DUP-IF ?DUP-0=-IF
Strings: S" S\" C" COUNT CHAR [CHAR] COMPARE
SEARCH SLITERAL /STRING -TRAILING BLANK S+
Defining words: : ; :NONAME CONSTANT VARIABLE VALUE
FVALUE 2CONSTANT 2VARIABLE 2VALUE CREATE ALLOT , C,
DOES> IMMEDIATE DEFER DEFER! DEFER@ IS ACTION-OF
BUFFER: MARKER [: ;]
Compilation: STATE [ ] LITERAL 2LITERAL POSTPONE ]] [[ '
['] EXECUTE COMPILE, HERE FIND >BODY >NUMBER TO
SAVE-INPUT RESTORE-INPUT
Exception handling: CATCH THROW ABORT ABORT" TRY IFERROR
RESTORE ENDTRY
File access: OPEN-FILE CREATE-FILE CLOSE-FILE READ-FILE
WRITE-FILE DELETE-FILE FILE-POSITION FILE-SIZE
REPOSITION-FILE RESIZE-FILE READ-LINE WRITE-LINE SOURCE-ID
INCLUDE-FILE INCLUDED INCLUDE REQUIRED REQUIRE
RENAME-FILE FILE-STATUS FLUSH-FILE R/O W/O R/W BIN
SLURP-FID SLURP-FILE
Floating-point: F+ F- F* F/ FNEGATE FABS FMAX
FMIN FLOOR FROUND FSQRT F0< F0= F< FDUP FDROP
FSWAP FOVER FROT F@ F! D>F F>D FDEPTH >FLOAT
REPRESENT FLITERAL F. FLOAT+ FLOATS FALIGN FALIGNED
FVARIABLE FCONSTANT FSIN FCOS FTAN FASIN FACOS FATAN
FATAN2 FSINCOS FSINH FCOSH FTANH FASINH FACOSH FATANH
FEXP FEXPM1 FLN FLNP1 FLOG FALOG F** F>S S>F
FTRUNC F= F~ SF! SF@ DF! DF@ PRECISION SET-PRECISION
FS. FE. FVALUE
Facility: BEGIN-STRUCTURE END-STRUCTURE +FIELD FIELD:
CFIELD: AT-XY PAGE KEY? EMIT? MS TIME&DATE UTIME EKEY
EKEY? EKEY>CHAR EKEY>FKEY
Extended-Character (UTF-8): XC-SIZE X-SIZE XC@+ XC!+
XCHAR+ XCHAR- XEMIT XKEY XKEY? +X/STRING XC-WIDTH
X-WIDTH EKEY>XCHAR -TRAILING-GARBAGE XHOLD X\STRING-
Search order: FORTH-WORDLIST GET-ORDER SET-ORDER
GET-CURRENT SET-CURRENT WORDLIST SEARCH-WORDLIST DEFINITIONS
FORTH ALSO ONLY PREVIOUS ORDER VOCABULARY
Locals: (LOCAL) {: ... :} { ... } TO
Memory allocation: ALLOCATE FREE RESIZE
Programming tools: .S ? DUMP WORDS [DEFINED]
[UNDEFINED] [IF] [ELSE] [THEN] CODE ;CODE INTERPRETER
AHEAD CS-PICK CS-ROLL N>R NR> SYNONYM TRAVERSE-WORDLIST
NAME>STRING NAME>INTERPRET NAME>COMPILE
String extensions: SUBSTITUTE REPLACES UNESCAPE PLACE
System: ATEXIT BYE EVALUATE QUIT ENVIRONMENT? SYSTEM
ARGC ARG NEXT-ARG SHIFT-ARGS GETENV SETENV
C interface: C-FUNCTION C-VALUE C-VARIABLE \C
CODE and ;CODE let you define words with raw C code that is
emitted verbatim into the compiled output. This is useful for
performance-critical operations or calling C library functions
directly.
CODE words can declare their stack signature on the first line using
Gforth-compatible type codes. Parameters are named p0, p1, p2,
... (deepest first) and outputs use return:
CODE FAST-ADD n n -- n
return p0 + p1;
;CODE
CODE FILL a n n --
memset(p0, (int)p2, (size_t)p1);
;CODEType codes:
| Code | C type | Stack | Description |
|---|---|---|---|
n |
cell_t |
data | Integer cell |
a |
void * |
data | Address (auto-cast from cell_t) |
u |
ucell_t |
data | Unsigned cell |
r |
double |
float | Floating-point value |
Typed CODE words generate a parameterised __p function that the
optimiser calls directly — no stack spill/reload needed. A
void(void) wrapper is also generated for backward compatibility
(and tree-shaken if unused).
CODE words that return multiple values use pre-declared output
variables r0, r1, r2, ... The emitter declares them with the
correct C type from the signature and appends a return epilogue that
constructs the struct. Do not use return or PUSH() — just
assign the output variables:
CODE UM/MOD n n n -- n n
unsigned __int128 _n = ((unsigned __int128)(ucell_t)p1 << 64) | (ucell_t)p0;
r0 = (cell_t)(ucell_t)(_n % (ucell_t)p2);
r1 = (cell_t)(ucell_t)(_n / (ucell_t)p2);
;CODE
CODE FSINCOS r -- r r
r0 = sin(p0); r1 = cos(p0);
;CODE
CODE SDL-POLL-EVENT -- n n
SDL_Event _e;
if (SDL_PollEvent(&_e)) {
r0 = _e.button.button; r1 = _e.type;
} else { r0 = 0; r1 = 0; }
;CODEMulti-return typed CODE words generate __p functions with struct
returns, so parameterised callers can call them directly without
spill/reload.
Without a type signature, CODE words use manual POP()/PUSH()
macros as before:
CODE FAST-SQUARE
{ cell_t n = POP(); PUSH(n * n); }
;CODEYou can use the runtime macros (PUSH, POP, TOS, FPUSH,
FPOP, etc.). Addresses on the stack are real C pointers — use the
CADDR(a) macro (expands to (unsigned char*)(uintptr_t)(a)) to
cast them for byte access.
C-FUNCTION declares an external C function with typed parameters,
following Gforth's calling convention:
\C #include <math.h>
C-FUNCTION c-sqrt sqrt r -- r
: MAIN 2.0e0 c-sqrt F. CR ; \ prints 1.41421\C embeds a line of C code verbatim into the generated output
(hoisted to the top alongside #include lines).
C-FUNCTION creates a typed CODE word that calls the named C
function directly — no manual POP()/PUSH() needed.
C-VALUE exposes a C constant, macro, or expression as a Forth word:
\C #include <SDL2/SDL.h>
C-VALUE SDL_INIT_VIDEO SDL_INIT_VIDEO -- nC-VARIABLE exposes a C variable's address, like a Forth VARIABLE:
\C static int counter = 0;
C-VARIABLE my-counter counter
: MAIN my-counter @ 1+ my-counter ! ;Both follow Gforth's C interface conventions.
If the C code contains #include directives, they are hoisted to the
top of the generated file and de-duplicated:
CODE C-SIN r -- r
#include <math.h>
return sin(p0);
;CODEIf a Go primitive with the same name already exists, CODE adds the
C body to it — the word works in both modes (Go primitive in the
interpreter, inline C in the compiler). If a Forth colon definition
exists, CODE creates a new entry that uses the C body for compiled
mode and falls back to the Forth definition for interpretation. If
neither exists, the CODE word only runs in compiled mode; calling it
in the interpreter raises an error.
Use INTERPRETER with [IF]/[ELSE]/[THEN] to provide portable
definitions that work in both modes when there is no Go primitive:
INTERPRETER [IF]
: FAST-SQUARE DUP * ;
[ELSE]
CODE FAST-SQUARE n -- n
return p0 * p0;
;CODE
[THEN]Words that need both an interpreter implementation and an optimised C
implementation follow one of two patterns in core.fth:
- Shadowed definitions: The Forth colon definition is placed
first, then a
CODEblock insideINTERPRETER [IF] [ELSE] ... [THEN]shadows it. TheCODEword detects the existing Forth definition and preserves it as an interpreter fallback while adding the C implementation for compiled output. Used by double-cell arithmetic, memory alignment, and similar words. - Guarded definitions: Words that only make sense in one mode use
INTERPRETER [IF] ... [ELSE] ... [THEN]so each mode gets exactly one definition. Used by I/O, file-access, and floating-point I/O words where the interpreter has a Go primitive.
The generated C is designed to be readable. Use ccforth -c to emit
it (add -no-line-directives to omit #line markers for easier
reading).
Each Forth word becomes one or two static inline C functions. Words
with statically analysable stack effects get a parameterised
version (suffix __p) that takes stack inputs as parameters and
returns outputs via cell_t or a struct, plus a void(void) wrapper
for backward compatibility. Words that cannot be analysed get only the
void(void) form. The name is formed by prefixing word_ and
replacing special characters:
| Character | Replacement |
|---|---|
- |
_sub_ |
+ |
_add_ |
* |
_mul_ |
/ |
_div_ |
! |
_store_ |
@ |
_fetch_ |
< |
_lt_ |
> |
_gt_ |
= |
_eq_ |
? |
_q_ |
. |
_dot_ |
" |
_quote_ |
Alphanumerics pass through unchanged. Any other character becomes
_<codepoint>_. For example, FM/MOD → word_FM_div_MOD,
2>R → word_2_gt_R.
If a word name is unique in the dictionary, the function name is just
the mangled name (e.g. word_MAIN). If the same name has been
redefined (e.g. a Forth colon definition shadowed by a CODE word),
the execution token is appended as a suffix to disambiguate
(e.g. word_CR_348).
Local variables declared with {: ... :} or { ... } or (LOCAL)
are emitted as C locals using their Forth names, prefixed by _local_
(or _flocal_ for float locals):
: DISTANCE {: F: x F: y -- F: r :} x x F* y y F* F+ FSQRT ;generates:
static inline void word_DISTANCE(void) {
double _flocal_y;
double _flocal_x;
_flocal_y = FPOP();
_flocal_x = FPOP();
FPUSH(_flocal_x);
FPUSH(_flocal_x);
/* ... */
}Buffer locals (W^, D^, F^, C^) get a _buf suffix on the
backing array (e.g. _local_buf with _local_buf_buf[1] as storage).
Double-cell locals occupy two slots; only the first slot carries the
Forth name, the second uses a numeric fallback. If two locals would
mangle to the same C name, a numeric suffix is appended
(e.g. _local_x_1, _local_x_2).
For a complete per-word reference organised by standard word set, see WORDS.md.
ccforth targets the Forth 2012 standard. The table below summarises which word sets are implemented. "Both" means words work in the interpreter and compile to C; "interpreter-only" means they work in the interpreter but have no C emission.
| Word set | Status | Mode | Notes |
|---|---|---|---|
| CORE | Complete | Both | All 134 words |
| CORE EXT | Complete | Both | All standard words |
| BLOCK | Not implemented | ||
| DOUBLE | Complete | Both | All 20 words |
| DOUBLE EXT | Complete | Both | 2ROT 2VALUE DU< |
| EXCEPTION | Complete | Both | CATCH/THROW via setjmp/longjmp |
| EXCEPTION EXT | Partial | Both | TRY IFERROR RESTORE ENDTRY |
| FACILITY | Complete | Both | All 3 words (AT-XY KEY? PAGE) |
| FACILITY EXT | Partial | Both | Structure words, EKEY family, EMIT?, MS, TIME&DATE, UTIME; K-* constants not implemented |
| FILE | Complete | Both | All 21 words; CODE words in core.fth use FILE* table |
| FILE EXT | Partial | Both | FILE-STATUS FLUSH-FILE INCLUDE REFILL RENAME-FILE REQUIRED REQUIRE |
| FLOATING | Complete | Both | 34 words |
| FLOATING EXT | Complete | Both | Transcendentals, exp/log, F**, F~, SF!/SF@, DF!/DF@, PRECISION, SET-PRECISION, FS., FE., FVALUE |
| LOCALS | Complete | Both | (LOCAL) and {: ... :} syntax |
| LOCALS EXT | Partial | Both | Typed specifiers (W: D: F: C: W^ D^ F^ C^); no LOCALS| |
| MEMORY | Complete | Both | Real malloc/free/realloc in compiled code; heap allocator in interpreter |
| SEARCH | Complete | Interpreter-only | Dictionary restructured with wordlists |
| SEARCH EXT | Partial | Interpreter-only | ALSO ONLY PREVIOUS ORDER |
| STRING | Complete | Both | All 8 words |
| STRING EXT | Partial | Both | SUBSTITUTE REPLACES UNESCAPE |
| XCHAR | Complete | Both | UTF-8 encode/decode, XEMIT, XKEY |
| XCHAR EXT | Complete | Both | +X/STRING, X-WIDTH, XHOLD, X\STRING-, -TRAILING-GARBAGE |
| TOOLS | Partial | Both | .S ? DUMP WORDS |
| TOOLS EXT | Partial | Both | [DEFINED] [UNDEFINED] [IF] [ELSE] [THEN] CODE ;CODE INTERPRETER AHEAD CS-PICK CS-ROLL N>R NR> SYNONYM TRAVERSE-WORDLIST NAME>STRING NAME>INTERPRET NAME>COMPILE |
All 134 CORE words are implemented:
! # #> #S ' ( * */ */MOD + +! +LOOP ,
- . ." / /MOD 0< 0= 1+ 1- 2! 2* 2/ 2@
2DROP 2DUP 2OVER 2SWAP : ; < <# = > >BODY
>IN >NUMBER >R ?DUP @ ABORT ABORT" ABS ACCEPT
ALIGN ALIGNED ALLOT AND BASE BEGIN BL C! C, C@
CELL+ CELLS CHAR CHAR+ CHARS CONSTANT COUNT CR
CREATE DECIMAL DEPTH DO DOES> DROP DUP ELSE EMIT
ENVIRONMENT? EVALUATE EXECUTE EXIT FILL FIND FM/MOD
HERE HOLD I IF IMMEDIATE INVERT J KEY LEAVE
LITERAL LOOP LSHIFT M* MAX MIN MOD MOVE NEGATE
OR OVER POSTPONE QUIT R> R@ RECURSE REPEAT ROT
RSHIFT S" S>D SIGN SM/REM SOURCE SPACE SPACES
STATE SWAP THEN TYPE U. U< UM* UM/MOD UNLOOP
UNTIL VARIABLE WHILE WITHIN WORD XOR [ ['] [CHAR]
]
.( .R 0<> 0> <> ?DO :NONAME 2>R 2R> 2R@ AGAIN
BUFFER: BYE C" CASE COMPILE, DEFER DEFER! DEFER@
ENDCASE ENDOF ERASE FALSE HEX HOLDS IS ACTION-OF
MARKER NIP OF PAD PARSE PARSE-NAME PICK REFILL
RESTORE-INPUT ROLL S\" SAVE-INPUT TO TRUE TUCK U.R
U> UNUSED VALUE -ROT
Interpreter-only word sets. The following word sets work in the interpreter but cannot be compiled to C:
- Search order — dictionary manipulation is inherently an interpreter-time operation.
INCLUDE-FILE/INCLUDED— loading Forth source at runtime is an interpreter operation.
CODE / ;CODE are repurposed for inline C rather than assembly
language. When a CODE word is defined for a name that already has a Go
primitive, the primitive is augmented with the C body — the word works
in both modes. When a Forth colon definition exists, CODE shadows
it for compilation while preserving the Forth definition for
interpreter use. Otherwise, CODE words only execute in compiled mode.
Use INTERPRETER [IF] to provide a Forth fallback for interpreter
mode (see "Inline C with CODE" above).
INTERPRETER is a non-standard extension that pushes TRUE in
interpreter mode and FALSE in compiler mode, enabling conditional
definitions with [IF]/[ELSE]/[THEN].
Memory allocation uses malloc/free/realloc in compiled code,
so ALLOCATE can allocate beyond the Forth memory size and FREE
genuinely releases memory. In the interpreter, a heap allocator grows
downward from the top of memory — separate from the dictionary space
(HERE) — so ALLOCATE does not affect the data-space pointer.
Compile-time allocations (e.g., 1024 ALLOCATE THROW CONSTANT BUF)
are relocated to malloc'd pointers at program startup via an init
function in the generated C.
Floating-point literals require an E or e exponent marker per
the standard (e.g., 1E, 3.14E0, .5E-3). Plain decimal numbers
like 3.14 are not recognised as floats.
Locals support the {: ... :} syntax with initialised and
uninitialised locals. Multiple {: or { } blocks in a single word
extend the same local frame (Gforth-compatible). LOCALS| is not
implemented.
ENVIRONMENT? recognises a fixed set of queries and does not
report all implementation limits.
Cell size is 64 bits (int64_t) by default, which is not the most
common choice for Forth systems but is valid per the standard. The -32
flag generates 32-bit (int32_t) cells for cross-compilation targets.
QUIT clears the stacks and returns to the interpreter in
interpreter mode. In compiled mode it calls exit(0).
ccforth is not a Gforth clone, but it implements many Gforth extensions that make it straightforward to port Gforth programs. This section covers what works, what doesn't, and what to watch out for.
Locals. The { ... } brace syntax works identically to Gforth,
including typed specifiers (W: D: F: C: for stack-initialised
locals, W^ D^ F^ C^ for buffer locals) and the | separator
for uninitialised locals. Multiple { } blocks in a single word
extend the same local frame, matching Gforth behaviour. The standard
{: ... :} syntax is also supported.
Strings and files. SLURP-FID, SLURP-FILE, PLACE, and S+
work as in Gforth.
System interface. ARGC, ARG, NEXT-ARG, SHIFT-ARGS,
GETENV, SETENV, SYSTEM, and UTIME all behave as in Gforth.
Structures. Both Gforth-style (STRUCT / END-STRUCT / FIELD)
and Forth 2012-style (BEGIN-STRUCTURE / END-STRUCTURE / FIELD:)
structure definitions are supported.
Control flow. ?DUP-IF, ?DUP-0=-IF, and the postpone-state
brackets ]] / [[ work as in Gforth.
Exception allocation. EXCEPTION ( addr u -- n ) allocates a
unique negative throw code, matching Gforth.
C interface. C-FUNCTION, C-VALUE, C-VARIABLE, and \C
follow Gforth's calling convention for declaring external C functions,
constants, and variables. Type codes n, a, u, r match
Gforth's conventions. CODE words also accept Gforth-style type
signatures on the first line.
Miscellaneous. VOCABULARY, CELL, <=, >=, 0<=, 0>=,
and F= are all available.
| Topic | ccforth | Gforth |
|---|---|---|
| Cell size | 64-bit (int64_t) |
Typically 64-bit on modern systems, 32-bit on older ones |
| Division | Floored (matches Gforth) | Floored |
| Float literals | Require E/e exponent (3.14E0) |
Also accepts 3.14 in some configurations |
CODE / ;CODE |
Inline C | Inline assembly |
The 64-bit cell size matches Gforth on modern 64-bit systems but
differs from 32-bit Gforth installations. This affects CELL+,
CELLS, and memory layout of packed data.
LOCALS|— not implemented. Use{ ... }or{: ... :}instead.- Search order in compiled code —
VOCABULARY,DEFINITIONS,GET-ORDER,SET-ORDER, etc. work in the interpreter but cannot be compiled to C (dictionary manipulation is inherently an interpreter-time operation). - Runtime file loading —
INCLUDE-FILE,INCLUDED,REQUIRE,REQUIREDare interpreter-only. CODEdifferences — GforthCODEdefines words with inline assembly; ccforthCODEdefines words with inline C. The syntax is the same but the body language is different. ccforth extendsCODEwith optional type signatures (CODE name n n -- n) that Gforth does not have.
- Replace
LOCALS|with{ }—LOCALS| a b c |becomes{ a b c }(note: stack order is reversed in{ }syntax). - Add exponent markers to float literals —
3.14→3.14E0. - Guard platform-specific CODE words — use
INTERPRETER [IF] ... [ELSE] CODE ... ;CODE [THEN]to provide Forth fallbacks for the interpreter. - Test in the REPL first —
ccforth -iruns in pure interpreter mode, so you can verify word-level behaviour before compiling.
Download a pre-built binary from the
GitHub releases page
and put it on your PATH. Binaries are available for Linux, macOS,
and Windows (amd64 and arm64).
Or install from source with Go:
go install github.com/ncw/ccforth/cmd/ccforth@latestOr build from a checkout:
go build -o ccforth ./cmd/ccforthYou also need a C compiler (gcc or clang) on your PATH for
compiling Forth programs. The -c flag emits C without invoking a
compiler, so a C compiler is not required for that mode or for the
interactive REPL.
# Compile a Forth program to an executable
ccforth -o hello testdata/end-to-end/hello.fth
./hello
# Emit C source only (don't invoke the C compiler)
ccforth -c testdata/end-to-end/factorial.fth
# Use a specific C compiler and entry word
ccforth -cc gcc -entry MAIN -o program source.fth
# Start an interactive REPL
ccforth -i
# Load source files then enter the REPL
ccforth -i mylib.fth
| Flag | Default | Description |
|---|---|---|
-i |
false |
Interactive REPL mode |
-o |
a.out |
Output executable path |
-c |
false |
Emit C only, don't compile |
-cc |
cc |
C compiler to use |
-Cflags |
Extra flags for the C compiler | |
-entry |
MAIN |
Entry word called from main() |
-memsize |
1048576 |
Forth memory size in bytes |
-v |
false |
Print emitted C to stderr |
-32 |
false |
Generate 32-bit cell C code (for cross-compilation) |
-no-line-directives |
false |
Omit #line directives from generated C |
The -i flag starts an interactive REPL. Source files given on the
command line are loaded first, then you get a prompt where you can
type Forth interactively:
$ ccforth -i
ccforth interactive mode
Type `BYE' to exit
1 2 + . 3 ok
: SQUARE DUP * ; ok
5 SQUARE . 25 ok
BYE
Features:
- Tab completion of dictionary words (case-insensitive).
- Line editing and history (persisted to
~/.ccforth_history). BYEor Ctrl-D to exit.
The REPL runs in pure interpreter mode, so INTERPRETER pushes TRUE
and CODE-only words are skipped during bootstrap. All Go primitives
and Forth-defined words are available.
The -32 flag generates C code with 32-bit cells (int32_t) instead
of the default 64-bit cells. This is useful for embedded targets, retro
computing, and cross-compilation to 32-bit architectures.
When -32 is active, the Go interpreter also runs with 32-bit cell
semantics, so all compile-time constants (e.g. 0 INVERT 1 RSHIFT
for MAX-INT) are computed correctly for the target. You provide the
appropriate cross-compiler via -cc and any necessary flags via
-Cflags:
# Cross-compile for ARM32
ccforth -32 -cc arm-linux-gnueabihf-gcc -Cflags "-static" -o hello hello.fth
# Compile 32-bit x86 on a 64-bit Linux host
ccforth -32 -Cflags "-m32" -o hello hello.fth
# Emit 32-bit C only (inspect generated code)
ccforth -32 -c hello.fthThe easiest way to produce a 32-bit executable on an x86-64 machine is
to pass -m32 to the C compiler via -Cflags. You need the 32-bit C
library and development headers installed:
# Debian / Ubuntu — install 32-bit toolchain
sudo apt install gcc-multilib
# Fedora / RHEL
sudo dnf install glibc-devel.i686 libgcc.i686
# Arch
sudo pacman -S lib32-glibc lib32-gcc-libsThen compile and run as normal:
ccforth -32 -Cflags "-m32" -o hello hello.fth
./hello # runs natively on x86-64 Linux
file hello # ELF 32-bit LSB executable, Intel 80386 ...On macOS, -m32 is no longer supported by Apple Clang (dropped in
Xcode 10). Use a cross-compiler or Docker instead.
When -32 is set, -march=native is omitted from the default C
compiler flags since it is incompatible with cross-compilation.
Limitations:
- Values exceeding 2^31 are truncated to 32 bits at interpret time, which is correct Forth behavior for a 32-bit cell system.
- User
CODEwords that assume 64-bit cells will need#ifdef CCFORTH_32BITguards for portable operation. FLOAT+andFLOATSremain 8-byte (doubles are always 64-bit), whileCELL+andCELLSbecome 4-byte.
The examples/ directory contains complete programs demonstrating
different ccforth features:
| Example | Description | Dependencies |
|---|---|---|
| Ray tracer | Renders 3 spheres on a checkerboard ground plane to a PPM image. Demonstrates floating-point math, typed locals, recursive algorithms, and compile-time data with CREATE/F,. |
None |
| Mandelbrot viewer | Interactive Mandelbrot set explorer with click-to-zoom. Uses SDL2 for windowed rendering. | SDL2 |
| Mandelbrot terminal | Animated Mandelbrot zoom in the terminal using ANSI 24-bit color and Unicode half-block characters. | None |
| Breakout | Classic breakout game with paddle, ball, and bricks. Uses SDL2 for rendering and input. | SDL2 |
| Game Boy | Game Boy DMG emulator with CPU/ALU in pure Forth. Renders in the terminal using ANSI 256-color and Unicode half-blocks. Ported from ioccc-gameboy. | None |
| Whetstone benchmark | Classic double-precision floating-point benchmark. Compares C, ccforth compiled, and Gforth interpreted performance (C 1.0x, ccforth 1.4x, Gforth 41.9x). | None |
| Dhrystone benchmark | Classic integer benchmark (v1.1). Compares C, ccforth compiled, and Gforth interpreted performance (C 1.0x, ccforth 3.2x, Gforth 217.5x). | None |
| Towers of Hanoi | Animated terminal demo with rainbow-colored disks, smooth lift/slide/drop animation, and live statistics. Configurable 1–13 disks via command line. | None |
# Ray tracer (no dependencies)
ccforth -o raytrace examples/raytrace/raytrace.fth
./raytrace > scene.ppm
# Mandelbrot terminal animation (no dependencies)
ccforth -o mandelterm examples/mandelbrot/mandelterm.fth
./mandelterm
# Mandelbrot SDL viewer (requires SDL2)
ccforth -Cflags "-lSDL2" -o mandelbrot examples/mandelbrot/mandelbrot.fth
./mandelbrot
# Breakout game (requires SDL2)
ccforth -Cflags "-lSDL2" -o breakout examples/breakout/breakout.fth
./breakout
# Game Boy emulator (no dependencies)
ccforth -Cflags "-Iexamples/gameboy" -o gameboy examples/gameboy/gameboy.fth
./gameboy rom.gbccforth/
+-- cmd/ccforth/ CLI entry point
+-- pkg/
| +-- scanner/ Lexer: source -> tokens
| +-- dictionary/ Word headers, flags, lookup
| +-- memory/ Linear byte-addressed memory model
| +-- interpreter/ Outer interpreter, primitives, control flow
| +-- compiler/ C emitter, peephole optimiser
| +-- codegen/ Orchestrates complete C output
| +-- forth/ Bootstrap Forth definitions (core.fth)
+-- runtime/ Embedded C runtime (runtime.c.src, embed.go)
+-- testdata/
| +-- end-to-end/ Test Forth programs with expected output
| +-- benchmarks/ Benchmark programs
| +-- forth2012-test-suite/ Forth 2012 conformance test suite
go test ./...
Tests run at every level:
- Unit tests for the scanner, memory, dictionary, interpreter, emitter, and peephole optimiser.
- End-to-end compiler tests — each
.fthfile intestdata/end-to-end/is compiled to C, built with gcc, run, and its stdout compared against the matching.txtfile. - End-to-end interpreter tests — the same
.fthfiles are loaded into the interpreter,MAINis called, and output is verified. - Forth 2012 test suite — the official Forth 2012 test suite
(
testdata/forth2012-test-suite/) is run through the interpreter, validating standard compliance across all implemented word sets.
To add a new test, create testdata/end-to-end/mytest.fth (with a
: MAIN ... ; word) and testdata/end-to-end/mytest.txt (with the
expected output). The test harness discovers them automatically.
The Forth 2012 test suite
is included in the testdata/forth2012-test-suite/ directory and run as an
integration test (TestForth2012Suite). It validates conformance to the
Forth 2012 standard across the following word sets:
| Word set | Errors |
|---|---|
| Core | 0 |
| Core extension | 0 |
| Double | 0 |
| Exception | 0 |
| File-access | 0 |
| Locals | 0 |
| Memory-allocation | 0 |
| Programming-tools | 0 |
| Search-order | 0 |
| String | 0 |
| Block | - (not implemented) |
To run just the test suite:
go test -run TestForth2012Suite -v
The test parses the suite's error report and fails if any word set
reports errors. Word sets marked - are not implemented and are
excluded from the pass/fail check.
ccforth does not try to be a good optimising compiler. Instead, it generates simple, transparent C that a good optimising C compiler (gcc, clang) can reason about. The philosophy is: do the minimum work in the emitter to remove obstacles that prevent the C compiler from doing its job.
GCC and clang already know how to allocate registers, schedule
instructions, unroll loops, and eliminate dead code. What they
struggle with is aliasing: when values live in global arrays
(dstack[], rstack[], fstack[]), the compiler cannot prove that
a write through one pointer doesn't clobber a read through another,
so it conservatively reloads values from memory on every access. The
optimisations below focus on keeping hot values out of global arrays
and reducing the number of operations the C compiler has to see
through.
Every generated function is declared static inline, which lets the
C compiler inline across word boundaries and optimise the resulting
code as a single unit. The default compiler flags are
-std=c11 -O3 -march=native -flto.
Operates on the body IR (word references, literals, branches) before C emission. Patterns are matched in two- and three-item windows:
- Constant folding —
LIT 3 LIT 4 +becomesLIT 7. Works for+,-,*,AND,OR,XOR,LSHIFT,RSHIFT. - Identity elimination —
LIT 0 +,LIT 1 *,LIT -1 AND, etc. are removed entirely. - Strength reduction —
LIT 1 +becomes1+,LIT 2 *becomes2*,LIT 2 /becomes2/,DUP +becomes2*,SWAP DROPbecomesNIP. - Word combining —
OVER OVERbecomes2DUP,DROP DROPbecomes2DROP. - SWAP elimination —
SWAPbefore a commutative operation (+,*,AND,OR,XOR,=,<>) is removed, since the operand order doesn't matter. - Return-stack cleanup —
>Rimmediately followed byR>is eliminated (identity round-trip).
This is the single most impactful optimisation. Standard Forth DO
loops store their index and limit on the return stack — a global
array (rstack[]). Every I, LOOP, and +LOOP reads and writes
rstack[rsp-1] and rstack[rsp-2], which the C compiler cannot
promote to registers because it can't prove that other code
(function calls, pointer writes) doesn't alias the same memory.
The emitter rewrites eligible DO/LOOP constructs to use C local variables instead. With local variables, GCC promotes the loop counter and limit to registers. The inner loop of a DO/LOOP benchmark compiles to a handful of register-only instructions with no memory traffic.
Nested loops each get their own C locals (_loop0_*, _loop1_*,
etc.). I refers to the innermost loop's local, J to the next
outer loop. +LOOP uses the same locals with the standard
boundary-crossing check. LEAVE becomes a bare goto (no rstack
cleanup needed). UNLOOP before EXIT is a no-op since there are
no rstack entries to remove.
Eligibility: in the traditional emitter, a loop falls back to
rstack-based emission if its body contains >R, R>, R@, or
UNLOOP. In the parameterised emitter, this restriction does not
apply because >R/R> are handled via the virtual return stack
(see below).
When a comparison word is immediately followed by a conditional
branch (IF, UNTIL, WHILE), the emitter produces a single C
conditional instead of two separate operations:
// Before: 0= IF
TOS = (TOS == 0) ? -1 : 0; // comparison → flag
if (POP() == 0) goto L; // branch on flag
// After: fused
if (POP() != 0) goto L; // one test, no intermediate flagThis eliminates the intermediate -1/0 Forth truth flag and the
second conditional test. Covers all standard comparisons: 0=,
0<>, 0<, 0>, =, <>, <, >, <=, >=, U<, U>.
When a literal is immediately followed by an arithmetic or bitwise operation, the emitter produces a compound assignment instead of a push-then-operate sequence:
// Before: LIT 128 +
PUSH(128);
SECOND += TOS; sp--;
// After: fused
TOS += 128;This avoids a stack push and the associated array write that GCC
cannot always optimise away. Covers +, -, *, AND, OR,
XOR.
The single most impactful optimisation. Instead of every word being
void word(void) with all data flowing through the global dstack[]
array, words with statically known stack effects are emitted as proper
C functions where stack inputs become parameters and outputs become
return values:
// Forth: : FACT DUP 1 > IF DUP 1- RECURSE * ELSE DROP 1 THEN ;
// Generated C:
static inline cell_t word_FACT__p(cell_t s0) {
if (s0 > 1) {
cell_t t0 = s0 - 1;
cell_t t1 = word_FACT__p(t0);
cell_t _m0 = s0 * t1;
return _m0;
} else {
return 1;
}
}This lets GCC keep all values in registers instead of bouncing them
through dstack[]. Stack shuffles like SWAP, ROT, OVER, and
DUP become zero-cost renames in a virtual stack — no C code is
emitted for them at all.
The same approach is applied to all three stacks:
- Data stack — inputs become function parameters, outputs become
return values (single
cell_tor a struct for multiple outputs). Stack shuffles are pure name renames. - Return stack —
>R/R>/R@transfer variable names between a virtual data stack and a virtual return stack. In compiled code the return stack is never used for return addresses (C handles those), so>R/R>are just temporary variable storage. Zero C code is emitted for them. - Float stack —
FDUP/FSWAP/FOVER/FROTare zero-cost name renames.F+/F*etc. becomedouble fN = a + b;with gcc-allocated registers. Float values are flushed to the globalfstack[]at control flow boundaries (branches, loops, word calls) since the float stack does not participate in the branch merge logic.
A stack effect analyser walks each word's body IR to compute
its (inputs, outputs) signature, handling branches (IF/ELSE/THEN),
loops (DO/LOOP, BEGIN/WHILE), locals, VALUEs, and self-recursion
(RECURSE). Words that cannot be statically analysed fall back to the
traditional global-stack emitter. The following constructs prevent
parameterisation:
| Construct | Reason |
|---|---|
PICK |
Dynamic stack depth (index from data stack) |
ROLL |
Dynamic stack depth (count from data stack) |
EXECUTE |
Unknown callee stack effect |
DEPTH |
Reads global sp which parameterised code doesn't maintain |
FDEPTH |
Reads global fsp which parameterised code doesn't maintain |
N>R / NR> |
Dynamic count from data stack |
CATCH |
Modifies control flow via setjmp/longjmp |
Mismatched EXIT depth |
EXIT leaves a different stack depth than the normal return path |
CREATE ... DOES> |
DOES> body has unknown stack effect |
| Mismatched branch depths | IF/ELSE paths leave different stack depths (e.g. ?DUP) |
| Untyped CODE words | CODE words without a type signature or stack effect table entry |
Note: ?DUP-IF and ?DUP-0=-IF are parameterisable — they
compile to dedicated branch instructions (BranchQDupIf /
BranchQDupNot) that the analyser handles natively. Only bare ?DUP
(without IF) is unconvertible due to its internal branch mismatch.
Note: EXIT inside loops is allowed — the analyser verifies
that all EXIT paths leave the same stack depth as the normal return.
All typed CODE words (including multi-return like -- n n) generate
parameterised __p functions. Multi-return words use output variables
r0, r1, ... and return a struct. 92 of 101 CODE words in core.fth
have typed signatures; the remaining 9 (PICK, ROLL, EXECUTE, CATCH,
N>R, NR>, .S, ENVIRONMENT?, >FLOAT) are genuinely unconvertible due
to dynamic stack access or variable-length effects.
Each parameterised word also gets a void(void) wrapper that
pops inputs from the global stack, calls the parameterised version,
and pushes outputs back. This ensures backward compatibility: old-style
callers and function pointers (EXECUTE, CATCH) work unchanged.
When a parameterised word calls an unconverted word, it spills its
virtual stack to dstack[], makes the call, and reloads the
results.
Tail call optimisation converts self-recursive calls at tail
position into parameter reassignment plus goto, turning recursion
into iteration with constant stack space.
- Structured control flow reconstruction. Reconstructing
if/while/forfrom flatgotowould add complexity for marginal gain — GCC's control-flow analysis handlesgotographs well.
A benchmark suite in testdata/benchmarks/ covers 12 categories.
The table below compares three execution methods: ccforth compiled to C
(via gcc -O3 -march=native -flto), the ccforth Go interpreter, and
Gforth 0.7.3. Times are medians of 3 runs on an AMD Ryzen 7 PRO 5850U.
| Benchmark | Compiled | Interpreter | Gforth | Gforth/Compiled | Interpreter/Compiled |
|---|---|---|---|---|---|
| control-flow | 655µs | 1.34s | 206ms | 315x | 2049x |
| floating-point | 7ms | 152ms | 36ms | 5x | 21x |
| integer-arithmetic | 660µs | 274ms | 38ms | 57x | 415x |
| locals-vs-stack | 686µs | 1.91s | 256ms | 373x | 2787x |
| loop-comparison | 665µs | 374ms | 46ms | 69x | 562x |
| matrix-multiply | 358ms | 84.81s | 8.08s | 23x | 237x |
| memory-access | 2ms | 655ms | 68ms | 29x | 279x |
| recursion | 7ms | 584ms | 78ms | 11x | 80x |
| sieve | 13ms | 4.42s | 503ms | 39x | 343x |
| stack-manipulation | 655µs | 340ms | 49ms | 74x | 519x |
| string-memory | 105ms | 7.89s | 756ms | 7x | 75x |
| word-call-return | 2ms | 827ms | 135ms | 56x | 340x |
The compiled output is typically 5–373x faster than Gforth and 21–2787x faster than the interpreter, depending on how well the benchmark's hot loop maps to C optimisation.
Run the benchmarks using Go's standard benchmark framework:
go test -bench BenchmarkCompiled -run ^$ -timeout 300s
go test -bench BenchmarkInterpreter -run ^$ -timeout 300s
go test -bench BenchmarkGforth -run ^$ -timeout 600s
Run a specific benchmark:
go test -bench BenchmarkCompiled/sieve -run ^$ -timeout 60s
Contributions are welcome at https://github.com/ncw/ccforth. To get started:
- Fork the repository and create a feature branch.
- Make your changes. Run
go test ./...and ensure all tests pass. - If adding new Forth words, add both interpreter and C emitter
support, and include a test program in
testdata/end-to-end/. - Open a pull request with a clear description of what you changed and why.
- Block word set — not currently implemented.
- Float stack branch merging — the virtual float stack is flushed
to the global
fstack[]at every control flow boundary (IF, ELSE, BEGIN, DO, word calls). Float values that span across branches could instead participate in the same merge-variable logic used for the data stack, avoiding the flush/reload overhead. This would benefit words likeF-ABS(x F0< IF x FNEGATE ELSE x THEN) where the result is currently bounced through the global array at the merge. - Inline small parameterised words at IR level — copy the body
of small
__pwords into callers before C emission, enabling cross-word peephole optimisation and reducing function call overhead. - Expression folding — collapse chains of single-use temps into
compound expressions (
cell_t t0 = s0 + 1; cell_t t1 = t0 * 2;→cell_t t1 = (s0 + 1) * 2;). This is cosmetic — gcc already does it — but produces cleaner generated C. - More peephole patterns and dead code elimination.
ccforth is licensed under the MIT License.
Output exception: The C source code generated by ccforth (including the embedded runtime) is not considered a derivative work of ccforth. You may use, distribute, and license the generated code and compiled binaries under any terms you choose, with no obligation to apply the MIT license or provide attribution to ccforth.