/* A sometimes minimal FORTH compiler and tutorial for Linux / i386 systems. -*- asm -*-
By Richard W.M. Jones <rich@annexia.org> http://annexia.org/forth
+ This is PUBLIC DOMAIN (see public domain release statement below).
gcc -m32 -nostdlib -static -Wl,-Ttext,0 -o jonesforth jonesforth.S
This code draws heavily on the design of LINA FORTH (http://home.hccnet.nl/a.w.m.van.der.horst/lina.html)
by Albert van der Horst. Any similarities in the code are probably not accidental.
+ Also I used this document (http://ftp.funet.fi/pub/doc/IOCCC/1992/buzzard.2.design) which really
+ defies easy explanation.
+
+ PUBLIC DOMAIN ----------------------------------------------------------------------
+
+ I, the copyright holder of this work, hereby release it into the public domain. This applies worldwide.
+
+ In case this is not legally possible, I grant any entity the right to use this work for any purpose,
+ without any conditions, unless such conditions are required by law.
+
SETTING UP ----------------------------------------------------------------------
Let's get a few housekeeping things out of the way. Firstly because I need to draw lots of
strings (or rather it used to, but the developers removed it!) so I've abused the syntax
slightly to make things readable. Ignore these warnings.
+ If you want to run your own FORTH programs you can do:
+
+ ./jonesforth < myprog.f
+
+ If you want to load your own FORTH code and then continue reading user commands, you can do:
+
+ cat myfunctions.f - | ./jonesforth
+
ASSEMBLER ----------------------------------------------------------------------
(You can just skip to the next section -- you don't need to be able to read assembler to
Don't worry too much about the exact implementation details of this macro - it's complicated!
*/
+/* Flags - these are discussed later. */
+#define F_IMMED 0x80
+#define F_HIDDEN 0x20
+
// Store the chain of links.
.set link,0
|
LINK in next word
- Again, for brevity in writing the header I'm going to use an assembler macro called defcode.
+ Again, for brevity in writing the header I'm going to write an assembler macro called defcode.
*/
.macro defcode name, namelen, flags=0, label
NEXT
defcode "+",1,,ADD
- pop %eax
- addl %eax,(%esp)
+ pop %eax // get top of stack
+ addl %eax,(%esp) // and add it to next word on stack
NEXT
defcode "-",1,,SUB
- pop %eax
- subl %eax,(%esp)
+ pop %eax // get top of stack
+ subl %eax,(%esp) // and subtract if from next word on stack
NEXT
defcode "*",1,,MUL
orl %eax,(%esp)
NEXT
- defcode "INVERT",6,,INVERT
+ defcode "INVERT",6,,INVERT // this is the FORTH "NOT" function
notl (%esp)
NEXT
-/* Flags. */
-#define F_IMMED 0x80
-#define F_HIDDEN 0x20
+/*
+ RETURNING FROM FORTH WORDS ----------------------------------------------------------------------
- // COLD must not return (ie. must not call EXIT).
- defword "COLD",4,,COLD
- // XXX reinitialisation of the interpreter
- .int INTERPRETER // call the interpreter loop (never returns)
- .int LIT,1,SYSEXIT // hmmm, but in case it does, exit(1).
+ Time to talk about what happens when we EXIT a function. In this diagram QUADRUPLE has called
+ DOUBLE, and DOUBLE is about to exit (look at where %esi is pointing):
+
+ QUADRUPLE
+ +------------------+
+ | codeword |
+ +------------------+ DOUBLE
+ | addr of DOUBLE ---------------> +------------------+
+ +------------------+ | codeword |
+ | addr of DOUBLE | +------------------+
+ +------------------+ | addr of DUP |
+ | addr of EXIT | +------------------+
+ +------------------+ | addr of + |
+ +------------------+
+ %esi -> | addr of EXIT |
+ +------------------+
+
+ What happens when the + function does NEXT? Well, the following code is executed.
+*/
defcode "EXIT",4,,EXIT
POPRSP %esi // pop return stack into %esi
NEXT
+/*
+ EXIT gets the old %esi which we saved from before on the return stack, and puts it in %esi.
+ So after this (but just before NEXT) we get:
+
+ QUADRUPLE
+ +------------------+
+ | codeword |
+ +------------------+ DOUBLE
+ | addr of DOUBLE ---------------> +------------------+
+ +------------------+ | codeword |
+ %esi -> | addr of DOUBLE | +------------------+
+ +------------------+ | addr of DUP |
+ | addr of EXIT | +------------------+
+ +------------------+ | addr of + |
+ +------------------+
+ | addr of EXIT |
+ +------------------+
+
+ And NEXT just completes the job by, well in this case just by calling DOUBLE again :-)
+
+ LITERALS ----------------------------------------------------------------------
+
+ The final point I "glossed over" before was how to deal with functions that do anything
+ apart from calling other functions. For example, suppose that DOUBLE was defined like this:
+
+ : DOUBLE 2 * ;
+
+ It does the same thing, but how do we compile it since it contains the literal 2? One way
+ would be to have a function called "2" (which you'd have to write in assembler), but you'd need
+ a function for every single literal that you wanted to use.
+
+ FORTH solves this by compiling the function using a special word called LIT:
+
+ +---------------------------+-------+-------+-------+-------+-------+
+ | (usual header of DOUBLE) | DOCOL | LIT | 2 | * | EXIT |
+ +---------------------------+-------+-------+-------+-------+-------+
+
+ LIT is executed in the normal way, but what it does next is definitely not normal. It
+ looks at %esi (which now points to the literal 2), grabs it, pushes it on the stack, then
+ manipulates %esi in order to skip the literal as if it had never been there.
+
+ What's neat is that the whole grab/manipulate can be done using a single byte single
+ i386 instruction, our old friend LODSL. Rather than me drawing more ASCII-art diagrams,
+ see if you can find out how LIT works:
+*/
+
defcode "LIT",3,,LIT
// %esi points to the next command, but in this case it points to the next
// literal 32 bit integer. Get that literal into %eax and increment %esi.
push %eax // push the literal number on to stack
NEXT
- defcode "LITSTRING",9,,LITSTRING
- lodsl // get the length of the string
- push %eax // push it on the stack
- push %esi // push the address of the start of the string
- addl %eax,%esi // skip past the string
- addl $3,%esi // but round up to next 4 byte boundary
- andl $~3,%esi
- NEXT
-
- defcode "BRANCH",6,,BRANCH
- add (%esi),%esi // add the offset to the instruction pointer
- NEXT
+/*
+ MEMORY ----------------------------------------------------------------------
- defcode "0BRANCH",7,,ZBRANCH
- pop %eax
- test %eax,%eax // top of stack is zero?
- jz code_BRANCH // if so, jump back to the branch function above
- lodsl // otherwise we need to skip the offset
- NEXT
+ As important point about FORTH is that it gives you direct access to the lowest levels
+ of the machine. Manipulating memory directly is done frequently in FORTH, and these are
+ the primitive words for doing it.
+*/
defcode "!",1,,STORE
pop %ebx // address to store at
push %eax // push value onto stack
NEXT
+/*
+ BUILT-IN VARIABLES ----------------------------------------------------------------------
+
+ These are some built-in variables and related standard FORTH words. Of these, the only one that we
+ have discussed so far was LATEST, which points to the last (most recently defined) word in the
+ FORTH dictionary. LATEST is also a FORTH word which pushes the address of LATEST (the variable)
+ on to the stack, so you can read or write it using @ and ! operators. For example, to print
+ the current value of LATEST (and this can apply to any FORTH variable) you would do:
+
+ LATEST @ . CR
+
+ To make defining variables shorter, I'm using a macro called defvar, similar to defword and
+ defcode above. (In fact the defvar macro uses defcode to do the dictionary header).
+*/
+
.macro defvar name, namelen, flags=0, label, initial=0
defcode \name,\namelen,\flags,\label
push $var_\name
.int \initial
.endm
- // The STATE variable is 0 for execute mode, != 0 for compile mode
- defvar "STATE",5,,STATE
+/*
+ The built-in variables are:
- // This points to where compiled words go.
- defvar "HERE",4,,HERE,user_defs_start
+ STATE Is the interpreter executing code (0) or compiling a word (non-zero)?
+ LATEST Points to the latest (most recently defined) word in the dictionary.
+ HERE Points to the next free byte of memory. When compiling, compiled words go here.
+ _X These are three scratch variables, used by some standard dictionary words.
+ _Y
+ _Z
+ S0 Stores the address of the top of the parameter stack.
+ R0 Stores the address of the top of the return stack.
- // This is the last definition in the dictionary.
+*/
+ defvar "STATE",5,,STATE
+ defvar "HERE",4,,HERE,user_defs_start
defvar "LATEST",6,,LATEST,name_SYSEXIT // SYSEXIT must be last in built-in dictionary
-
- // _X, _Y and _Z are scratch variables used by standard words.
defvar "_X",2,,TX
defvar "_Y",2,,TY
defvar "_Z",2,,TZ
-
- // This stores the top of the data stack.
defvar "S0",2,,SZ
-
- // This stores the top of the return stack.
defvar "R0",2,,RZ,return_stack
- defcode "DSP@",4,,DSPFETCH
- mov %esp,%eax
- push %eax
- NEXT
+/*
+ RETURN STACK ----------------------------------------------------------------------
- defcode "DSP!",4,,DSPSTORE
- pop %esp
- NEXT
+ These words allow you to access the return stack. Recall that the register %ebp always points to
+ the top of the return stack.
+*/
defcode ">R",2,,TOR
pop %eax // pop parameter stack into %eax
lea 4(%ebp),%ebp // pop return stack and throw away
NEXT
+/*
+ PARAMETER (DATA) STACK ----------------------------------------------------------------------
+
+ These functions allow you to manipulate the parameter stack. Recall that Linux sets up the parameter
+ stack for us, and it is accessed through %esp.
+*/
+
+ defcode "DSP@",4,,DSPFETCH
+ mov %esp,%eax
+ push %eax
+ NEXT
+
+ defcode "DSP!",4,,DSPSTORE
+ pop %esp
+ NEXT
+
+/*
+ INPUT AND OUTPUT ----------------------------------------------------------------------
+
+ These are our first really meaty/complicated FORTH primitives. I have chosen to write them in
+ assembler, but surprisingly in "real" FORTH implementations these are often written in terms
+ of more fundamental FORTH primitives. I chose to avoid that because I think that just obscures
+ the implementation. After all, you may not understand assembler but you can just think of it
+ as an opaque block of code that does what it says.
+
+ Let's discuss input first.
+
+ The FORTH word KEY reads the next byte from stdin (and pushes it on the parameter stack).
+ So if KEY is called and someone hits the space key, then the number 32 (ASCII code of space)
+ is pushed on the stack.
+
+ In FORTH there is no distinction between reading code and reading input. We might be reading
+ and compiling code, we might be reading words to execute, we might be asking for the user
+ to type their name -- ultimately it all comes in through KEY.
+
+ The implementation of KEY uses an input buffer so a certain size (defined at the end of the
+ program). It calls the Linux read(2) system call to fill this buffer and tracks its position
+ in the buffer using a couple of variables, and if it runs out of input buffer then it refills
+ it automatically. The other thing that KEY does is if it detects that stdin has closed, it
+ exits the program, which is why when you hit ^D the FORTH system cleanly exits.
+*/
+
#include <asm-i386/unistd.h>
defcode "KEY",3,,KEY
mov $__NR_exit,%eax // syscall: exit
int $0x80
+/*
+ By contrast, output is much simpler. The FORTH word EMIT writes out a single byte to stdout.
+ This implementation just uses the write system call. No attempt is made to buffer output, but
+ it would be a good exercise to add it.
+*/
+
defcode "EMIT",4,,EMIT
pop %eax
call _EMIT
.bss
2: .space 1 // scratch used by EMIT
+/*
+ Back to input, WORD is a FORTH word which reads the next full word of input.
+
+ What it does in detail is that it first skips any blanks (spaces, tabs, newlines and so on).
+ Then it calls KEY to read characters into an internal buffer until it hits a blank. Then it
+ calculates the length of the word it read and returns the address and the length as
+ two words on the stack (with address at the top).
+
+ Notice that WORD has a single internal buffer which it overwrites each time (rather like
+ a static C string). Also notice that WORD's internal buffer is just 32 bytes long and
+ there is NO checking for overflow. 31 bytes happens to be the maximum length of a
+ FORTH word that we support, and that is what WORD is used for: to read FORTH words when
+ we are compiling and executing code. The returned strings are not NUL-terminated, so
+ in some crazy-world you could define FORTH words containing ASCII NULs, although why
+ you'd want to is a bit beyond me.
+
+ WORD is not suitable for just reading strings (eg. user input) because of all the above
+ peculiarities and limitations.
+
+ Note that when executing, you'll see:
+ WORD FOO
+ which puts "FOO" and length 3 on the stack, but when compiling:
+ : BAR WORD FOO ;
+ is an error (or at least it doesn't do what you might expect). Later we'll talk about compiling
+ and immediate mode, and you'll understand why.
+*/
+
defcode "WORD",4,,WORD
call _WORD
push %ecx // push length
// overwrite this buffer. Maximum word length is 32 chars.
5: .space 32
- defcode "EMITSTRING",10,,EMITSTRING
- mov $1,%ebx // 1st param: stdout
- pop %ecx // 2nd param: address of string
- pop %edx // 3rd param: length of string
-
- mov $__NR_write,%eax // write syscall
- int $0x80
+/*
+ . (also called DOT) prints the top of the stack as an integer. In real FORTH implementations
+ it should print it in the current base, but this assembler version is simpler and can only
+ print in base 10.
- NEXT
+ Remember that you can override even built-in FORTH words easily, so if you want to write a
+ more advanced DOT then you can do so easily at a later point, and probably in FORTH.
+*/
defcode ".",1,,DOT
pop %eax // Get the number to print into %eax
call _EMIT
ret
- // Parse a number from a string on the stack -- almost the opposite of . (DOT)
- // Note that there is absolutely no error checking. In particular the length of the
- // string must be >= 1 bytes.
+/*
+ Almost the opposite of DOT (but not quite), SNUMBER parses a numeric string such as one returned
+ by WORD and pushes the number on the parameter stack.
+
+ This function does absolutely no error checking, and in particular the length of the string
+ must be >= 1 bytes, and should contain only digits 0-9. If it doesn't you'll get random results.
+
+ This function is only used when reading literal numbers in code, and shouldn't really be used
+ in user code at all.
+*/
defcode "SNUMBER",7,,SNUMBER
pop %edi
pop %ecx
jnz 1b
ret
+/*
+ DICTIONARY LOOK UPS ----------------------------------------------------------------------
+
+ We're building up to our prelude on how FORTH code is compiled, but first we need yet more infrastructure.
+
+ The FORTH word FIND takes a string (a word as parsed by WORD -- see above) and looks it up in the
+ dictionary. What it actually returns is the address of the dictionary header, if it finds it,
+ or 0 if it didn't.
+
+ So if DOUBLE is defined in the dictionary, then WORD DOUBLE FIND returns the following pointer:
+
+ pointer to this
+ |
+ |
+ V
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT |
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+
+ See also >CFA which takes a dictionary entry pointer and returns a pointer to the codeword.
+
+ FIND doesn't find dictionary entries which are flagged as HIDDEN. See below for why.
+*/
+
defcode "FIND",4,,FIND
pop %edi // %edi = address
pop %ecx // %ecx = length
xor %eax,%eax // Return zero to indicate not found.
ret
- defcode ">CFA",4,,TCFA // DEA -> Codeword address
+/*
+ FIND returns the dictionary pointer, but when compiling we need the codeword pointer (recall
+ that FORTH definitions are compiled into lists of codeword pointers).
+
+ In the example below, WORD DOUBLE FIND >CFA
+
+ FIND returns a pointer to this
+ | >CFA converts it to a pointer to this
+ | |
+ V V
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT |
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+
+ Notes:
+
+ Because names vary in length, this isn't just a simple increment.
+
+ In this FORTH you cannot easily turn a codeword pointer back into a dictionary entry pointer, but
+ that is not true in most FORTH implementations where they store a back pointer in the definition
+ (with an obvious memory/complexity cost). The reason they do this is that it is useful to be
+ able to go backwards (codeword -> dictionary entry) in order to decompile FORTH definitions.
+*/
+
+ defcode ">CFA",4,,TCFA
pop %edi
call _TCFA
push %edi
andl $~3,%edi
ret
- defcode "CHAR",4,,CHAR
- call _WORD // Returns %ecx = length, %edi = pointer to word.
- xor %eax,%eax
- movb (%edi),%al // Get the first character of the word.
- push %eax // Push it onto the stack.
- NEXT
+/*
+ COMPILING ----------------------------------------------------------------------
+
+ Now we'll talk about how FORTH compiles words. Recall that a word definition looks like this:
+
+ : DOUBLE DUP + ;
+
+ and we have to turn this into:
+
+ pointer to previous word
+ ^
+ |
+ +--|------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT |
+ +---------+---+---+---+---+---+---+---+---+------------+--|---------+------------+------------+
+ ^ len pad codeword |
+ | V
+ LATEST points here points to codeword of DUP
+
+ There are several problems to solve. Where to put the new word? How do we read words? How
+ do we define : (COLON) and ; (SEMICOLON)?
+
+ FORTH solves this rather elegantly and as you might expect in a very low-level way which
+ allows you to change how the compiler works in your own code.
+
+ FORTH has an INTERPRETER function (a true interpreter this time, not DOCOL) which runs in a
+ loop, reading words (using WORD), looking them up (using FIND), turning them into codeword
+ points (using >CFA) and deciding what to do with them. What it does depends on the mode
+ of the interpreter (in variable STATE). When STATE is zero, the interpreter just runs
+ each word as it looks them up. (Known as immediate mode).
+
+ The interesting stuff happens when STATE is non-zero -- compiling mode. In this mode the
+ interpreter just appends the codeword pointers to user memory (the HERE variable points to
+ the next free byte of user memory).
+
+ So you may be able to see how we could define : (COLON). The general plan is:
+
+ (1) Use WORD to read the name of the function being defined.
+
+ (2) Construct the dictionary entry header in user memory:
+
+ pointer to previous word (from LATEST) +-- Afterwards, HERE points here, where
+ ^ | the interpreter will start appending
+ | V codewords.
+ +--|------+---+---+---+---+---+---+---+---+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL |
+ +---------+---+---+---+---+---+---+---+---+------------+
+ len pad codeword
+
+ (3) Set LATEST to point to the newly defined word and most importantly leave HERE pointing
+ just after the new codeword. This is where the interpreter will append codewords.
+
+ (4) Set STATE to 1. Go into compile mode so the interpreter starts appending codewords.
+
+ After : has run, our input is here:
+
+ : DOUBLE DUP + ;
+ ^
+ |
+ Next byte returned by KEY
+
+ so the interpreter (now it's in compile mode, so I guess it's really the compiler) reads DUP,
+ gets its codeword pointer, and appends it:
+
+ +-- HERE updated to point here.
+ |
+ V
+ +---------+---+---+---+---+---+---+---+---+------------+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP |
+ +---------+---+---+---+---+---+---+---+---+------------+------------+
+ len pad codeword
+
+ Next we read +, get the codeword pointer, and append it:
+
+ +-- HERE updated to point here.
+ |
+ V
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + |
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+
+ len pad codeword
+
+ The issue is what happens next. Obviously what we _don't_ want to happen is that we
+ read ; and compile it and go on compiling everything afterwards.
+
+ At this point, FORTH uses a trick. Remember the length byte in the dictionary definition
+ isn't just a plain length byte, but can also contain flags. One flag is called the
+ IMMEDIATE flag (F_IMMED in this code). If a word in the dictionary is flagged as
+ IMMEDIATE then the interpreter runs it immediately _even if it's in compile mode_.
+
+ I hope I don't need to explain that ; (SEMICOLON) is an IMMEDIATE flagged word. And
+ all it does is append the codeword for EXIT on to the current definition and switch
+ back to immediate mode (set STATE back to 0). After executing ; we get this:
+
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+ | LINK | 6 | D | O | U | B | L | E | 0 | DOCOL | DUP | + | EXIT |
+ +---------+---+---+---+---+---+---+---+---+------------+------------+------------+------------+
+ len pad codeword ^
+ |
+ HERE
+
+ And that's it, job done, our new definition is compiled.
+
+ The only last wrinkle in this is that while our word was being compiled, it was in a
+ half-finished state. We certainly wouldn't want DOUBLE to be called somehow during
+ this time. There are several ways to stop this from happening, but in FORTH what we
+ do is flag the word with the HIDDEN flag (F_HIDDEN in this code) just while it is
+ being compiled. This prevents FIND from finding it, and thus in theory stops any
+ chance of it being called.
+
+ Compared to the description above, the actual definition of : (COLON) is comparatively simple:
+*/
defcode ":",1,,COLON
movl $1,var_STATE
NEXT
+/*
+ , (COMMA) is a standard FORTH word which appends a 32 bit integer (normally a codeword
+ pointer) to the user data area pointed to by HERE, and adds 4 to HERE.
+*/
+
defcode ",",1,,COMMA
pop %eax // Code pointer to store.
call _COMMA
movl %edi,var_HERE // Update HERE (incremented)
ret
- defcode "HIDDEN",6,,HIDDEN
- call _HIDDEN
+/*
+ ; (SEMICOLON) is also elegantly simple. Notice the F_IMMED flag.
+*/
+
+ defcode ";",1,F_IMMED,SEMICOLON
+ movl $EXIT,%eax // EXIT is the final codeword in compiled words.
+ call _COMMA // Store it.
+ call _HIDDEN // Toggle the HIDDEN flag (unhides the new word).
+ xor %eax,%eax // Set STATE to 0 (back to execute mode).
+ movl %eax,var_STATE
NEXT
-_HIDDEN:
- movl var_LATEST,%edi // LATEST word.
- addl $4,%edi // Point to name/flags byte.
- xorb $F_HIDDEN,(%edi) // Toggle the HIDDEN bit.
- ret
+
+/*
+ IMMEDIATE mode words aren't just for the FORTH compiler to use. You can define your
+ own IMMEDIATE words too. The IMMEDIATE word toggles the F_IMMED (IMMEDIATE flag) on the
+ most recently defined word, or on the current word if you call it in the middle of a
+ definition.
+
+ Typical usage is:
+
+ : MYIMMEDWORD IMMEDIATE
+ ...definition...
+ ;
+
+ but some FORTH programmers write this instead:
+
+ : MYIMMEDWORD
+ ...definition...
+ ; IMMEDIATE
+
+ The two are basically equivalent.
+*/
defcode "IMMEDIATE",9,F_IMMED,IMMEDIATE
call _IMMEDIATE
xorb $F_IMMED,(%edi) // Toggle the IMMED bit.
ret
- defcode ";",1,F_IMMED,SEMICOLON
- movl $EXIT,%eax // EXIT is the final codeword in compiled words.
- call _COMMA // Store it.
- call _HIDDEN // Toggle the HIDDEN flag (unhides the new word).
- xor %eax,%eax // Set STATE to 0 (back to execute mode).
- movl %eax,var_STATE
+/*
+ HIDDEN toggles the other flag, F_HIDDEN, of the latest word. Note that words flagged
+ as hidden are defined but cannot be called, so this is rarely used.
+*/
+
+ defcode "HIDDEN",6,,HIDDEN
+ call _HIDDEN
NEXT
+_HIDDEN:
+ movl var_LATEST,%edi // LATEST word.
+ addl $4,%edi // Point to name/flags byte.
+ xorb $F_HIDDEN,(%edi) // Toggle the HIDDEN bit.
+ ret
+
+/*
+ ' (TICK) is a standard FORTH word which returns the codeword pointer of the next word.
+
+ The common usage is:
+
+ ' FOO ,
+
+ which appends the codeword of FOO to the current word we are defining (this only works in compiled code).
+
+ You tend to use ' in IMMEDIATE words. For example an alternate (and rather useless) way to define
+ a literal 2 might be:
+
+ : LIT2 IMMEDIATE
+ ' LIT , \ Appends LIT to the currently-being-defined word
+ 2 , \ Appends the number 2 to the currently-being-defined word
+ ;
+
+ So you could do:
+
+ : DOUBLE LIT2 * ;
-/* This definiton of ' (TICK) is strictly cheating - it also only works in compiled code. */
+ (If you don't understand how LIT2 works, then you should review the material about compiling words
+ and immediate mode).
+
+ This definition of ' uses a cheat which I copied from buzzard92. As a result it only works in
+ compiled code.
+*/
defcode "'",1,,TICK
lodsl // Get the address of the next word and skip it.
pushl %eax // Push it on the stack.
NEXT
+/*
+ BRANCHING ----------------------------------------------------------------------
+
+ It turns out that all you need in order to define looping constructs, IF-statements, etc.
+ are two primitives.
+
+ BRANCH is an unconditional branch. 0BRANCH is a conditional branch (it only branches if the
+ top of stack is zero).
+
+ This is how BRANCH works. When BRANCH executes, %esi starts by pointing to the offset:
+
+ +---------------------+-------+---- - - ---+------------+------------+---- - - - ----+------------+
+ | (Dictionary header) | DOCOL | | BRANCH | offset | (skipped) | word |
+ +---------------------+-------+---- - - ---+------------+-----|------+---- - - - ----+------------+
+ ^ | ^
+ | | |
+ | +-----------------------+
+ %esi added to offset
+
+ The offset is added to %esi to make the new %esi, and the result is that when NEXT runs, execution
+ continues at the branch target. Negative offsets work as expected.
+
+ 0BRANCH is the same except the branch happens conditionally.
+
+ Now standard FORTH words such as IF, THEN, ELSE, WHILE, REPEAT, etc. are implemented entirely
+ in FORTH. They are IMMEDIATE words which append various combinations of BRANCH or 0BRANCH
+ into the word currently being compiled.
+
+ As an example, code written like this:
+
+ condition-code IF true-part THEN rest-code
+
+ compiles to:
+
+ condition-code 0BRANCH OFFSET true-part rest-code
+ | ^
+ | |
+ +-------------+
+*/
+
+ defcode "BRANCH",6,,BRANCH
+ add (%esi),%esi // add the offset to the instruction pointer
+ NEXT
+
+ defcode "0BRANCH",7,,ZBRANCH
+ pop %eax
+ test %eax,%eax // top of stack is zero?
+ jz code_BRANCH // if so, jump back to the branch function above
+ lodsl // otherwise we need to skip the offset
+ NEXT
+
+/*
+ PRINTING STRINGS ----------------------------------------------------------------------
+
+ LITSTRING and EMITSTRING are primitives used to implement the ." operator (which is
+ written in FORTH). See the definition of that operator below.
+*/
+
+ defcode "LITSTRING",9,,LITSTRING
+ lodsl // get the length of the string
+ push %eax // push it on the stack
+ push %esi // push the address of the start of the string
+ addl %eax,%esi // skip past the string
+ addl $3,%esi // but round up to next 4 byte boundary
+ andl $~3,%esi
+ NEXT
+
+ defcode "EMITSTRING",10,,EMITSTRING
+ mov $1,%ebx // 1st param: stdout
+ pop %ecx // 2nd param: address of string
+ pop %edx // 3rd param: length of string
+ mov $__NR_write,%eax // write syscall
+ int $0x80
+ NEXT
+
+/*
+ COLD START AND INTERPRETER ----------------------------------------------------------------------
+
+ COLD is the first FORTH function called, almost immediately after the FORTH system "boots".
+
+ INTERPRETER is the FORTH interpreter ("toploop", "toplevel" or REPL might be a more accurate
+ description).
+*/
+
+
+ // COLD must not return (ie. must not call EXIT).
+ defword "COLD",4,,COLD
+ .int INTERPRETER // call the interpreter loop (never returns)
+ .int LIT,1,SYSEXIT // hmmm, but in case it does, exit(1).
+
/* This interpreter is pretty simple, but remember that in FORTH you can always override
* it later with a more powerful one!
*/
interpret_is_lit:
.int 0 // Flag used to record if reading a literal
+/*
+ ODDS AND ENDS ----------------------------------------------------------------------
+
+ CHAR puts the ASCII code of the first character of the following word on the stack. For example
+ CHAR A puts 65 on the stack.
+
+ SYSEXIT pops the status off the stack and exits the process (using Linux exit syscall).
+*/
+
+ defcode "CHAR",4,,CHAR
+ call _WORD // Returns %ecx = length, %edi = pointer to word.
+ xor %eax,%eax
+ movb (%edi),%al // Get the first character of the word.
+ push %eax // Push it onto the stack.
+ NEXT
+
// NB: SYSEXIT must be the last entry in the built-in dictionary.
defcode SYSEXIT,7,,SYSEXIT
pop %ebx
mov $__NR_exit,%eax
int $0x80
-/*----------------------------------------------------------------------
- * Input buffer & initial input.
- */
+/*
+ START OF FORTH CODE ----------------------------------------------------------------------
+
+ We've now reached the stage where the FORTH system is running and self-hosting. All further
+ words can be written as FORTH itself, including words like IF, THEN, .", etc which in most
+ languages would be considered rather fundamental.
+
+ As a kind of trick, I prefill the input buffer with the initial FORTH code. Once this code
+ has run (when we get to the "OK" prompt), this input buffer is reused for reading user input.
+
+ Some notes about the code:
+
+ \ (backslash) is the FORTH way to start a comment which goes up to the next newline. However
+ because this is a C-style string, I have to escape the backslash, which is why they appear as
+ \\ comment.
+
+ Similarly, any backslashes in the code are doubled, and " becomes \" (eg. the definition of ."
+ is written as : .\" ... ;)
+
+ I use indenting to show structure. The amount of whitespace has no meaning to FORTH however
+ except that you must use at least one whitespace character between words, and words themselves
+ cannot contain whitespace.
+
+ FORTH is case-sensitive. Use capslock!
+
+ Enjoy!
+*/
+
.data
.align 4096
buffer:
- // XXX gives 'Warning: unterminated string; newline inserted' messages which you can ignore
+ // Multi-line constant gives 'Warning: unterminated string; newline inserted' messages which you can ignore
.ascii "\
\\ Define some character constants
: '\\n' 10 ;
.int buffer
bufftop:
.int _initbufftop
+
+/* END OF jonesforth.S */