https://github.com/ncw/ccforth
ccforth is a mostly Gforth compatible Forth 2012 compliant Forth-to-C compiler written in Go
https://github.com/ncw/ccforth
compiler forth gforth
Last synced: 6 days ago
JSON representation
ccforth is a mostly Gforth compatible Forth 2012 compliant Forth-to-C compiler written in Go
- Host: GitHub
- URL: https://github.com/ncw/ccforth
- Owner: ncw
- License: mit
- Created: 2026-03-18T17:21:46.000Z (4 months ago)
- Default Branch: master
- Last Pushed: 2026-06-02T10:01:55.000Z (about 2 months ago)
- Last Synced: 2026-07-11T21:00:07.356Z (19 days ago)
- Topics: compiler, forth, gforth
- Language: Go
- Homepage:
- Size: 1000 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# ccforth
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.
- GitHub: https://github.com/ncw/ccforth
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.
## Features
- **Fast compiled output** — generates optimised C11, compiled with
gcc/clang at `-O3`. Typically 5–373x faster than Gforth on
[benchmarks](#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 -i` gives you a full interpreter
with tab completion and line editing for prototyping and debugging.
- **Forth 2012 standard** — passes the official Forth 2012
[test suite](#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](#gforth-compatibility) so existing code ports
easily.
- **Inline C escape hatch** — `CODE` / `;CODE` lets 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 [`-32`
flag](#32-bit-mode) generates 32-bit cell code for cross-compiling
to ARM32, embedded targets, or 32-bit x86 (via `-Cflags "-m32"`).
- **Readable generated C** — `ccforth -c` emits human-readable C with
`#line` directives mapping back to your `.fth` source, so you can
debug with gdb.
- **Floating-point** — full Forth 2012 FLOATING and FLOATING EXT word
sets including transcendentals.
- **Exception handling** — `CATCH` / `THROW` and the `TRY` /
`ENDTRY` extension, compiled via `setjmp` / `longjmp`.
- **Locals** — `{: ... :}` and Gforth `{ ... }` syntax with typed
specifiers, compiled to C local variables.
## How it works
1. **`.fth` source** is read by the **Scanner** (lexer), which
tokenises it.
2. The **Interpreter** executes immediate words and metaprogramming,
building a dictionary of word bodies.
3. The **C emitter** walks the finished dictionary and generates
flattened C from the word bodies.
4. The result is a self-contained **`output.c`** with the runtime
inlined.
5. **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).
### Why a full interpreter?
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.
### Key design choices
- **64-bit cells** (`int64_t`) by default — a cell must be big enough
to hold a pointer and most platforms are 64-bit. A `-32` flag
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 to `void(void)` functions with global stack operations. Both
forms are `static inline`, letting the C compiler optimise across
word boundaries.
- **`goto`-based control flow** — direct translation of Forth branch
instructions. No structured `if`/`while` reconstruction 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 and `malloc`'d buffers from `ALLOCATE`.
- **Peephole optimiser** — operates on the body before C emission.
Folds constants, eliminates identity operations and redundant stack
shuffles. See the [Performance](#performance) section for details.
- **`#line` directives** — generated C includes `#line` directives
that map back to the original `.fth` source files with absolute
paths. This means gdb can be used to debug crashes, though you'll
want to pass `-Cflags -g`. Use `-no-line-directives` to disable this
(useful when debugging the compiler itself).
- **Bootstrap from Forth** — `core.fth` defines many standard words in
Forth itself, as is traditional, and provides `CODE` words 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.
## Supported Forth words
For detailed per-word documentation including stack effects, see [WORDS.md](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`
## Inline C with CODE
`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.
### Typed CODE words
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`:
```forth
CODE FAST-ADD n n -- n
return p0 + p1;
;CODE
CODE FILL a n n --
memset(p0, (int)p2, (size_t)p1);
;CODE
```
Type 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).
### Multi-return CODE words
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:
```forth
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; }
;CODE
```
Multi-return typed CODE words generate `__p` functions with struct
returns, so parameterised callers can call them directly without
spill/reload.
### Untyped CODE words
Without a type signature, CODE words use manual `POP()`/`PUSH()`
macros as before:
```forth
CODE FAST-SQUARE
{ cell_t n = POP(); PUSH(n * n); }
;CODE
```
You 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.
### Calling C library functions
`C-FUNCTION` declares an external C function with typed parameters,
following [Gforth's calling convention](https://gforth.org/manual/Declaring-C-Functions.html):
```forth
\C #include
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 constants and variables
`C-VALUE` exposes a C constant, macro, or expression as a Forth word:
```forth
\C #include
C-VALUE SDL_INIT_VIDEO SDL_INIT_VIDEO -- n
```
`C-VARIABLE` exposes a C variable's address, like a Forth `VARIABLE`:
```forth
\C static int counter = 0;
C-VARIABLE my-counter counter
: MAIN my-counter @ 1+ my-counter ! ;
```
Both follow Gforth's C interface conventions.
### #include hoisting
If the C code contains `#include` directives, they are hoisted to the
top of the generated file and de-duplicated:
```forth
CODE C-SIN r -- r
#include
return sin(p0);
;CODE
```
### Dual-mode definitions
If 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:
```forth
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 `CODE` block inside `INTERPRETER [IF] [ELSE] ...
[THEN]` shadows it. The `CODE` word 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.
## Reading the generated C
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).
### Word names
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
`__`. 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 variable names
Local variables declared with `{: ... :}` or `{ ... }` or `(LOCAL)`
are emitted as C locals using their Forth names, prefixed by `_local_`
(or `_flocal_` for float locals):
```forth
: DISTANCE {: F: x F: y -- F: r :} x x F* y y F* F+ FSQRT ;
```
generates:
```c
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`).
## Forth 2012 standard compliance
For a complete per-word reference organised by standard word set, see [WORDS.md](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` |
### CORE word set (complete)
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]`
`]`
### CORE EXT words implemented
`.(` `.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`
### Known deviations from the standard
**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)`.
## Gforth compatibility
ccforth is not a [Gforth](https://gforth.org/) 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.
### What works
**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.
### Semantic differences
| 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.
### What doesn't work
- **`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`,
`REQUIRED` are interpreter-only.
- **`CODE` differences** — Gforth `CODE` defines words with inline
assembly; ccforth `CODE` defines words with inline C. The syntax
is the same but the body language is different. ccforth extends
`CODE` with optional type signatures (`CODE name n n -- n`) that
Gforth does not have.
### Porting tips
1. **Replace `LOCALS|` with `{ }`** — `LOCALS| a b c |` becomes
`{ a b c }` (note: stack order is reversed in `{ }` syntax).
2. **Add exponent markers to float literals** — `3.14` →
`3.14E0`.
3. **Guard platform-specific CODE words** — use
`INTERPRETER [IF] ... [ELSE] CODE ... ;CODE [THEN]` to provide
Forth fallbacks for the interpreter.
4. **Test in the REPL first** — `ccforth -i` runs in pure
interpreter mode, so you can verify word-level behaviour before
compiling.
## Installation
Download a pre-built binary from the
[GitHub releases page](https://github.com/ncw/ccforth/releases)
and put it on your `PATH`. Binaries are available for Linux, macOS,
and Windows (amd64 and arm64).
Or install from source with Go:
```bash
go install github.com/ncw/ccforth/cmd/ccforth@latest
```
Or build from a checkout:
```bash
go build -o ccforth ./cmd/ccforth
```
You 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.
## Usage
```
# 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
```
### Flags
| 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 |
### Interactive mode
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`).
- **`BYE`** or **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.
### 32-bit mode
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`:
```bash
# 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.fth
```
#### 32-bit x86 on a 64-bit host
The 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:
```bash
# 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-libs
```
Then compile and run as normal:
```bash
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 `CODE` words that assume 64-bit cells will need `#ifdef
CCFORTH_32BIT` guards for portable operation.
- `FLOAT+` and `FLOATS` remain 8-byte (doubles are always 64-bit),
while `CELL+` and `CELLS` become 4-byte.
## Examples
The `examples/` directory contains complete programs demonstrating
different ccforth features:
| Example | Description | Dependencies |
|---------|-------------|--------------|
| [**Ray tracer**](examples/raytrace/) | 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**](examples/mandelbrot/mandelbrot.fth) | Interactive Mandelbrot set explorer with click-to-zoom. Uses SDL2 for windowed rendering. | SDL2 |
| [**Mandelbrot terminal**](examples/mandelbrot/mandelterm.fth) | Animated Mandelbrot zoom in the terminal using ANSI 24-bit color and Unicode half-block characters. | None |
| [**Breakout**](examples/breakout/) | Classic breakout game with paddle, ball, and bricks. Uses SDL2 for rendering and input. | SDL2 |
| [**Game Boy**](examples/gameboy/) | 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](https://github.com/ncw/ioccc-gameboy). | None |
| [**Whetstone benchmark**](examples/whetstone/) | 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**](examples/dhrystone/) | 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**](examples/hanoi/) | Animated terminal demo with rainbow-colored disks, smooth lift/slide/drop animation, and live statistics. Configurable 1–13 disks via command line. | None |
### Quick start
```bash
# 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.gb
```
## Project structure
```
ccforth/
+-- 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
```
## Testing
```
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 `.fth` file in
`testdata/end-to-end/` is compiled to C, built with gcc, run, and
its stdout compared against the matching `.txt` file.
- **End-to-end interpreter tests** — the same `.fth` files are loaded
into the interpreter, `MAIN` is 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.
### Forth 2012 test suite
The [Forth 2012 test suite](https://github.com/gerryjackson/forth2012-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.
## Performance
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`.
### Peephole optimiser
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 +` becomes `LIT 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 +` becomes `1+`, `LIT 2 *` becomes
`2*`, `LIT 2 /` becomes `2/`, `DUP +` becomes `2*`, `SWAP DROP`
becomes `NIP`.
- **Word combining** — `OVER OVER` becomes `2DUP`, `DROP DROP`
becomes `2DROP`.
- **SWAP elimination** — `SWAP` before a commutative operation (`+`,
`*`, `AND`, `OR`, `XOR`, `=`, `<>`) is removed, since the operand
order doesn't matter.
- **Return-stack cleanup** — `>R` immediately followed by `R>` is
eliminated (identity round-trip).
### DO/LOOP to C local variables
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).
### Comparison + branch fusion
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:
```c
// 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 flag
```
This eliminates the intermediate `-1`/`0` Forth truth flag and the
second conditional test. Covers all standard comparisons: `0=`,
`0<>`, `0<`, `0>`, `=`, `<>`, `<`, `>`, `<=`, `>=`, `U<`, `U>`.
### Literal + operation fusion
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:
```c
// 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`.
### Parameterised C emission (stack-to-locals)
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:
```c
// 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_t` or 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` / `FROT` are
zero-cost name renames. `F+` / `F*` etc. become `double fN = a + b;`
with gcc-allocated registers. Float values are flushed to the
global `fstack[]` 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.
### What we deliberately don't do
- **Structured control flow reconstruction.** Reconstructing
`if`/`while`/`for` from flat `goto` would add complexity for
marginal gain — GCC's control-flow analysis handles `goto` graphs
well.
### Benchmarks
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
```
## Contributing
Contributions are welcome at https://github.com/ncw/ccforth. To get
started:
1. Fork the repository and create a feature branch.
2. Make your changes. Run `go test ./...` and ensure all tests pass.
3. If adding new Forth words, add both interpreter and C emitter
support, and include a test program in `testdata/end-to-end/`.
4. Open a pull request with a clear description of what you changed
and why.
### Areas where help is appreciated
- **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 like `F-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 `__p` words 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.
## License
ccforth is licensed under the [MIT License](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.