Monday, June 02, 2025
I've just implemented a Forth system
It started out innocently enough—I just wanted to know how to implement the Forth word DOES>
.
I ended up implementing ANS Forth for the 6809,
as you do.
I've had a fascination with Forth since college. I have a copy of both the first edition and the second edition of Starting Forth. I have a copy of Thinking Forth I also have a copy of Threaded Interpretive Languages with the Robert Tinney cover art. I even wrote my own Forth-like langauge in college, which I used for a class project and a few work-relelated programs.
But the one concept I couldn't figure out how to implement was the Forth word DOES>
.
As a user of Forth,
using DOES>
is pretty easy and one of those things never thought about,
much like closures to a JavaScript programmer.
But implementing it?
That was a problem.
So in April I set out to do that, using the 6809 (because why not?) with the intent of figuring it out. I made a stab with it a few years ago, writing just enough of a Forth implementation in C and got it working. Barely. I wanted to do better.
And boy, did I.
I didn't intend on implementing ANS Forth,
but once I “got” DOES>
done
(and I know the grammar on that doesn't quite work,
but that's Forth for you)
I had enough of a system up that sure—why not finish it?
It's a classical indirect thread coded Forth interpreter. They tend to be the easiest to write and the most compact. They aren't exactly the fastest though. And the 6809 is unique among the 8-bit CPUs in that Forth is a very good match for it. Forth requires two stacks, the 6809 has two stack pointers. There are two index registers, so one can be used as the Forth instruction pointer, and the other one for use. It even has some limited 16-bit arithmatic operations. So it's a good match for Forth.
It just took a bit longer than expected. I ended up implementing 254 Forth words (in Forth, a “word“ is like a function) out of the possible 435—I wanted a Forth independent of any existing operating system, so I skipped implementing a few wordsets (Forth jargon for “module” or “library”). The only routines that need to be provided are a character input routine, a character output routine, and a routine to return back to the operating system. I also didn't implement floating point, as that would take up a considerable amount of space (the IEEE-754 routines for the 6809 Motorola placed into the public domain clock in at 8K and all that gives you is addition, subtraction, multiplcation, division and square roots—and the ANS Forth standard for floating point requires a lot more).
I also tried writing as much of it using ANS Forth standard words as possible, only it turned out to be less than I thought using the restrictions I placed upon myself (mainly—avoid using non-standard Forth words but more on that in a later post).
Then it took a while to get it to pass the test suite—the semantics of some Forth words are a bit tricker than I expected, and some words worked completely differently than how I expected them to work, but again, I'll be going into detail about that later. I also had to debug the test suite as it made some unwarrented assumptions about the Forth environment, mainly case-insensitivity (mine is case-sensitive, which is allowed by the Forth standard) and line length (mine is limited to 80 characters, again, the minimum allowed by the Forth standard). And the occastional outright bug (mostly typos).
Fun times.
Anyway, expect a flurry of posts about implemeting an ANS Forth system.
Discussions about this entry
Wednesday, June 04, 2025
The basics of an indirect threaded code ANS Forth implementation
I need to get into the habit of writing prose more often.
Before I go into the implementation of my ANS Forth system, I need to define somethings. First, a bit about Forth terminology—a Forth “word” can be thought of as a function or subroutine of other languages, but it's a bit more than that—it can also refer to a variable, a constant, the equivalent of a key word. It's a fluid concept, but thinking of a “word” as a function won't be too far off. Also, a collection of Forth words is collected into a wordset (or if you are reading older documentation about Forth, a “dictionary”). You can think of a “dictionary” as a library, or maybe a module, of code.
Second, Forth is a stack-based language. There are rarely any explicit parameters, just data placed on a stack, known as the “data stack.” Because of this, expressions are written in Reverse Polish Notation (RPN)—data is specified first, then the operator.
Third, there's a second stack, the “return stack” that is pretty much what it sounds like—it records the return address when calling into a Forth word.
Forth, the langauge is ridiculously easy to parse—a token is just a collection of characters separated by space. So not only is my_variable_name a valid name for a Forth word, so is my-variable-name, #array, 23-skidoo and even 3.14 valid names for Forth words. Yes, that means it's easy to do stupid things like define the token “1” to be 2 (you know, for when 1 equals two for large values of 1). But this also makes Forth trivial to parse. In fact, a Forth interpreter is nothing more than:
begin name = parse_token() word = lookup(name) if (word) execute(word) else { if (valid_number(name,current_base)) push_number(convert(name,current_base)) else error() } repeat
That's it. That's a Forth interpreter more or less. Compiling Forth isn't much harder:
begin name = parse_token() word = lookup(name) if (word) { if (immediate(word)) execute(word) else compile_into_definition(word) } else { if (valid_number(name,current_base)) add_code_to_push(convert(name,current_base)) else error() } repeat
A Forth word can be marked as “immediate,”
which just means the word is executed during compilation mode rather than being compiled,
and this is how the Forth compiler can be extended.
A Forth word like IF
or BEGIN
is just another Forth word,
albeit marked as “immediate” so it can do its job when compiling.
The details about switching between interpreting and compiling will be covered in a later post, but it's not a difficult as it may seem.
And one bit about ANS Forth in particular—the standard defines a collection of “wordsets,” most of which are optional in an implementation. The minimum required is the Core Word set. The rest, including the Core Extension words, are optional. I did not implement all the wordsets, but again, more on that later.
Anyway, my ANS Forth system is a classic indirect threaded code (ITC) implementation. As I mentioned, it's easy to implement but perhaps not the fastest of implementation styles. As this is on the MC6809, I used a typical-for-the-6809 register use for my Forth system:
Register | Type | Forth usage |
---|---|---|
D | 16-bit accumulator | Free for use (top of stack is kept in memory) |
X | 16-bit index register | execution token of word being run, free for use |
Y | 16-bit index register | Forth IP register |
U | 16-bit index register/user stack | data stack pointer |
S | 16-bit index register/system stack | return stack pointer |
I elected to keep the top of stack in memory and not in the D
register.
I'm not sure if this effects the speed any,
but it was easier,
implementation wise,
to keep the top of stack always in memory.
The Forth words in my implementation are either primitives,
that is,
written in assembly language,
or secondaries,
that is,
written in Forth.
Here's the format of a primitive word,
+
:
forth_core_plus ; n1|u2 n2|u2 -- n3|u3 ) fdb forth_core_star_slash_mod ; previous word in dictionary fdb .xt - .name ; length of name .name fcc "+" ; name .xt fdb .body ; execution token of word .body ldd ,u++ ; body of word addd ,u std ,u ldx ,y++ jmp [,x]
The first thing to notice is the label.
Given how difficult naming is in Computer Science,
I decided to use the canonical name of each Forth Word as defined by the standard.
All the labels start with “forth_,”
and are followed by the wordset
(in this case, “core_”)
followed by the actual Forth word.
Yes,
this makes for some long labels,
but I don't have to think about naming,
which is nice.
The .xt
label
(which is a “local label”—this defines the full label of forth_core_plus.xt
)
defines the “execution token”
(xt)
which is defined as “a value that identifies the execution semantics of a definition.”
Here,
this is a pointer to a function that provides the execution semantics of the word and in this case,
just points to the “body” of the Forth word,
which adds the top two elements of the data stack.
Of particular note are the last two instructions:
ldx ,y++ jmp [,x]
Get used to seeing this fragment,
it's used a lot—this is used to execute the next word in the program.
As stated above,
the Y
register is the Forth IP,
and this loads the X
register with the xt of the next word,
then jumps to the code that handles this word.
The [,X]
bit informs the CPU that we are jumping through a function pointer and not directly to the code
(if we did that, JMP ,X
,
that would turn this from “indirect threaded code” to “direct threaded code”—and yes,
that is the only difference between an ITC and DTC implementation).
The overall format of a word is a link to the previous word in the dictionary
(here to */MOD
),
followed by a 16-bit length,
the text that makes up the word,
followed by the xt and then the body of the definition.
You might be wondering why I would use a full 16-bits for the length on an 8-bit CPU—wouldn't that waste space?
Yes,
it would,
especially given that in this implementation,
a Forth word is restricted to just 31 characters,
but I need a way to mark some information about each word,
like if it's an immediate word or not.
And while an 8-bit length where the largest value would be 31 giving me three bits for flag values,
I ended up needed a few more than just three.
So 16 bits for the length gives me a potential of 11 bits to use as flags.
A word written in Forth will have a different xt and body, for example, the very next word in the dictionary:
forth_core_plus_store ; ( n|u a-addr -- ) fdb forth_core_plus fdb .xt - .name .name fcc "+!" .xt fdb forth_core_colon.runtime ;=============================== ; : +! DUP @ ROT + SWAP ! ; ;=============================== fdb forth_core_dupe.xt fdb forth_core_fetch.xt fdb forth_core_rote.xt fdb forth_core_plus.xt fdb forth_core_swap.xt fdb forth_core_store.xt fdb forth_core_exit.xt
The xt here points to the label forth_core_colon.runtime
.
This is usually called DOCOLON
but I'm being explicit here—Forth words defined in Forth are created by the Forth word :
,
and this label,
forth_core_colon.runtime
,
implements the runtime portion of said word.
The body of this word is then an array of execution tokens of various Forth words comprising the definition.
The last word of all Forth words defined this way end with a call to corth_core_exit.xt
.
The :
runtime function looks like:
forth_core_colon ; ( C: "<spaces>name" -- colon-sys ) E ( i*x -- j*x ) fdb forth_core_two_swap fdb .xt - .name .name fcc ":" .xt fdb .body .body ... ; I'll get to this bit of the code in a later post .runtime pshs y leay 2,x ldx ,y++ jmp [,x]
Here,
the runtime will push the Forth IP
(which is the Y
register)
onto the return stack,
set the Y
register to the body of the word being executed and that two instruction sequence that goes to the next word to execute.
And the function EXIT
looks like:
forth_core_exit ; E ( -- ) ( R: nest-sys -- ) fdb forth_core_execute fdb _NOINTERP :: .xt - .name .name fcc "EXIT" .xt fdb .body .body puls y ; restore the Forth IP ldx ,y++ ; and execute next word jmp [,x]
The first thing to notice here is the _NOINTERP
flag.
EXIT
is defined as having no interpretation semantics,
so typing EXIT
outside of a word being defined is meaningless.
Yes,
this flag is used,
and it does generate an error,
but that again,
is a later post.
I should also mention that the ::
here is a special operator in my assembler.
The left hand side defines the most significant byte of a 16-bit quantity,
and the right hand side defines the least significant byte.
It's short hand for _NOINTERP * 256 + (.xt - .name)
.
The second thing to notice is that the Forth IP
(again, the Y
register)
is pulled from the return stack,
and we yet again,
run that two instruction sequence to run the next word.
And this is pretty much the entire execution engine of Forth.
In fact, I wrote this bit first, even before writing code to compile Forth (and yes, I hand-compiled all Forth code, so I had something to compare against when I eventually got to compiling).
So on the one hand, Forth is an easy language to implement and can be quite small. On the other hand, ANS Forth has some subtle semantics that make for some … interesting implementation details and isn't that easy to implement, as we shall see over the coming posts.
Discussions about this entry
- The basics of an indirect threaded code ANS Forth implementation | Hacker News
- Lazy Reading for 2025/06/08 – DragonFly BSD Digest
Thursday, June 05, 2025
Avoiding Roko's basilisk, part II
The other day I came across this comment on Lobsters:
On a personal level I have helped various people get value out of AI tools where they initially did not understand how to use it properly. But that setting is more of a 1:1 for a specific situation. For generic how to use agentic tools, there are so many articles already. Peter Steinberger has a multi hour talk online of him using an army of agents to write on his project.
If someone has a specific situation where they failed using an agent, ideally with some open source code, I would be happy to have a look at it. It’s just hard to engage on abstract “does not work for me” posts.
Comment on “AI Changes Everything”
I failed using an agent a few months ago. It was on an open source project of mine. Perhaps mitsuhiko would be happy to have a look at it. So I replied.
And mitsuhiko was happy to look at it.
Or rather, spend a few minutes telling his “coding agent” to look at the code and let it do its thing. So I took a look.
Development was done on a Mac,
which doesn't have the vm86()
system call,
so his agent,
“Claude,”
started writing an 8086 emulator.
Or I should say,
an 80386 emulator since that's the most common architecture these days.
It also came up with a few tests and once it those tests were working,
it stopped.
When I tried the code,
attempting to run RACTER.EXE
,
it just sat there,
turning my computer into a space heater.
Looking a bit further,
I saw there was an option for debug output
(but the option appears at the end of the command line,
not after the command itself,
like every other command on Unix).
Then I saw line after line of
... Execute: 2010:0020: 8B Unhandled opcode at 2010:0020: 8B Execute: 2010:0021: EC Unhandled opcode at 2010:0021: EC Execute: 2010:0022: 81 Unhandled opcode at 2010:0022: 81 Execute: 2010:0023: EC Unhandled opcode at 2010:0023: EC Execute: 2010:0024: 02 Unhandled opcode at 2010:0024: 02 Execute: 2010:0025: 00 Unhandled opcode at 2010:0025: 00 Execute: 2010:0026: 9A Unhandled opcode at 2010:0026: 9A Execute: 2010:0027: C2 Unhandled opcode at 2010:0027: C2 Execute: 2010:0028: 10 Unhandled opcode at 2010:0028: 10 Execute: 2010:0029: 52 Unhandled opcode at 2010:0029: 52 Execute: 2010:002A: 24 Unhandled opcode at 2010:002A: 24 Execute: 2010:002B: 9A Unhandled opcode at 2010:002B: 9A Execute: 2010:002C: A2 Unhandled opcode at 2010:002C: A2 Execute: 2010:002D: 19 Unhandled opcode at 2010:002D: 19 Execute: 2010:002E: 52 Unhandled opcode at 2010:002E: 52 ...
To say I was underwhelmed is an understatement.
The thread somewhat petered out.
I noticed today that mitsuhiko gave it another attempt.
He put the whole thing into Docker so he could run under a Linux VM,
and the code now could run enough of RACTER.EXE
to display the banner:
[spc]lucy:/tmp/racter>/tmp/NaNoGenMo-2015/C/msdos RACTER.EXE .-----------------------------------------------------, | | | A CONVERSATION WITH RACTER | | | | COPYRIGHTED BY INRAC CORPORATION, 1984 | | PORTIONS COPYRIGHTED BY MICROSOFT CORPORATION, 1982 | | ........... | `-----------------------------------------------------' Hello, I'm Racter. You are? >Sean Sean
But that's it. It's still chugging along, turning my computer into a space heater. I'm still unimpressed.
This isn't to fault mitsuhiko.
I'm sure he finds value in AI agents coding for him,
but I think this was way out of his bailiwick,
which is why he didn't bother to understand what I was trying to attempt.
“Claude” got to the point of printing the banner from RACTER.EXE
and stopped,
because I think that's all it was instructed to do,
besides attempting to buffer the input.
I'll close this out with the last few comments in the thread:
- Sean
- What type of programming do you do? Or rather, what type of programming do you have Claude do for you? Because I am still unconvinced it will be any benefit to the programming I do.
- mitsuhiko
- Right now I’m building a backend for a prototype of the next project I’m working on. That is a rather complex web application using both Python and Rust. Over the last year or so I used it quite a bit to extend minijinja (but that wasn’t agentic yet).
- Sean
- Ah, stuff that is definitely over-represented in the training sets. Gotcha.
- mitsuhiko
- Considering that I’m doing a very fringe thing I’m not so sure that this is a very accurate assessment :)
- Sean
- Python, Rust and web applications are over-represented in the training sets. The 6809,
RACTER.EXE
and ANS Forth aren’t. What you are doing might be novel, but the tech being used isn’t. The stuff I described isn’t novel (well, maybe having RACTER and Eliza chat, but I was riffing on an article written in the 80s about doing that) but using tech that (in my opinion) is novel (that is, not mainstream). There’s a difference.
I do appreciate the attempt though.
Update on Friday, June 6th, 2025 at 3:06 AM
One last comment from mitsuhiko in the thread: “I had excellent results with completely niche technology too. For as long as you have a way for the machine to validate it’s [sic] outputs it can even program in languages that you just invented.”
I think I'll have to keep this in mind for next time.
Monday, June 09, 2025
Implementing DOES> in Forth, the entire reason I started this mess
The issue I had with DOES>
isn't that it's hard to use—it's just that I had no idea how one would go about implementing it,
much like Javascript programmers use closures without having to think about how they're implemented
(even if they're aware of closures in the first place).
So,
before going into how it works,
a sample from Starting Forth is in order.
: STAR 42 EMIT ; : .ROW CR 8 0 DO DUP 128 AND IF STAR ELSE SPACE THEN 2* LOOP DROP ; : SHAPE CREATE 8 0 DO C, LOOP DOES> DUP 7 + DO I C@ .ROW -1 +LOOP CR ; HEX 18 18 3C 5A 99 24 24 24 SHAPE MAN
These two words support the example.
The first word, STAR
just prints a asterisk
(42 is the ASCII code for the word).
The second word,
.ROW
,
takes an 8-bit value and for each bit,
if it's a 1,
prints an asterisk,
otherwise,
it prints a space.
DO
LOOP
is Forth's for
loop by the way.
The next word,
SHAPE
is the interesting one.
But first,
we need to discuss CREATE
.
This word creates a new entry in the Forth dictionary by reading the next word
(defined as a collection of non-space letters)
in the input as the name.
It then gives the newly created word a default action of pushing the address of the body of the word into the stack.
Going ahead a bit,
the word MAN
just after CREATE
is run will look like this
(in assembly):
man fdb shape ; link to next word fdb .xt - .name .name fcc 'man' .xt fdb forth_core_create.runtime .body
When MAN
is run,
the address of .body
will be pushed onto the stack.
CREATE
is typically used to create “smart data structures”—data structures that know how to do some action.
Now,
getting back to the example,
when SHAPE
is run,
the first thing it does is call CREATE
to create a new word,
then it compiles 8 values off the top of the stack into the body of the newly created word.
Just prior to DOES>
,
MAN
will look like:
man fdb shape ; link to next word fdb .xt - .name .name fcc 'man' .xt fdb forth_core_create.runtime .body fcb $24 fcb $24 fcb $24 fcb $99 fcb $5A fcb $3C fcb $18 fcb $18
Now we get to DOES>
.
Due to the nature of what it does,
DOES>
is an immediate word—that is,
its executing during compilation to do the voodoo that it do.
Um, does.
Somehow,
it needs to modify the newly created word to not only push the address of its body onto the stack,
but execute the code that appears after itself.
So the code to be executed needs to be compiled and stored somewhere,
and somehow MAN
(in this example) needs to run this code.
And this was the problem I had with the word—how does this all work?
Even the well known JonesForth,
implemented as an ITC,
didn't bother with implementing DOES>
(and now that I have implemented DOES>
,
I suspect I know why JonesForth didn't implement it).
The runtime portion of CREATE
just pushes the address of the body of the word into the stack.
The data bytes following the xt have no meaning in and of themselves
(even as code it's nonsensical).
I did a search and found only one page that describes how to implement DOES>
,
but:
- it was part three of a series of articles describing how Forth's are implemented;
- using terminology no longer used by the ANS Forth standard;
- attempting to describe how to implement Forth on several different CPU architectures;
- using a few different methods (like ITC, DTC and STC);
- and on this page, a wierd side trip through another Forth word
;CODE
.
It wasn't an exactly easy source to read,
but between part three and part one,
I was able to puzzle it out
(and it makes much more sense now that I've done it).
Now I can discribe the result using a single architecture
(6809) and a single implementation (ITC).
The trick here is to realize that DOES>
has a temporal aspect unlike any other Forth word.
Most immediate words in Forth have two temporal aspects—at the time of compilation,
and later at runtime.
For instance,
IF
's compile time aspect is to compile a conditional jump into the word,
and the runtime aspect is to do said conditional jump
(at least,
it does so in my implementation).
But DOES>
has three temporal aspects:
: SHAPE CREATE ...a DOES> ( time 1 ) ...b ; ...c SHAPE MAN (time 2 ) MAN (time 3 )
At time 1,
we are compiling a word that creates other words
(so at this point, CREATE
is compiled, not run).
The compiler looks up DOES>
,
notices that it's an immediate word and executes it.
DOES>
at this point needs to include code to cause SHAPE
to stop executing,
then somehow leave … something … behind for time 2,
and somehow compile the rest of the code ...b
for later execution.
At time 2,
we're defining a new word.
CREATE
has been called and the initialization code for this new word …a
has been executed.
At this point,
DOES>
needs to modify the new word … somehow … to execute the code that followed it at time 1.
And at time 3,
the word created is run and somehow,
it needs to know where the code to run is located.
But going back to what CREATE
and the inialization code left us:
man fdb shape ; link to next word fdb .xt - .name .name fcc 'man' .xt fdb forth_core_create.runtime .body fcb $24 fcb $24 fcb $24 fcb $99 fcb $5A fcb $3C fcb $18 fcb $18
What can be done?
The easy answer—DOES>
updates the xt of the newly created word at time 2.
Where is this xt created?
At time 1.
And when is it uses?
At time 3.
Here's what happens.
DOES>
is an immediate word.
When it runs at time 1,
it compiles into the current word
(in this example, SHAPE
)
the xt of its runtime.
So SHAPE
will look like this:
shape fdb dot_row ; link to next word fdb .xt - .name .name fcc 'shape' .xt fdb forth_core_colon.runtime fdb forth_core_create.xt fdb forth_core_literal.runtime_xt fdb 8 fdb forth_core_literal.runtime_xt fdb 0 fdb forth_core_do.runtime_xt .L1 fdb forth_core_literal.runtime_xt fdb 128 fdb forth_core_and.xt fdb forth_core_if.runtime_xt fdb .L2 fdb dot_row.xt fdb forth_core_ext_again.runtime_xt fdb .L3 .L2 fdb forth_core_space.xt .L3 fdb forth_core_two_star.xt fdb forth_core_loop.runtime_xt fdb .L1 fdb forth_core_drop.xt fdb forth_core_does.runtime_xt
(Note: here you can see that literal numbers have the LITERAL
runtime action,
that IF
compiles to its runtime action.
There are two Forth words that pretty much do the same thing—AHEAD
does an unconditional branch forward,
and AGAIN
does an unconditional branch backwards;
they basically both do an unconditional branch,
so I picked one to handle both internally and I picked AGAIN
for this.
More on this in a later post.)
To create the new xt that words created by SHAPE
will use
(or any word that includes DOES>
)
it then lays out a single instruction,
JSR forth_core_create.does_hook
(more on this in a bit).
It then exits,
keeping the compiler “on” so the rest of the code that follows DOES>
gets compiled into the word
(SHAPE
in this case).
This is all DOES>
does (man, that sounds weird) at time 1.
At the end,
SHAPE
looks like:
shape fdb dot_row ; link to next word fdb .xt - .name .name fcc 'shape' .xt fdb forth_core_colon.runtime fdb forth_core_create.xt fdb forth_core_literal.runtime_xt fdb 8 fdb forth_core_literal.runtime_xt fdb 0 fdb forth_core_do.runtime_xt .L1 fdb forth_core_literal.runtime_xt fdb 128 fdb forth_core_and.xt fdb forth_core_if.runtime_xt fdb .L2 fdb dot_row.xt fdb forth_core_ext_again.runtime_xt fdb .L3 .L2 fdb forth_core_space.xt .L3 fdb forth_core_two_star.xt fdb forth_core_loop.runtime_xt fdb .L1 fdb forth_core_drop.xt fdb forth_core_does.runtime_xt .does jsr forth_core_create.does_hook ; !!! fdb forth_core_dupe.xt fdb forth_core_literal.runtime_xt fdb 7 fdb forth_core_plus.xt fdb forth_core_do.runtime_xt .L4 fdb forth_core_i.xt fdb forth_core_c_fetch.xt fdb dot_row.xt fdb forth_core_literal.runtime_xt fdb -1 fdb forth_core_ext_plus_loop.runtime_xt fdb .L4 fdb forth_core_c_r.xt fdb forth_core_exit.xt
Now we execute SHAPE
.
Things go along until we get to forth_core_does.runtime_xt
.
At this point,
the Y
register is pointing to the JSR forth_core_create.does_hook
(see the previous installment for why this is—but to recap:
the Y
register is the Forth IP).
We get the xt of the newly created word
(and yes,
I had to modify CREATE
to stash this for later use)
to replace the default xt.
At this point, MAN
now looks like:
man fdb shape ; link to next word fdb .xt - .name .name fcc 'man' .xt fdb shape.does .body fcb $24 fcb $24 fcb $24 fcb $99 fcb $5A fcb $3C fcb $18 fcb $18
Then the DOES>
runtime basically does a Forth return,
ending the execution of SHAPE
.
Thus ends the steps that happen at time 2.
When MAN
executes,
it executes JSR forth_core_create.does_hook
.
This is a small extension to forth_core_create that does the double duty of pushing the address of the body onto the stack,
and setting things up to run the Forth code compiled just after that instruction:
forth_core_create fdb forth_core_c_r fdb .xt - .name .name fcc "CREATE" .xt fdb .body .body ... ; not important right now .does_hook puls d ; pull return address of the stack pshs y ; push Forth IP onto return stack tfr d,y ; point to DOES> code .runtime leax 2,x ; get body from xt pshu x ; push into the stack ldx ,y++ ; NEXT jmp [,x]
The forth_core_create.does_hook
pulls the return address
(from the JSR
instruction) from the stack—this contains the Forth code after DOES>
that needs to run.
We then push the existing Y
register onto the stack,
then set Y
to the Forth code to execute.
This leads right into forth_core_create.runtime
,
which pushes the body of the word
(in this case, MAN
)
onto the stack,
and then jumps into the code following the DOES>
.
And at the end of all this, you get:
MAN ** ** **** * ** * * ** * * * * * * * OK
I suspect the reason why JonesForth didn't implement DOES>
has to do with the direct subroutine call in the middle of a Forth word.
This only works if memory is both writable and exectuable,
and modern systems tend to disallow that.
There might be a way around this,
but I haven't yet bothered to figure it out.
I'm just happy to have figured it out as it is.
Discussions about this entry
- Implementing DOES> in Forth, the entire reason I started this mess | Lobsters
- Implementing DOES> in Forth, the entire reason I started this mess - Lemmy: Bestiverse
- Implementing DOES> in Forth, the entire reason I started this mess | Hacker News
- Implementing DOES> in Forth, the entire reason I started this mess - The Boston Diaries - Captain Napalm - programming.dev
DOES> RECURSE doesn't DOES> RECURSE does't DOES> RECURSE …
Recursion in Forth isn't as straitforward as you would think. The obvious:
: FOO ... FOO .. ;
doesn't work.
It will either error out as FOO
isn't found,
or it will call the previous definition of FOO
if it exists.
This is a quirk of Forth,
and it one reason why globals aren't as much of an issue as they are in other languages—if you define the word REPEAT
it won't break existing code that called REPEAT
,
they will just keep using the old version of REPEAT
while new words will use the new version.
In fact,
the ANS Forth standard says as much: “The current definition shall not be findable in the dictionary until [colon] is ended.”
Thus the reason for the word RECURSE
,
an immedate word
(which is run durring compilation, not compiled)
to exist in Forth—to do recursion.
This was an easy word to implement:
forth_core_recurse ; ( -- ) fdb forth_core_r_fetch fdb _IMMED | _NOINTERP :: .xt - .name .name fcc "RECURSE" .xt fdb .body .body ldx forth__here ; get current comp location ldd forth__create_xt ; get xt of current word std ,x++ ; recurse stx forth__here ldx ,y++ ; NEXT jmp [,x]
So the above would be written as:
: FOO ... RECURSE ... ;
And the resulting code would look like:
foo fdb ... fdb .xt - .name .name fcc "FOO" .xt fdb forth_core_colon.runtime .body fdb dot_dot_dot.xt fdb foo.xt ; FOO's xt fdb dot_dot_dot.xt fdb forth_core_exit.xt
The only reason I'm mentioning this word is because of this bit from the Standard:
“An ambiguous condition exists if RECURSE appears in a definition after DOES>
.”
There's a reason for that—depending upon the implementation,
it may be impossible to do recursion after DOES>
.
Why?
In my Forth implementation,
the code following DOES>
doesn't have an xt to reference.
The xt of any word is the address of the .xt
field.
So using the example from my explaination of DOES>
,
the xt of MAN
would be of its .xt
field:
man fdb shape ; link to next word fdb .xt - .name .name fcc 'man' .xt fdb shape.does ; the XT of this word is this address .body fcb $24 fcb $24 fcb $24 fcb $99 fcb $5A fcb $3C fcb $18 fcb $18
But the problem is—that address doesn't exist until the word is defined!
If,
for example,
the definition of SHAPE
used RECURSE
:
: SHAPE CREATE 8 0 DO C, LOOP DOES> ... RECURSE ... ;
when RECURSE
is executed,
there is no xt for it to use.
We can't use the xt for SHAPE
—that's not the word we want to recurse on.
And we can't use the address of shape.does
because that's not an actual xt.
And the code following DOES>
can be shared by multiple words:
... SHAPE MAN ... SHAPE FACE-HUGGER ... SHAPE ALIEN ... SHAPE FLAME-THROWER
so there's no single xt that RECURSE
could use when compiling the code after DOES>
(never mind the fact that that happens before the words that use the code are created).
So,
in my Forth implementation,
no RECURSE
after DOES>
.
Which is fine,
because it's an ambiguous condition.
Could I make it work? Maybe. But it would be a lot of work for a feature that Forth programmers can't rely upon anyway.
Wednesday, June 11, 2025
More or fewer, many or less
At Chez Boca, Bunny is the prescriptivist in the household, and I the descriptivist. So while “Grammar rules you can stop sticking to” meshed with my biases, Bunny remained unconvinced with a small exception towards not ending a sentence with a preposition.
But the majority of our discussion centered around the use of “fewer” and “less.”
The rule Bunny was taught was to use “fewer” for a countable number of items and “less” for uncountable or fungible items.
For example,
we have fewer cookies around here because we had less flour to make them
(I originally ended this sentence with “to work with” but I wanted to avoid ending with a preposition).
I always say “less” but I suspect this has less to do with my descriptivism and more to do with programming,
where x < 3
is translated to “x is less than three.”
It just seems weird to say “x is fewer than three,”
despite most numbers on a computer system being countable,
if potentially large
(the only exceptions would be ±inifinty and NaN).
I also wondered about the opposites of “fewer” and “less.” When I asked Bunny for the opposite for “fewer” she said “more,” and when asked for the opposite of “less” she also said “more.” To her, the word “more” could be applied to either countable items, like “I need more cookies,” and for fungible items, like “I need more flour.” But that struck me as odd—why separate words for “a smaller number or amount of” and not for “a greater number or amount of?” Why does “more” get a pass for both concepts, and not something like “many” for countable items, and “more” for fungible items? Why the rule for “less” and “fewer?” I need many cookies, and I need more flour to make them.
After our discussion, I thought about this for a bit. While Robert Baker made this distinction in 1770 (per the video), I have to wonder why he felt the distinction needed to be made, applying “fewer” to numbers rather than “less.”
At first, I thought it may have something to do with the Norman conquest of England. As my 1924 copy of Roget's Treasury Of Words says: “[i]t is interesting to note that the French names for different kinds of food became restricted to the cooked meats; while the English names were reserved for the living animals.” It also noted the act of word doubling—using both the Norman-French and Saxxon terms, such as humble and lowly, poor and needy, act and deed, aid and abet, use and wont, will and testament, and assault and battery.
Could this be a reason for the distinction between “fewer” and “less?”
It's not due to the Norman invasion that's for sure.
While looking through my copy of the Oxford English Dictionary, I found the word “less” is an Old English word from Northumbria, having been a word in both Old Frisian and Old Teutonic. The usage meaning “smaller quantity” didn't first appear until 1314. And as Oxford states, the opposite is “more.”
The word “few” is also an Old English word, also in Old Frisian and Old Teutonic but importantly, not from Northumbria! It's meaning of “smaller quantity” or “a small number” is documented from around 900, and it's “antithesis” (as Oxford calls it) is “many!”
How about that?
But I'm now of the opinion that Robert Baker wanted to signal he wasn't part of the hoi polloi and came up with a pointless distinction. Bunny remains unconvinced of my theory.