Output & Running It
Hello, World
Both lines are read and executed the moment the interpreter sees them. The Forth version has to wrap its text in a definition first, because
." is one of a handful of words that only work while compiling.print("Hello, World!"): HELLO ." Hello, World!" CR ;
HELLODefining
HELLO and then running it are two separate acts, and the second is what a reader would type at the prompt. Lua's print adds the newline; Forth's CR is a separate word, because printing and ending the line are separate things.Printing Several Values
print takes any number of arguments and separates them with tabs. .S takes none at all: it shows the whole data stack, which is where the three numbers already are.print(1, 2, 3)1 2 3
.S CRLua's variadic
print and Forth's .S look similar and are not — .S is a debugging tool that leaves the stack untouched, and there is no way to ask it for only some of the values. To consume and print one, use ..string.format Versus Pictured Output
string.format borrows C's format strings. Forth builds the text right to left instead: <# starts, each # takes one digit, HOLD inserts a character, #S takes all the remaining digits, and #> hands back an address and length.print(string.format("%d.%03d", 3, 300)): .FIXED ( thousandths -- )
S>D <# # # # [CHAR] . HOLD #S #> TYPE ;
3300 .FIXED CRIt reads backwards until you notice it is a pipeline running right to left, and then it is hard to unsee. It exists because
. always prints a trailing space, so a number with anything after it has to be assembled this way.load Versus EVALUATE
Lua's
load compiles a string into a function you can then call. EVALUATE takes an address and a length and interprets the text immediately, using the same interpreter that read the rest of the file.local chunk = load("return 2 + 3")
print(chunk()): RUN-SOURCE S" 2 3 + . CR" EVALUATE ;
RUN-SOURCEBoth languages ship their compiler at run time, which is unusual company to be in and is part of why both get embedded. The difference is that Lua hands you a function to call later, while
EVALUATE simply runs it now — there is no chunk object to hold on to.Values On A Stack, Not In Names
local Versus The Stack
Lua gives each value a name and then uses the names. Forth pushes the two numbers and lets
* take them, so nothing is ever named — the order they were pushed in is the only record of which is which.local width = 6
local height = 7
print(width * height)6 7
* . CRThis is the single biggest adjustment coming from Lua: there is no
local, and this Forth has no locals word either. A word that needs a value twice says DUP, and one that needs them the other way round says SWAP.Multiple Return Values
Lua is unusual in returning several values without a container, and Forth is the same — a word simply leaves more cells than it took. Neither needs a tuple, and neither charges you an allocation for it.
local function minmax(a, b)
if a < b then return a, b else return b, a end
end
local low, high = minmax(9, 4)
print(low, high): MINMAX ( a b -- low high )
2DUP > IF SWAP THEN ;
9 4 MINMAX
SWAP . . CRThe difference is that Lua's multiple returns are adjusted to fit the call site, padding with
nil or discarding extras. Forth does no adjusting at all: whatever the word left is still there, and a caller that expected a different number is simply wrong.Swapping Two Values
Lua's parallel assignment evaluates the right side first, which is what makes the one-line swap work.
SWAP is the same idea with the names removed: it exchanges the top two cells.local first, second = 1, 2
first, second = second, first
print(first, second)1 2
SWAP
. . CRForth has a small vocabulary for exactly this —
SWAP, OVER, ROT, NIP, TUCK, DUP, DROP — and learning it is learning the language. Needing more than three or four in a row is the sign that a word is doing too much.There Is No nil
Lua's
nil is a value distinct from every other, which is what lets or supply a default. A Forth cell holds a number and nothing else, so "missing" has to be a number you agree to treat that way.local value = nil
print(value == nil)
print(value or "default")0 CONSTANT MISSING
MISSING 0= . CR
: OR-DEFAULT ( n -- n ) DUP 0= IF DROP 42 THEN ;
MISSING OR-DEFAULT . CRChoosing 0 for that is a convention with a cost: a real measurement of zero is indistinguishable from an absent one. Where the difference matters the idiom is a second cell carrying a flag, which is the same shape Lua's
nil saves you from needing.Words Instead Of Functions
function Versus :
Both define a reusable operation. The Forth version names no parameter and declares no return:
DUP * copies whatever is on top and multiplies, and whatever is left is the result.local function square(value)
return value * value
end
print(square(9)): SQUARE ( n -- n*n ) DUP * ;
9 SQUARE . CRThe body has nowhere to put a parameter name, so the stack comment carries the whole interface — and it is a comment, checked by nobody. Lua does not check types either, but it does check that
square is called with parentheses and gets something.Definition Order Matters
A Lua
local function must also be defined before it is referenced, so this will feel familiar — but Lua only needs the name to exist when the call runs, while Forth needs the word to be in the dictionary when the caller is compiled.local function double(value)
return value * 2
end
local function main()
print(double(21))
end
main(): DOUBLE ( n -- n ) 2 * ;
: MAIN 21 DOUBLE . CR ;
MAINThat difference shows up with globals: a Lua function can call a global defined later, because the lookup happens at call time. Forth resolves the name once, at compile time, and never looks again — which is what makes a call a direct jump with no lookup at all.
Recursion Needs A Keyword
Lua's
local function f is sugar for declaring the local first so the body can see it — without that sugar, recursion would not resolve. Forth has the same problem and a different answer: the word is not in the dictionary until ;, so RECURSE names the definition being compiled.local function factorial(n)
if n > 1 then return n * factorial(n - 1) else return 1 end
end
print(factorial(5)): FACTORIAL ( n -- n! )
DUP 1 > IF DUP 1 - RECURSE * ELSE DROP 1 THEN ;
5 FACTORIAL . CRWriting
FACTORIAL inside its own body would find an earlier word of that name, which is occasionally what you want when redefining something in terms of the version it replaces. Lua's plain function f has the mirror-image trap: it assigns a global that the body then looks up at call time.Varargs Versus Stack Depth
Lua's
... collects however many arguments arrived, and select("#", ...) can count them. Forth cannot count what is on the stack for you, so a variadic word takes the count as its last argument.local function total(...)
local sum = 0
for _, value in ipairs({...}) do sum = sum + value end
return sum
end
print(total(1, 2, 3, 4)): TOTAL ( n1 .. nk k -- sum )
0 SWAP 0 ?DO + LOOP ;
1 2 3 4 4 TOTAL . CRPassing the count explicitly is the standard Forth answer, and it is why so many stack comments end in a number.
DEPTH does report the whole stack depth, but a word that consults it is reaching outside its own arguments and will surprise its caller.Numbers Without A Float
Division Is Not Float Division
Lua 5.3 has two division operators —
/ always produces a float and // floors toward negative infinity. Forth has one / and it is integer division, because there is nothing else for it to produce.print(7 // 2)
print(1 / 2)7 2 / . CR
1 2 / . CRThe second line is the whole row: Lua answers
0.5 and Forth answers 0. A Lua programmer reaching for / out of habit gets a float; in Forth the surprise runs the other way, and there is no warning and no other operator to have meant instead.Fixed Point Instead Of Floats
Lua has one number type that is a double (and, since 5.3, an integer subtype). This Forth has no floating point at all, so a fractional quantity is stored scaled — thousandths here — and only turned into a decimal point when it is printed.
local millivolts = 3300
print(string.format("%.3f", millivolts / 1000)): .VOLTS ( millivolts -- )
S>D <# # # # [CHAR] . HOLD #S #> TYPE ;
3300 .VOLTS CRFixed point is what both languages do on a microcontroller with no floating-point unit, and Forth having dedicated words for printing it is a fair hint about where it is used. The value on the stack is an ordinary integer the whole time; only the printing knows where the point goes.
How Wide Is A Number
Lua 5.3 has one number type with two subtypes: a 64-bit integer and a double, and
/ always produces the second. A Forth cell is whatever the system uses — 32 bits here, which 1 CELLS 8 * reports — and there is no second kind to be promoted to.print(math.type(1))
print(math.type(1 / 2))1 CELLS 8 * . CR
2147483647 1 + . CRBecause the width belongs to the system rather than to the value, the same source prints something else on a 64-bit Forth: gforth on a desktop answers 64 and 2147483648 for these two lines. Lua's integer stays 64 bits wherever it runs, and overflows into a float rather than wrapping.
Bitwise Operators
Lua 5.3 added
&, |, ~ and the shifts as operators. Forth has had AND, OR, XOR, LSHIFT and RSHIFT as words since the beginning, and here they are given names that read at the call site.local flags = 0
flags = flags | (1 << 3)
flags = flags | (1 << 5)
print(flags)
print(flags & (1 << 3) ~= 0): BIT ( position -- mask ) 1 SWAP LSHIFT ;
: SET ( value position -- value ) BIT OR ;
: SET? ( value position -- flag ) BIT AND 0<> ;
0 3 SET 5 SET
DUP . CR
3 SET? . CRLua prints
true where Forth prints -1, because a Forth true flag is all bits set — which is exactly what makes AND and OR work on flags and on numbers with one set of words. Comparing a flag against 1 is therefore a bug waiting.The Table Versus Raw Memory
An Array Is An Address
CREATE makes a word that pushes its own address, and , lays one cell down after it. READING turns an index into an address by multiplying and adding — which is what a Lua array access compiles to, with the bounds check removed.local readings = {10, 20, 30}
print(readings[1] + readings[2] + readings[3])CREATE READINGS 10 , 20 , 30 ,
: READING ( index -- addr ) CELLS READINGS + ;
0 READING @ 1 READING @ 2 READING @ + + . CRForth arrays are 0-based because an index is an offset from the start, and Lua's are 1-based because an index is a key. That is one of the two places a Lua programmer will slip; the other is that nothing here stops
3 READING from reading whatever follows.There Is No Hash Part
A Lua table indexes by any value, so a record and an array are the same structure. Forth has no keys at all: a named field is a word that adds a fixed offset, decided when the code was written.
local settings = { baud = 9600, bits = 8 }
io.write(settings.baud, " ", settings.bits, "\n")CREATE SETTINGS 9600 , 8 ,
: BAUD ( -- addr ) SETTINGS ;
: BITS ( -- addr ) SETTINGS CELL+ ;
BAUD @ . BITS @ . CRThis is the whole of Forth's data modelling, and it is why
CREATE … DOES> in the next section matters — generating those offset words is the only way to avoid writing them out. A Lua table can grow a new key at run time; this layout is fixed the moment it is compiled.Nothing Knows Its Own Length
Lua's
# asks the table how long it is. A Forth array is an address and nothing else, so the length is a separate thing you define and keep correct by hand — by convention named with a leading #.local readings = {10, 20, 30}
print(#readings)CREATE READINGS 10 , 20 , 30 ,
3 CONSTANT #READINGS
#READINGS . CRChanging the array without changing the constant is a bug nothing will catch, which is why the two are written adjacent. It is the same discipline as the
/SAMPLE stride convention: a name that stands in for a fact the machine does not track.Nothing Is Collected
Dropping the last reference to a Lua table is all it takes — the collector reclaims it eventually, and the program never says when.
HERE is the address of the next free byte in the dictionary, so subtracting it before and after shows what the definition cost; that pointer only ever moves forward.local scratch = {}
for index = 1, 100 do scratch[index] = index end
print(#scratch)
scratch = nil
print(scratch)HERE
CREATE SCRATCH 100 CELLS ALLOT
HERE SWAP - . CR
.( and none of it comes back ) CRThere is no garbage collector, no free and no allocator — memory use is decided when the source is read, which is why a Forth system has no collection pause to reason about. Lua's incremental collector is one of the reasons it is embeddable and also the thing a hard-real-time program must budget for. 🚨
collectgarbage itself is not implemented in Fengari, so this row cannot measure the heap the way it would on a desktop Lua.Metatables Versus Defining Words
CREATE … DOES> Writes Words
COLOR is a defining word: running it creates a new word. Everything before DOES> runs once, at definition time, laying the bytes down; everything after it is the code the new word runs when it is used.local function make_color(red, green, blue)
return setmetatable({}, {
__index = { red = red, green = green, blue = blue },
})
end
local plum = make_color(128, 64, 200)
io.write(plum.red, " ", plum.green, " ", plum.blue, "\n"): COLOR ( blue green red -- ) CREATE C, C, C, DOES> ( -- addr ) ;
200 64 128 COLOR PLUM
PLUM C@ . PLUM 1 + C@ . PLUM 2 + C@ . CRThis is the closest thing Forth has to a constructor plus a metatable, and it is an ordinary word rather than a separate mechanism. The values are baked in at definition time —
PLUM has no state to look up, which is why there is no __index to chase.__index Versus A Word That Looks Up
Lua's
__index makes the fallback automatic: a miss on the table consults the metatable. In Forth the fallback is a word you write, and every reader has to go through it for the default to apply.local defaults = { timeout = 30 }
local options = setmetatable({}, { __index = defaults })
print(options.timeout)
options.timeout = 5
print(options.timeout)30 CONSTANT DEFAULT-TIMEOUT
VARIABLE TIMEOUT
0 TIMEOUT !
: EFFECTIVE ( -- n ) TIMEOUT @ DUP 0= IF DROP DEFAULT-TIMEOUT THEN ;
EFFECTIVE . CR
5 TIMEOUT ! EFFECTIVE . CRNothing forces anyone to call
EFFECTIVE rather than TIMEOUT @, so the convention is the whole mechanism. That is the recurring shape of this comparison: Lua provides a hook, and Forth provides a place to write the same behavior by hand.Generating The Field Words
FIELD is a defining word that makes accessor words. Each call stores the current offset, defines a word that adds it to whatever address is given, and hands back the next offset — so the running total ends up as /POINT, the record size.local Point = {}
Point.__index = Point
function Point.new(x, y)
return setmetatable({ x = x, y = y }, Point)
end
local point = Point.new(3, 4)
io.write(point.x, " ", point.y, "\n"): FIELD ( offset -- offset' ) CREATE DUP , CELL+ DOES> @ + ;
0
FIELD >X
FIELD >Y
CONSTANT /POINT
CREATE POINT /POINT ALLOT
3 POINT >X ! 4 POINT >Y !
POINT >X @ . POINT >Y @ . CRThree lines replace writing
POINT CELL+ by hand everywhere, and adding a field in the middle re-numbers the rest automatically. This is the idiom that makes structs bearable in Forth, and it is built from the same CREATE … DOES> as the color above.Strings
A String Is Two Cells
A Lua string is an immutable, interned object that knows its own length.
S" leaves an address and a length as two separate cells, and every word that takes a string takes both.local text = "borrowed slice"
print(#text)
print(text): TEXT ( -- addr len ) S" borrowed slice" ;
: SHOW TEXT NIP . CR TEXT TYPE CR ;
SHOWNothing binds the two cells together, so
NIP can throw the address away and keep the length. Taking a substring is free — add to the address, subtract from the length — which is the compensation for having no string library at all.Substrings Cost Nothing
Lua's
sub builds a new string, which is then interned. Adjusting the address and the length picks out a substring without copying anything — there is no new object, because there were never any objects.local text = "borrowed slice"
print(text:sub(10, 14)): /STRING ( addr len n -- addr+n len-n ) DUP >R - SWAP R> + SWAP ;
: TEXT ( -- addr len ) S" borrowed slice" ;
TEXT 9 /STRING TYPE CR🚨
/STRING is not in this Forth, so the row defines it — which is ordinary practice rather than a workaround, and is how a Forth system grows. It is one line because the operation really is just "add to the address, subtract from the length".There Is No .. Operator
Lua's
.. allocates a new string each time, and the interning makes repeated concatenation famously expensive. Forth has no operator: you reserve a buffer and copy bytes into it with MOVE, tracking how far you have got.local greeting = "hello" .. " " .. "world"
print(greeting)CREATE JOINED 32 ALLOT
VARIABLE FILLED
0 FILLED !
: APPEND ( addr len -- )
DUP >R JOINED FILLED @ + SWAP MOVE
R> FILLED +! ;
: BUILD S" hello" APPEND S" " APPEND S" world" APPEND ;
BUILD
JOINED FILLED @ TYPE CRDoing it by hand means the cost is visible — one buffer, one copy per piece — where Lua's
.. hides three allocations in eleven characters. It also means the buffer's size is your problem, and 32 ALLOT here is a promise nobody checks.No Patterns, No Library
Lua's pattern matching is a small language of its own, built into the string library. Forth has no string library at all, so finding the
= means writing the scan: FIND-EQUALS walks the bytes and reports where it stopped.local text = "baud=9600"
local key, value = text:match("(%w+)=(%w+)")
io.write(key, " ", value, "\n"): TEXT ( -- addr len ) S" baud=9600" ;
: FIND-EQUALS ( addr len -- index )
DUP 0 DO
OVER I + C@ [CHAR] = = IF 2DROP I UNLOOP EXIT THEN
LOOP 2DROP -1 ;
: SHOW-PARTS
TEXT 2DUP FIND-EQUALS >R
OVER R@ TYPE SPACE
R@ 1 + - SWAP R> 1 + + SWAP
TYPE CR ;
SHOW-PARTSA real program would define
SPLIT once and use it everywhere, which is what a Forth library is. This is the clearest case on the page of "minimal" meaning two different things: Lua ships a whole pattern engine in a language famous for being small, and Forth ships nothing and expects you to write the twelve lines.Control Flow
Conditionals Are Postfix
The condition runs before
IF does, because everything is postfix — 25 > leaves a flag and IF consumes it. THEN marks where the branches rejoin; it is not the start of the true branch.local temperature = 30
if temperature > 25 then
print("warm")
else
print("cool")
end: REPORT ( temperature -- )
25 > IF ." warm" ELSE ." cool" THEN CR ;
30 REPORTReading
THEN as "and then carry on" makes the shape work. Unlike Lua, only zero is false here: there is no nil, and 0 being false means a legitimate zero measurement takes the else branch.The Numeric for Loop
Lua's numeric
for is inclusive of its final value, so 0, 4 runs five times. Forth's DO is half-open like a slice: 5 0 DO runs from 0 up to but not including 5.for index = 0, 4 do
io.write(index, " ")
end
print(): COUNT-TO ( limit -- )
0 DO I . LOOP CR ;
5 COUNT-TOThe limit is pushed first and the starting index second, which is the reverse of how the range reads aloud. Between that and the inclusive/exclusive difference, this is the row most likely to produce an off-by-one for a Lua programmer.
A Loop That Should Not Run
Lua checks the range before the first iteration, so
1, 0 does nothing. Forth's plain DO does not check: 0 0 DO runs the body once and then wraps all the way round the cell.for index = 1, 0 do
io.write(index, " ")
end
print("done"): SAFE-COUNT ( limit -- )
0 ?DO I . LOOP ." done" CR ;
0 SAFE-COUNT?DO is the version that tests first, and it is what you want whenever the count comes from a variable. This is the single most dangerous default in the language for someone arriving from a language with real iterators.while And Its Parts
BEGIN marks the top, WHILE consumes a flag and leaves when it is false, and REPEAT jumps back. The condition sits between BEGIN and WHILE rather than in a header.local remaining = 8
while remaining > 1 do
io.write(remaining, " ")
remaining = remaining // 2
end
print(): HALVE-DOWN ( n -- )
BEGIN DUP 1 > WHILE
DUP . 2 /
REPEAT DROP CR ;
8 HALVE-DOWNThe value being tested stays on the stack the whole time, so the loop ends with it still there and
DROP has to clear it — the bookkeeping a Lua local does for free. Forgetting that DROP is how a Forth word silently returns one cell too many.Leaving A Loop Early
LEAVE ends the innermost DO loop, like break. It does not return from the word, and the loop's own cleanup still happens.local readings = {3, 8, 15, 4}
for index = 1, #readings do
if readings[index] > 10 then break end
io.write(readings[index], " ")
end
print()CREATE READINGS 3 , 8 , 15 , 4 ,
: SCAN ( -- )
4 0 DO
READINGS I CELLS + @
DUP 10 > IF DROP LEAVE THEN
.
LOOP CR ;
SCANThe
DROP before LEAVE is needed because the reading is still on the stack and nothing else will take it. Lua's break needs no such care, because the value belonged to a local that is simply going out of scope.Errors Without pcall
error Versus ABORT"
ABORT" throws away both stacks and stops with a message. It is the nearest thing to error, with one crucial difference shown in the next row.local function checked(value)
if value > 25 then error("out of range") end
return value
end
print(checked(12))
print(pcall(checked, 30)): CHECKED ( n -- n ) DUP 25 > ABORT" out of range" ;
12 CHECKED . CR
.( 30 CHECKED would stop the program here ) CRThe row deliberately does not run
30 CHECKED, because it would end the program and nothing after it would print. In Lua the same call under pcall returns false and the message, and the program carries on.There Is No pcall
Standard Forth has
CATCH and THROW, which do what pcall does — but this Forth has neither. A word that might fail therefore returns a flag, and the caller tests it.local ok, message = pcall(function()
error("something failed", 0)
end)
io.write(tostring(ok), " ", message, "\n"): RISKY ( -- result true | false )
FALSE ;
: MAIN RISKY IF . ELSE ." false something failed" THEN CR ;
MAINThat is not only a limitation of this implementation: returning a flag is the ordinary Forth style even where
CATCH exists, because the decision to give up belongs to the application rather than to the word. A library that aborts has taken that choice away from every caller.Returning A Value And A Flag
Lua returns
nil for "not found", which works because nil is a value no real result can be. Forth leaves the value and a flag, with the flag on top, so the caller tests before it finds anything underneath.local function find_even(values)
for _, value in ipairs(values) do
if value % 2 == 0 then return value end
end
return nil
end
local found = find_even({3, 7, 8, 9})
if found then print("found " .. found) else print("none") endCREATE VALUES 3 , 7 , 8 , 9 ,
: FIND-EVEN ( -- value true | false )
4 0 DO
VALUES I CELLS + @
DUP 2 MOD 0= IF TRUE UNLOOP EXIT THEN
DROP
LOOP FALSE ;
: REPORT FIND-EVEN IF ." found " . ELSE ." none" THEN CR ;
REPORTThe two outcomes leave different numbers of cells, so a caller that forgets to test is unbalanced rather than merely wrong — the stack will be one cell off for the rest of the program.
UNLOOP before EXIT is mandatory: the loop's index and limit live on the return stack.Functions As Values
Passing A Word As A Value
' pushes a word's execution token — its address — and EXECUTE runs whatever token is on top. That is the whole mechanism; there is no function type, because a token is just a number.local function double(value) return value * 2 end
local function triple(value) return value * 3 end
local function apply(value, operation)
return operation(value)
end
io.write(apply(5, double), " ", apply(5, triple), "\n"): DOUBLE ( n -- n ) 2 * ;
: TRIPLE ( n -- n ) 3 * ;
: APPLY ( n xt -- n ) EXECUTE ;
5 ' DOUBLE APPLY .
5 ' TRIPLE APPLY . CRA token being an ordinary cell means it can be stored in an array, passed through several words, or arrived at by arithmetic — and also that executing a number that was never a token does something undefined. Lua's functions are real values with a type you can check.
There Are No Closures
Lua's closure captures
count in an upvalue, so each counter made by make_counter has its own. Forth has no closures and no upvalues: the state has to live somewhere addressable, which here is a single VARIABLE.local function make_counter()
local count = 0
return function()
count = count + 1
return count
end
end
local next_value = make_counter()
io.write(next_value(), " ", next_value(), " ", next_value(), "\n")VARIABLE COUNT
0 COUNT !
: NEXT-VALUE ( -- n ) COUNT @ 1 + DUP COUNT ! ;
NEXT-VALUE . NEXT-VALUE . NEXT-VALUE . CR🚨 That means there is exactly one counter. Two independent counters need two variables, or an array of them and a word that takes an index — which is what a Forth programmer writes instead, and it is the biggest thing a Lua programmer gives up on this page.
A Table Of Functions
A Lua table of functions and a Forth array of execution tokens are the same idea with different storage.
, lays each token into the dictionary, and DISPATCH indexes and executes.local commands = {
function() print("stop") end,
function() print("start") end,
function() print("reset") end,
}
for index = 1, 3 do commands[index]() end: STOP ." stop" CR ;
: START ." start" CR ;
: RESET ." reset" CR ;
CREATE COMMANDS ' STOP , ' START , ' RESET ,
: DISPATCH ( index -- ) CELLS COMMANDS + @ EXECUTE ;
: RUN-ALL 3 0 DO I DISPATCH LOOP ;
RUN-ALLThis Forth has no
CASE, so a token table is the idiom once there are more than two or three branches. Lua's table would report nil for a missing index and error on the call; here, index 3 reads the cell after the table and executes it.Rebinding A Name
Assigning a new function to a Lua variable changes what every later call does, because the name is looked up each time. A Forth call is resolved when it is compiled, so changing behavior later needs a word built for it:
DEFER.local emit = function() print("serial") end
emit()
emit = function() print("silent") end
emit(): SERIAL ." serial" CR ;
: SILENT ." silent" CR ;
DEFER EMIT
' SERIAL IS EMIT
EMIT
' SILENT IS EMIT
EMITIS installs an execution token into the deferred word, and every caller compiled against it follows the new one. This is how a Forth driver is retargeted without recompiling anything — and it is opt-in, which is the opposite of Lua, where every global call pays that indirection.Coroutines And The Return Stack
There Are No Coroutines
A Lua coroutine has its own stack, so it can suspend in the middle of a loop and resume where it left off. Forth has one data stack and one return stack for the whole system, and no way to save or switch them.
local producer = coroutine.create(function()
for index = 1, 3 do coroutine.yield(index * 10) end
end)
for _ = 1, 3 do
local _, value = coroutine.resume(producer)
io.write(value, " ")
end
print()VARIABLE NEXT-INDEX
0 NEXT-INDEX !
: NEXT-VALUE ( -- n )
NEXT-INDEX @ 1 + DUP NEXT-INDEX ! 10 * ;
: DRAIN 3 0 DO NEXT-VALUE . LOOP CR ;
DRAIN🚨 There is no way to write
coroutine.yield in standard Forth — a generator has to be turned inside out into a word plus explicit state, as here. Multitasking Forths do exist and give each task its own pair of stacks, but that is a system feature, not something an example can reach for.The Return Stack Is Yours To Use
>R moves a cell to the return stack and R> brings it back. It is the same stack the processor uses for return addresses, and Forth lets you park values on it — carefully.local function reversed(a, b, c)
return c, b, a
end
print(reversed(1, 2, 3)): REVERSED ( a b c -- c b a )
>R SWAP R> SWAP ;
1 2 3 REVERSED
. . . CR🚨 Anything pushed with
>R must be removed before the word returns, or the word returns to the wrong place. Inside a DO loop the loop's index and limit are on that same stack, which is why R@ there reads a loop counter rather than what you stored.Both Are Meant To Be Embedded
One Data Structure Each
Lua's one structure is the table, which is an array, a record and a set at once. Forth's two are the stack, which holds values in flight, and the dictionary, which holds everything with a name — and neither has keys.
local everything = {}
everything[1] = "array part"
everything.key = "hash part"
everything[false] = "any key at all"
print(everything[1], everything.key, everything[false])1 2 3 .S CR
CREATE NAMED 42 ,
NAMED @ . CRLua is famously about 250KB with its standard library; a Forth kernel is measured in kilobytes and often in single digits.
WORDS will list everything the system knows, which is worth typing once at a prompt: that list is the language, and a word you define joins it on the same terms.Adding To The Language
A Lua library is a table of functions that callers index into. A Forth word joins the dictionary directly, so
CLAMP is used exactly like MIN and there is no module to name at the call site.local mathx = {}
function mathx.clamp(value, low, high)
if value < low then return low end
if value > high then return high end
return value
end
print(mathx.clamp(42, 0, 10)): CLAMP ( value low high -- value )
ROT MIN MAX ;
42 0 10 CLAMP . CRHaving no namespaces is a real cost on a large program, and Forth systems answer it with vocabularies or with naming conventions. What it buys is that extending the language and using it are the same act — which is the property both of these languages get embedded for.
Extending The Compiler Itself
An
IMMEDIATE word runs while the word containing it is being compiled, so it can emit whatever it likes into the definition. POSTPONE LITERAL is how it plants a number there.-- Lua's syntax is fixed: you cannot add a keyword.
-- The nearest thing is a function that takes a table.
local function CONFIG(options)
return options.baud
end
print(CONFIG{ baud = 9600 }): TWENTY-ONE 21 POSTPONE LITERAL ; IMMEDIATE
: ANSWER TWENTY-ONE 2 * ;
ANSWER . CRThis is where the two languages genuinely part company. Lua's syntax is fixed and its one extensibility trick is the call-with-table sugar shown above; a Forth program can add control structures to its own compiler using the same words it uses for everything else. The two columns compute different numbers because there is no Lua construct to put opposite an immediate word.
Gotchas For Lua Developers
Indexing Starts At Zero
Lua's arrays conventionally start at 1, and its whole standard library assumes it. A Forth index is an offset from the start of the array, so the first element is at offset 0 and there is no convention to break.
local readings = {10, 20, 30}
io.write(readings[1], " ", readings[2], "\n")CREATE READINGS 10 , 20 , 30 ,
: READING ( index -- addr ) CELLS READINGS + ;
0 READING @ . 1 READING @ . CRBoth columns print the same two numbers, and the indices that produced them are
1, 2 on one side and 0, 1 on the other — which is the whole row. It is the mechanical difference most likely to produce a wrong answer rather than an error, because 0 READING and 3 READING are equally valid addresses.Only Zero Is False
In Lua only
false and nil are false, so 0 takes the true branch. In Forth zero is the false flag and everything else is true, which is the opposite of the trap Lua sets.print(0 and "zero is true in Lua")
print(nil or "nil is false"): ZERO-TEST 0 IF ." never" ELSE ." zero is false in Forth" THEN CR ;
ZERO-TEST
1 2 AND . CRAND is bitwise, not logical, so 1 2 AND is 0 — false — even though both numbers are true flags. A canonical Forth true is -1, all bits set, precisely so that bitwise AND and OR behave as logical ones.The Built-In Words Are Uppercase
This Forth is case-sensitive and its built-in words are spelled in capitals —
CR is a word and cr is "undefined word". Your own definitions keep whatever case you gave them.-- Lua is case-sensitive, and its keywords are lowercase.
local value = 42
print(value): SHOUT ." forty-two" CR ;
SHOUT
42 . CRMost desktop Forths fold case, so an example typed in lowercase works in gforth and fails here. Defining
: Greet does not define GREET, which is the same rule Lua follows and a different result, because Lua has no folding Forths to be copied from.Redefining Does Not Reach Back
Defining
SCALE again adds a new dictionary entry and hides the old one from anything compiled afterwards. APPLY-SCALE was compiled earlier and still calls the first one.local function scale(value) return value * 2 end
local function apply_scale(value) return scale(value) end
print(apply_scale(10))
local function scale(value) return value * 100 end
io.write(apply_scale(10), " ", scale(10), "\n"): SCALE ( n -- n ) 2 * ;
: APPLY-SCALE ( n -- n ) SCALE ;
10 APPLY-SCALE . CR
: SCALE ( n -- n ) 100 * ;
10 APPLY-SCALE . 10 SCALE . CRA new Lua
local behaves the same way, for the same reason — the earlier function captured the earlier binding. Where they part is globals: replacing a Lua global changes every caller at once, while no Forth redefinition ever does.Nobody Is Counting
Lua pads missing arguments with
nil, so a one-argument call is detectable — the b or 0 above is how a Lua programmer handles it. Forth pads with whatever happened to be on the stack: here the leftover 99.local function takes_two(a, b)
return a + (b or 0)
end
print(takes_two(1, 2))
print(takes_two(5)): TAKES-TWO ( a b -- sum ) + ;
99
1 2 TAKES-TWO . CR
5 TAKES-TWO . CRBoth languages let the call happen; only one of them lets you find out. The habit this produces is checking the stack with
.S after any word you are unsure of, because a wrong answer is the only symptom you will get.