Blog › ICP guides
Forth developer on retainer: ANS Forth, stack discipline, embedded firmware, and Mecrisp-Stellaris on monthly retainer
October 4, 2026 · ~20 min read
An industrial sensor firmware project using Mecrisp-Stellaris — a Forth implementation for ARM Cortex-M microcontrollers — developed mysterious data corruption on SPI write operations after 30 to 90 minutes of runtime. The corruption pattern was non-deterministic: sometimes a byte was written twice, sometimes a byte was missing, sometimes the SPI peripheral locked up entirely requiring a hardware reset. The firmware team’s initial hypothesis was a hardware timing issue on the SPI clock line. A logic analyzer was rented. The clock timings were within specification. The SPI peripheral was replaced on two units. The corruption persisted. The Forth developer on retainer reviewed the word definitions with a different hypothesis: a stack comment mismatch.
In Forth, a “stack comment” — ( n addr -- ) — is a documentation convention describing a word’s stack effect: what it consumes from the data stack and what it leaves. Stack comments are not enforced by the Forth system; they are trust. When a word’s actual stack effect differs from its documented effect — because a SWAP or DUP was added or removed during a refactor, or because a conditional branch leaves a different number of items on each path — the mismatch silently corrupts the stack state of every word that calls it. The developer used SEE (Forth’s built-in decompiler) to disassemble spi-write-byte and traced its stack effect manually. The word was documented as ( byte -- ) but contained a defensive bounds check with an unreachable false branch that left one extra item on the data stack every time it executed — which was always. The extra item accumulated on the data stack across every call, eventually overwriting the return stack’s saved instruction pointers and producing the non-deterministic corruption. The fix was a single DROP before the false branch’s exit. One word. Thirty seconds to type in the UART terminal. The SPI corruption disappeared permanently. The work log entry “fixed stack mismatch in spi-write-byte, 0.5h” describes the duration and leaves the client unable to explain to their hardware team why a single DROP eliminated non-deterministic SPI corruption after 30 to 90 minutes of runtime.
Forth fundamentals: data stack, return stack, colon definitions, and stack manipulation
Forth is a stack-based, concatenative programming language. Every operation consumes values from the data stack and pushes results back. 5 3 + pushes 5, pushes 3, then + pops both and pushes 8. . pops and prints the top of the stack. .S prints the entire stack without consuming it — the Forth programmer’s primary debugging tool during interactive development. Stack comments use the notation ( before -- after ) to document a word’s stack effect: ( n1 n2 -- n3 ) means consume n1 and n2, leave n3. ( -- ) means the stack is unchanged. The return stack is separate from the data stack: ; (end of word definition) pops the return address from the return stack to know where to continue execution after the word returns. >R moves the top of the data stack to the return stack for temporary storage; R> moves it back. R@ copies the return stack top without removing it. Misusing the return stack — calling >R without a matching R> before ; — corrupts the return stack and produces a crash.
A colon definition creates a new Forth word: : square ( n -- n² ) dup * ; defines square as a word that duplicates the top of the data stack and multiplies the two copies, leaving the square. Calling 5 square . prints 25. Stack manipulation primitives are the core vocabulary: DUP ( n -- n n ) duplicates the top; DROP ( n -- ) discards the top; SWAP ( n1 n2 -- n2 n1 ) reverses the top two; OVER ( n1 n2 -- n1 n2 n1 ) copies the second item to the top; ROT ( n1 n2 n3 -- n2 n3 n1 ) rotates three items; NIP ( n1 n2 -- n2 ) drops the second item; TUCK ( n1 n2 -- n2 n1 n2 ) copies the top below the second. 2DUP, 2DROP, 2SWAP, 2OVER operate on double-wide (64-bit) values or pairs. A retainer engagement reviewing Forth firmware regularly identifies excessive stack manipulation — deep ROT/OVER chains — as a sign that a word is doing too much. The Forth discipline is that no word should need more than 3 items on the data stack at once. If the stack effect requires more, the word should be split.
IF...THEN and IF...ELSE...THEN are Forth’s conditional structures: : abs ( n -- |n| ) dup 0 < if negate then ; negates negative numbers, leaving the absolute value. Both branches of an IF...ELSE...THEN must leave the same number of items on the data stack — if the true branch leaves two items and the false branch leaves one, the mismatch will corrupt stack state for all subsequent words. The ANS Forth standard does not enforce stack balance in conditional branches at compile time; most embedded Forth implementations, including Mecrisp-Stellaris, have no compile-time stack effect checker. BEGIN...UNTIL loops until the top of the data stack is non-zero (true): : count-down ( n -- ) begin dup . 1- dup 0= until drop ; counts from n to 0. BEGIN...WHILE...REPEAT is the pre-test loop: the test after WHILE exits the loop when zero. DO...LOOP is an index loop: 5 0 do i . loop prints 0 1 2 3 4. I pushes the current loop index; J pushes the outer loop index in nested loops. +LOOP steps by an arbitrary amount: 10 0 do i . 2 +loop prints 0 2 4 6 8.
CREATE and DOES> are Forth’s meta-programming primitives. CREATE foo defines a new word foo that, when executed, pushes the address of its data field. CREATE foo 3 cells allot allocates 3 cells of data in foo’s body. Adding DOES> replaces foo’s default behavior (push address) with the code after DOES>. A peripheral factory pattern: : spi-peripheral ( base-addr -- ) create , does> @ ; defines spi-peripheral as a word that creates SPI peripheral words. $40013000 spi-peripheral SPI1 creates the word SPI1; calling SPI1 pushes $40013000 (the STM32 SPI1 base address). More complex: : register-field ( offset mask -- ) create 2, does> 2@ ( addr offset mask -- ) rot + swap @ and ; creates register-field accessor words. CREATE/DOES> is the mechanism underlying Forth’s data structure vocabulary — constants, variables, arrays, and peripheral abstractions are all built from this pattern.
IMMEDIATE words, CATCH/THROW, Mecrisp-Stellaris embedded Forth, and the SEE decompiler
IMMEDIATE marks a word to be executed at compile time rather than compiled into the current definition. Control flow words like IF, THEN, WHILE, REPEAT, DO, LOOP, and +LOOP are all defined as IMMEDIATE words in Forth systems — they execute at compile time to generate the branch instructions into the word being defined. POSTPONE word inside an IMMEDIATE word compiles word into the word currently being defined — even if word is itself IMMEDIATE. LITERAL compiles a literal value from the stack into the current definition at compile time. STATE is a variable that holds 1 during compilation and 0 during interpretation — IMMEDIATE words can read STATE to behave differently depending on context. An IMMEDIATE word that generates inline unrolled peripheral write sequences at compile time eliminates loop overhead in timing-critical firmware paths: the word executes at compile time, generating N copies of the write sequence inline rather than a loop with overhead per iteration. A retainer engagement designing timing-critical Forth firmware regularly involves IMMEDIATE word authorship for this pattern — code that is invisible as a binary artifact but required for meeting peripheral timing specifications.
CATCH and THROW are ANS Forth’s exception handling mechanism. ['] risky-word catch executes risky-word; if risky-word calls throw with a non-zero value, catch leaves the exception code on the stack and restores the data stack to its state at the catch point. THROW signals an exception: : safe-divide ( n1 n2 -- n3 ) dup 0= if -10 throw then / ; throws exception code -10 if the divisor is zero. Standard exception codes from the ANS Forth specification: -1 (ABORT), -9 (invalid memory address), -11 (result out of range), -13 (undefined word), -14 (interpreting a compile-only word), -22 (control structure mismatch). In Mecrisp-Stellaris firmware, CATCH/THROW is the mechanism for hardware fault isolation: a word that writes to an SPI peripheral wrapped in CATCH can detect the peripheral fault (indicated by a hardware timeout or status register error flag), reset the peripheral, and return a failure code rather than crashing the microcontroller and requiring a hardware reset. The recovery logic is invisible between call sites — it lives in the exception handler, not in the calling code — and a retainer engagement designing resilient Forth firmware typically spends significant time designing exception code taxonomies and recovery procedures for each peripheral fault mode.
Mecrisp-Stellaris is a Forth implementation for ARM Cortex-M microcontrollers (STM32, nRF52, GD32, SAMD) that lives entirely in flash memory — typically 20 to 40 KB for the Forth kernel, with the application dictionary growing from there. The development workflow: connect to the microcontroller over UART using a terminal emulator (picocom, minicom, or GNU screen at 115200 baud), type Forth words directly, and observe results immediately. compiletoflash switches compilation target from RAM to flash — new words are written to non-volatile flash memory and survive power cycles. compiletoram reverts to RAM-only compilation for iterative development. eraseflash clears the entire application dictionary from flash (kernel words are preserved). words lists all defined words in search-order. SEE word decompiles a word to human-readable Forth: it shows the threaded code representation with all constituent word calls and literal values in the order they were compiled. Accessing hardware peripherals: ARM Cortex-M peripherals are memory-mapped at fixed addresses. $40013800 constant USART1-BASE defines the USART1 base address on an STM32F4. USART1-BASE $04 + @ reads the USART1 status register. USART1-BASE $04 + @ $20 and isolates the TXE bit. A retainer engagement maintaining Mecrisp-Stellaris firmware spends the most invisible hours in peripheral register address arithmetic — computing the exact memory-mapped address, bit field position, and read/write mask for each peripheral register from the microcontroller’s reference manual.
VALUE and TO are cleaner alternatives to VARIABLE for simple named values. 10 value sample-rate creates sample-rate with initial value 10; calling sample-rate pushes 10 directly (unlike variable, which pushes the address requiring @ to fetch). 250 to sample-rate updates the value. 2VALUE is the double-width (64-bit) version. DEFER and IS provide deferred execution — a word whose implementation can be changed at runtime: defer on-uart-rx creates on-uart-rx as a deferred word that initially executes noop; ' process-incoming-byte is on-uart-rx makes on-uart-rx execute process-incoming-byte from that point forward. In embedded Forth firmware, DEFER/IS is the standard pattern for interrupt handler registration: defer uart-rx-handler is the default no-op; the application registers its actual receive handler at initialization: ' handle-sensor-data is uart-rx-handler. The interrupt service routine calls uart-rx-handler, which dispatches to whatever word was registered via IS. This separation of interrupt dispatch from handler implementation is the Forth equivalent of a function pointer in C, and it enables modular firmware design without requiring the ISR to know anything about the application layer.
How HourTab tracks Forth developer retainer hours
Forth retainer work produces one of the most extreme versions of the invisible-work problem across all programming language ecosystems. The root cause is Forth’s typeless, unchecked stack discipline: the most critical correctness work — auditing word stack effects, identifying mismatches in conditional branches, verifying that CREATE/DOES> patterns leave the correct stack state, confirming that IMMEDIATE words generate correct branch offsets — produces no artifact visible to anyone who cannot read the Forth source and trace the data stack manually. The client sees the SPI write corruption disappear. They do not see the SEE spi-write-byte decompilation session, the manual stack trace of each call path through the conditional branch, the identification of the extra item left by the false branch’s unreachable exit point, or the DROP that resolved it. The work log entry “fixed stack mismatch in spi-write-byte, 0.5h” describes the duration and leaves the client unable to explain to their hardware team why a single DROP word eliminated non-deterministic SPI corruption after 30 to 90 minutes of runtime — a pattern that had resisted two weeks of hardware diagnosis.
HourTab gives Forth developers a public retainer-hours URL they send to clients — typically embedded systems teams, industrial equipment manufacturers, or research hardware groups — at the start of an engagement. The client bookmarks the URL and checks hours remaining without emailing the developer. The work log is where the technical context lives. For Forth retainers, each entry should name the mechanism involved (data stack effect mismatch; return stack corruption via mismatched >R/R>; IMMEDIATE word compile-time behavior; CREATE/DOES> data structure anatomy; CATCH/THROW exception handler; Mecrisp-Stellaris compiletoflash/compiletoram/eraseflash; memory-mapped peripheral register arithmetic; SEE decompiler session; DEFER/IS interrupt handler registration), the specific word and its documented vs actual stack effect with full stack comment before and after the fix, the diagnostic tool and output (SEE spi-write-byte decompilation showing the unreachable branch; .S output showing extra item accumulation on the data stack after each call to the suspect word; words output after eraseflash/compiletoflash confirming the fix persisted in non-volatile flash), the change and why (DROP required because the conditional false branch’s unreachable exit left one item from the bounds check on the data stack — ANS Forth does not enforce stack balance in conditional branches at compile time, Mecrisp-Stellaris has no compile-time stack effect checker, so the accumulated item was invisible to the system until it overflowed into the return stack’s saved addresses), and the before/after observable metric (SPI write corruption events per hour: 2–4 → 0 after DROP fix; CATCH handler trigger rate: 12/hr → 0 after stack balance restoration; firmware uptime before reset: 30–90 minutes → continuous over 96-hour test run). Entries at that specificity turn an invoice line into a documented reliability improvement the hardware team can reference in their failure mode and effects analysis.
Forth retainers are often compared to Assembly developer retainers and WebAssembly developer retainers as low-level systems programming engagements where the most critical work involves register or stack discipline invisible in the final binary. Assembly retainers involve register allocation, calling convention discipline, and instruction scheduling that produces no visible artifact between correct and incorrect behavior at the assembly level. WebAssembly retainers involve structured stack discipline within the WebAssembly virtual machine and linear memory layout decisions. Forth retainers are unique in that the stack discipline must be maintained entirely by convention — no compiler, no type system, no runtime checker enforces word stack effects unless the Forth implementation specifically includes a stack-effect checker (which Mecrisp-Stellaris does not). Clients who engage a Lua developer on retainer for embedded scripting work encounter a similar “the runtime error is opaque without knowing the interpreter internals” challenge, but Lua’s stack model is hidden behind the C API — Forth’s data stack is the programming model itself, exposed directly to the developer at every level. HourTab’s work log bridges that gap: the entry names the stack effect, the mismatch, the diagnostic session, and the fix, so the client understands what the retainer accomplishes even if they cannot trace a Forth data stack manually.
Track Forth developer retainer hours without the status emails
HourTab gives Forth developers a public URL per client retainer. One link, no login, live burn-down. Your clients stop asking “how many hours do I have left?” and your work log becomes the proof of value that gets the retainer renewed.
See HourTab pricing →FAQ: Forth developer retainers
What does a Forth developer on retainer typically do?
A Forth developer on monthly retainer provides ongoing stack discipline auditing (tracing word stack effects against documented stack comments; identifying conditional branch balance mismatches; using SEE decompiler for manual stack effect verification; using .S for interactive debugging in Mecrisp-Stellaris or gForth), embedded firmware development (ARM Cortex-M peripheral control via memory-mapped register arithmetic; CREATE/DOES> peripheral abstraction data structures; Mecrisp-Stellaris compiletoflash/compiletoram/eraseflash workflow; DEFER/IS interrupt handler registration), ANS Forth wordset architecture (CORE/STRING/FILE/MEMORY wordset design; CATCH/THROW hardware fault isolation; BEGIN loop peripheral polling; VALUE/TO global configuration state; IMMEDIATE word compile-time code generation), and system integration (CONSTANT/2CONSTANT peripheral address definitions; VOCABULARY/SEARCH-ORDER namespace management; Forth-to-C FFI for embedded Forth systems; SEE-readable word design for future maintainability).
What Forth work is most underlogged in a retainer?
Stack effect mismatch diagnosis (word documented as ( byte -- ) had unreachable conditional branch false path leaving extra item on data stack; accumulated across every call; eventually overwrote saved return addresses; caused non-deterministic SPI corruption; fixed with DROP; SPI corruption events/hr: 2–4 → 0; firmware uptime before reset: 30–90 min → continuous over 96-hr test run; 6–16 hours invisible in a single DROP word), CREATE/DOES> data structure design (designing SPI peripheral factory creating instance words with base address, clock divisor, and transfer width in CREATE body; DOES> action pushing instance address; 8–20 hours invisible in a factory definition), and IMMEDIATE word compile-time code generation (unrolled inline peripheral write sequence eliminating loop overhead in timing-critical paths; requires COMPILE,/POSTPONE semantics and STATE variable understanding; 8–18 hours invisible in an IMMEDIATE word definition).
What are typical Forth developer retainer rates?
Entry-level Forth developers (1–2 years, colon definitions, DUP/DROP/SWAP/OVER/ROT, IF...THEN, BEGIN...UNTIL, CONSTANT/VARIABLE, interactive gForth development) bill at $70–$120/hr. Mid-level Forth engineers (2–4 years, stack effect tracing for complex words, CREATE/DOES> design, CATCH/THROW exception handling, IMMEDIATE word authorship, Mecrisp-Stellaris compiletoflash workflow, ARM Cortex-M register arithmetic, DEFER/IS interrupt design) bill at $115–$205/hr. Senior Forth architects (4–8 years, full embedded Forth firmware for production industrial systems, VOCABULARY/SEARCH-ORDER architecture, Forth-to-C FFI design, cross-implementation portability, IMMEDIATE compile-time code generation for tight timing loops, SEE-decompiler-verified word libraries) bill at $165–$300/hr. Monthly retainer ranges: $2,000–$5,000/mo for advisory retainers (15–25 hrs), $6,000–$18,000/mo for full embedded firmware engagements.
What should a Forth developer retainer agreement include?
A Forth developer retainer agreement should specify: stack discipline scope (manual stack effect tracing against documented stack comments; conditional branch balance auditing; SEE decompiler sessions for verification; .S interactive debugging), embedded firmware scope (ARM Cortex-M peripheral control via memory-mapped register arithmetic; CREATE/DOES> peripheral abstraction; Mecrisp-Stellaris compiletoflash/compiletoram/eraseflash workflow; DEFER/IS interrupt handler registration), ANS Forth wordset scope (CORE/STRING/FILE/MEMORY wordset design; CATCH/THROW hardware fault isolation; BEGIN loop design for peripheral polling; VALUE/TO global state; IMMEDIATE word compile-time code generation), system integration scope (CONSTANT/2CONSTANT peripheral address definitions; VOCABULARY/SEARCH-ORDER namespace management; Forth-to-C FFI for embedded host applications; SEE-readable word design for maintainers), and hour logging format (Forth implementation version, microcontroller family, word name and category, documented vs actual stack effect, SEE output, diagnostic method, before/after observable metric).
How should Forth developer retainer hours be logged?
Log each Forth retainer session with: advisory category (data stack effect mismatch; return stack corruption via mismatched >R/R>; IMMEDIATE word compile-time code generation; CREATE/DOES> data structure design; CATCH/THROW exception handler; Mecrisp-Stellaris compiletoflash/compiletoram/eraseflash; memory-mapped ARM Cortex-M peripheral register arithmetic; SEE decompiler session; DEFER/IS interrupt handler registration; VOCABULARY/SEARCH-ORDER namespace management; VALUE/TO global configuration; DO...LOOP index loop; BEGIN...WHILE...REPEAT peripheral polling), word name, category, and implementation, documented vs actual stack effect with full stack comment before/after, diagnostic tool and output (SEE word decompilation showing unreachable conditional branch; .S output showing extra item accumulation per call; CATCH output with throw code identifying peripheral fault), change and why (DROP required because ANS Forth does not enforce stack balance in conditional branches at compile time — accumulated item eventually overwrites return stack saved addresses; CATCH handler required to detect SPI peripheral timeout and reset peripheral without microcontroller crash), and before/after metric (SPI corruption events/hr: 2–4 → 0; CATCH trigger rate: 12/hr → 0; firmware uptime before reset: 30–90 min → continuous 96-hr test run). Include Forth implementation version, microcontroller family (STM32F4xx, nRF52840), word name, and category.