Implement a primitive stack machine and a ColorForth like language that I call TokenForth.
The CPU has 8-bit opcodes, a 16-bit ALU, 8-bit and 16-bit memory accesses, 16-bit data and return stacks. It implements the basic Forth instructions.
NOP
CALL
JP, JPZ
AND, OR, XOR, NOT, SR, SL, SRW, SLW
+, -, CARRY
<, <u
SWAP, OVER, DUP, NIP, ROT, -ROT, DROP, RDROP
>R, R>
@, !, c@, c!
PUSH8, PUSH
Only PUSH
and PUSH8
have immediate data.
Bit #7 of an opcode represents the return bit.
Other instructions can be added easily, since there are a lot of unused opcodes.
The CPU is implemented in file prim.py
, a very primitive assembler/disassembler
is implemented in primasm.py
.
A tokenizer (tokenizer.py
) parses the source code and converts it into a binary representation. This is compiled then by tokenforth.py
. Words in immediate mode are executed during compilation by the CPU.
The TokenForth language recognizes the following token types:
Defines a word (word as understood in Forth context). Definitions are prefixed with :
(colon).
Example:
:wordname
This is basically like a label in a "normal" assembly language.
Literals compile raw data and are prefixed with #
. There are two literal types, one for numbers, one for strings.
Examples:
#$ffff \ compiles 0xffff
#"string literal" \ compiles a counted string
Compilation tokens are compiled during compilation phase (...well, yes, it's true ;-) ).
Compiles code that pushes a number on stack.
Example:
:word 1 2 3 \ define word that pushes numbers 1, 2, 3 on stack
Compiles a call to a previously defined word.
Example:
:word1 1 2 + ; \ define word1 that adds two numbers and returns
:word2 word1 ; \ define word2 that calls word1 and returns
Compiles code that pushes the address of a definition on stack.
Example:
:word1 1 2 + ; \ define word1
:word2 'word1 ; \ define word2 that pushes address of word1 on stack
Immediates are not compiled during compilation phase, but executed immediately.
The syntax is like the compilation tokens, but immediate mode must be started with [
. Immediate mode can be ended by ]
.
:word1 1 [ 2 ] ; \ compile word that pushes 1 when executed.
During compilation of word1
, the compiler executes 2
resulting with a 2 on top of the data stack after the compilation. When word1
is executed, 1 is pushed onto the data stack.
:word1 1 [ word2 ] 2 ; \ executes word2 after compiling 1, but before compiling 2.
:word1 [ 'word2 ] 1 ; \ push address of word2 on stack before compiling 1.
Mnemonics are the names of the CPU instructions.
Buildins are tokens that are understood by the tokenizer, and will be translated somehow to CPU instructions.
Buildin | Description |
---|---|
; |
Return |
Semicolon (;
) is used to return from a call to a word and, for the time being, compiles to NOP.RET
.
:word1 1 2 and ; \ This is a comment
:word2 1 ( this is a comment ) 2 and ;
There is also a token for white space. This is ignored by the compiler. It is included to be able to reconstruct the source code from the token representation.
The initialization value of a variable is defined by a number literal. A definition is used to label the address of the variable in memory.
:myvar #1 \ #1 is a number literal
:inc 'myvar @ 1 + 'myvar ! \ read variable from memory, increment, and store back to memory
The following words are predefined:
1+ 1-
2dup 2drop
Looping words:
if else then
while repeat
do loop
All looping words have to be executed immediately.
while repeat
loops until the flag is 0. while
will consume the flag.
:test flag [ while ] flag [ repeat ] ;
do loop
loops n times. The counter is kept on the return stack.
This will execute 3 NOPs:
:test 3 [ do ] nop [ loop ] ;
not yet implemented
for next
works similar to a for loop in C. for
consumes stop and start values. The loop counter can be retrieved with i
.
This will print ABC on UART (tx
pops a value and send it via UART):
:test 3 0 [ for ] 65 i + tx [ next ] ;
:min ( n1 n2 -- n )
2dup < [ if ] drop [ else ] nip [ then ] ;
Hello World, for the time being implemented with "while repeat".
:msg "Hello, World!" ;
:count dup 1+ swap c@ ;
:tx $ffff c! ;
:type dup [ while ] 1- >r dup c@ tx 1+ r> dup [ repeat ] 2drop ;
[ msg count type ]