The Design and Implementation of Retro 10

Table of Contents

  1. Introducing Retro
  2. Ngaro, a Virtual Machine
  3. Retro's Implementation
  4. The Cross Compiler
  5. Writing Modules
  6. The Words
  7. Appendix 1: Internal Words
  8. Appendix 2: Ngaro Instruction Set
  9. Appendix 3: Cross Compiler Words
  10. Appendix 4: Building Retro


1. Introducing Retro

Retro is an implementation of the Forth language. It is minimalistic, but not totally minimal. In Retro you'll find influences from many sources. This is the tenth generation of Retro, and the fifth completely new implementation.

There is a long history behind Retro. Originally created in 1998 by Tom Novelli, it evolved from a 16-bit bootable Forth into a 32-bit bootable Forth that served as a prototype low level language for the Tunes project. A third incarnation, written using a custom assembler and metacompiler never fully worked, and the fourth was heavily influenced by colorForth though retaining the syntax of a classical Forth. When Tom ceased development, Charles Childers took over development and with help from many others brought the fourth generation code to many operating systems and significantly increased features. Along the way it spawned and influenced many other Forths.

The tenth generation is the first complete rewrite since Charles took over. In this incarnation, Retro is smaller and cleaner than before, yet still supports many of the features that were added in the previous generations.

The Retro language and implementation is continually evolving and changing with use. As much as possible this documentation will be kept in sync with the current code, though it may lag behind at times. If you find inaccuracies or have questions on specific areas that aren't clear here, please report them to Charles Childers (crc@retroforth.org).


2. Ngaro, a Virtual Machine

Rather than being written in x86 assembly language, Retro 10 was written to run on a custom virtual machine named Ngaro. This allows Retro to run on a wide variety of devices with minimal effort.

2.1 Processor and Memory Architecture

Ngaro emulates a MISC (minimal instruction set computing) processor. This provides two stacks, one for data and the other for addresses. There are instructions for reordering data on the data stack and moving values between the stacks. The MISC processor being emulated has 31 instructions (see Appendix 2). Encodings are trivial. Each instruction takes one memory location, with a few taking an additional value from the following memory location.

The memory is setup as simple as possible. Each memory location holds a single 32-bit value. Depending on the hardware being emulated, some memory will be used by the framebuffer.

2.2 Emulated Devices

Ngaro can emulate a number of I/O devices. Retro can detect the virtual hardware being emulated and adjusts the I/O words automatically.

A minimal Ngaro implementation will provide a console for output and keyboard for input. Some implementations provide a graphical console (as a framebuffer), but most only provide a traditional text console. The console device has a built in character generator to handle basic textual output. Additional devices may be provided, but should be treated with care as implementations are not required to provide them.

2.3 I/O Ports

The I/O devices are accessed through a set of I/O ports. The first 13 ports (0 - 12) are reserved for common functionality expected to be supported by all implementations. Ports above 12 can be claimed by implementers for custom devices.

2.3.1 Port 0

This is used to determine if Ngaro should enter a wait for hardware event loop. It should be set to 0, then use the wait instruction. In Retro, this is all done by the wait word.

2.3.2 Port 1

This is used to read a value from the keyboard. Wait, then read the key value from port 1. Example:

  wait 1 in

2.3.3 Port 2

Use the hardware character generator to display a character. This will take data off the stack, so you should make sure there are enough values on the stack before using it.

Example (for framebuffer):

  ( x y character )
  100 200 98
  1 2 out
  wait

Example (for console):

  ( character)
  98
  1 2 out
  wait

2.3.4 Port 3

This is used to force video updates. Ngaro implementations may cache output and only update the display periodically. If they do, then they should respond to this by immediately updating the display.

Example:

  0 3 out

2.3.5 Port 4

This port is used to save the image. Allowing saving of images is not required, but is strongly recommended if the underlying host platform will support it. To save, set port 4 to 1 and wait.

Example:

  1 4 out wait

2.3.6 Port 5

This port is a fairly new addition and is used to allow images to query the capabilities of the Ngaro implementation. It is a read/write port, you pass it a given value, wait, then read back from it to obtain the results of your query. The following queries are currently supported on the stable Ngaro implementations:

-1  Amount of memory being provided
-2  Address of framebuffer (0 if none)
-3  Width of framebuffer
-4  Height of framebuffer

2.4 Images

Ngaro works with memory image. An image corresponds exactly to what will be in memory while Ngaro is running. Some implementations of Ngaro allow for saving of memory images and reloading them later. The image file does not preserve any of the internal registers, so must be able to handle reloading as if it were being loaded for the first time.  After loading the image, the processor jumps to address 0 and execution proceeds from there.

2.5 Implementations

There are several implementations of Ngaro. These are mostly straight ports of the original implementation, but some differ significantly in the internal approach.

2.5.1 C

There are three implementations in C. The first two are closely related and differ only in the emulated devices. These were developed by Charles Childers and use a switch based processor. Both framebuffer and text console output is supported.

The third was developed by Matthias Schirm and uses a threaded processor. It emulates a text console. This is the fastest implementation of Ngaro.

2.5.2 JavaScript

A JavaScript implementation was developed using the original C implementation as a guide. It is also switch based, and emulates a text console. Given limitations with HTML as a front end, it uses forms and innerHTML to approximate the keyboard and text output areas.

The JavaScript implementation can not save images.

2.5.3 Java (experimental)

The JavaScript implementation was used to develop a Java implementation. This shares the same limitations and implementation approach, including the emulated interface.

2.5.4 Java 2 Micro Edition (J2ME) (experimental)

An implemention of Ngaro for J2ME has been developed, but is not officially supported yet. Work on integrating it into the main codebase is ongoing.

2.5.4 Perl (experimental, incomplete)

A Perl implementation is being developed, but is not yet complete. It's based on the original C implementation and emulates the text console device set.

3. Retro's Implementation

3.1 Threading Model

The threading model used in Retro is subroutine threading. This has been used successfully in Retro since the fourth generation, and allows for a fairly simple compiler and potential optimizations that make the code density tradeoffs worthwhile.

Subroutine threading works by compiling code to a series of native instructions, with calls to other words. For instance, given the following:

   : foo + . ;

Retro will compile:

   label: foo
     call +
     call .
     return

In the future this model will allow optimizations such as eliminating tail calls and inlining of small words for performance.

3.2 Word Classes

3.2.1 Introduction to Word Classes

Retro makes use of an implementation technique known as word classes. This approach was created by Helmar Wodtke and allows for a very clean interpreter and compiler. It makes use of special words, called class handlers, to process execution tokens. Each word in the dictionary has a class handler associated with it. When being executed, the address of the word is pushed to the stack and the class handler is invoked. The handler then does something with the address based on various bits of state.

3.2.2 Standard Classes

Class Name
Stack
Usage
.word
a --
At compile time, compile a call to the word. At interpret time, execute the word.
.macro
a --
Execute the word. The word may compile code or add data to the heap based on any form of state.
.data
a -- a
At compile time, lay down code to return the address of a data element. At interpret time, return the address of the data.


3.2.3 Adding New Classes

The easiest way to show how to add a new class is with an example. For this, we'll create a class for strings with the following behaviour:


Retro has a convention of using a . as the first character of a class name. We'll call our new class .string

On entry to a class handler, the address (xt) of the word or data element is on the stack. The compiler state (often important to class handlers) is stored in a variable named compiler. With this in mind, we'll define our class:

  : .string ( xt -- )
    compiler @ 0 =if type ;; then
    1 , , ['] type compile ;

The compile-time portion is a bit trickier than the interpret time since it has to lay down the proper code in the target definition. In this case a LIT instruction (opcode 1) is laid down, followed by the xt of the string. This is followed by code to lay down a call to type.

We now need a creator word to attach this class to a value. For this example we'll define displayString: to take a form like the one shown below.

  string-addr displayString: name

New dictionary entries are made using create, so we'll use that and change the class to our new .string handler.

  : displayString: ( string-addr -- )
    create ['] .string last @ d->class !
    keepString last @ d->xt ! ;

This uses create to make a new word, then sets the class to .string and the xt of the word to the string. It also makes the string permanent using keepString. last is a variable pointing to the most recently created dictionary entry. The words d->class and d->xt are dictionary field accessors and are used to provide portable access to fields in the dictionary.

We can now test the new class:

  " hello, world!" displayString: hello
  hello
  : foo hello cr ;   foo

You can use this approach to define as many classes as you want.

3.3 Vectors

The ninth generation of Retro introduced word vectors. This is a technique that allows for replacing existing words with new functionality. It is related to deferred words, which are common in the mainstream Forths, but is extended to allow words to have a default definition which can be restored later.

In Retro all non-data words are vectors. Vectors can be replaced by using is, or returned to their original definition with devector. For instance:

   : foo 23 . ;
   foo
   : bar 99 . ; ' bar is foo
   foo
   devector foo
   foo

There are also variations of is and devector which take the addresses of the words rather than parsing for the word name. These are :is and :devector. It is highly recommended that you become comfortable with manipulating vectors as this will allow you a much greater degree of control over the Retro system.

3.4 The Parser

In Retro, the parser  operates directly on the input stream, as it is being entered. This means that words are executed immediately upon hitting the space bar. In a normal build, only space marks separation between words. This behavior can be alterted either by loading the whitespace module into the Retro image, or by entering a replacement for (remap-keys) at runtime.

3.5 Dictionary Structure

The dictionary is a simple linked list. The structure of each node in this list is shown in the table below.

Location
Description
0
Holds the address of the previous entry. Set to 0 if there are no previous entries.
1
Holds the class identifier of the word.
2
Holds the address that the definition starts at.
3 .... n
String holding the name of the word. One character per cell, ending with a numeric value of 0.

New entries are made by create. The words : and macro: use create and then modify the class field as necessary.


4. The Cross Compiler

Retro is written in a custom language that combines an assembler for the Ngaro instruction set with a tiny subset of Forth syntax. It's essentially a limited machine forth dialect. The cross compiler generates both an image file and an image map providing addesses and symbolic names for each word and data structure.

The cross compiler is small, but consists of many words. The full list of words is provided in Appendix 3. There is a standard form for applications written using the cross compiler:

   begin imagename
      ... support code + data ...
   main:
      ... main entry point ...
   end

Code is built on previous code; the cross compiler does not support forward references. When begin is encountered, the compiler lays down a jump instruction, which is later patched to the address of main:. The end word lays down code to cause Ngaro to cease execution of the image, and saves the image to disk, removing any unused cells that follow end from the image size.

The cross compiler has some additional words that make development of Retro easier. These setup the initial dictionary, words to call upon startup, and perform some sanity checks to help catch bugs. When using these, the form changes to:

   begin imagename
      mark-dictionary variable last
      mark-init-chain variable initchain

      ... support code + data ...

     address word: retroname
     address macro: retroname
     address data: retroname

     address +init

     patch-init-chain
     patch-dictionary

   main:
      ... main entry point ...
   cross-summary
   end

In this, note mark-dictionary, word:, macro:, data:, and patch-dictionary. These are used to build the initial dictionary. The word:, macro:, and data: each take an address from the stack and parse for a name to add to the dictionary.

The use of mark-init-chain, +init, and patch-init-chain is used by Retro to support creation of a list of addresses to execute upon startup. This allows for modules to replace core functionality when the Retro image is loaded.

cross-summary displays some statistics and also performs a check to ensure that the stack is balanced to 0 entries. The cross compiler assumes that the stack will start and end empty; anything else implies bugs in the code being compiled.


5. Writing Modules

Retro, while minimalistic in nature, allows for expansion of the core functionality through the use of modules. Most modules are written to be loaded into the image during the build process, but is is also possible to load some modules into an existing image.

When building Retro, the core loads a file called extensions. This file contains a list of all existing modules which can be included in the image. To use a module, simply uncomment (remove the #!) the line before building. Any modules you write should be placed in rdev/retro/modules, and be added to extensions.

The standard layout for compile-time modules is:

   MODULE: name of the module
   AUTHOR: name of the developer(s) of the module
   NOTES:  some text describing the module and it's functionality.
   NOTES:  there can be multiple NOTES: lines.

   #! ----------------------------------------------------------

      ... code + data ...

   #! ----------------------------------------------------------

     
address +init

      address word: retroname
      address macro: retroname
      address data: retroname

When mapping words into the dictionary, you should start with normal words, followed by macros, and finally list the data words. You do not have to make any module words visible, nor do you have to use +init. You should use +init if your module needs custom initialization code to be run upon loading the image.

This structure is not forced, but modules should follow this fairly closely if you want them included in the standard distribution. Consistency helps ensure that they are maintainable.

Compile-time modules have full access to all internal words (See Appendix 1) and thus can make significant changes to Retro's functionality. The only limit on functionality and size is this: Retro has a maximum core size of 30k cells. All compile-time modules and data must fit in this.

You can load code into an existing image using a manual process. This assumes that you are running on some form of Unix. It's pretty simple:

   ./build
   cd bin
   cat filename | ./ngaro forth.image


There is a limiting factor in doing this. The standard Retro image only recognizes space as valid whitespace, so any use of tabs or newlines will cause problems. This can be fixed by building an image with the whitespace module included, or by having your code start with the following line. This should be one single line. Be sure to include a space at the end of the line:

   : fix dup 10 =if drop 32 then dup 13 =if drop 32 then
         dup 9 =if drop 32 then ; ' fix is (remap-keys)

If you want, this can be disabled later by doing:

   devector (remap-keys)

Make sure to end your file with save, followed by bye. Assuming there are no serious bugs, your changes will be present the next time you load your image.


6. The Words

Retro has many words. A large number of them are used internally; these are discussed in Appendix 1. All words accessible from the interpreter are shown in the table below. Stack behaviors are for runtime, not compile time.

Name
Data Stack
Address Stack
Description
here
-- a
--
Return the address of the top of the heap
,
n -- -- Write a cell to the top of the heap; increase heap pointer.
]
-- -- Turn compiler on
create
"name" -- -- Create a new dictionary entry with a class of data.
:
"name" -- -- Create a new dictionary entry with a class of word and call ].
macro
"name" -- -- Create a new dictionary entry with a class of macro and call ].
cr
-- -- Move the text cursor to the start of the next line
emit
n -- -- Display a character
home
-- -- Position the text cursor to the top left corner of the screen
type
$ptr -- -- Display a string
words
-- -- Display a list of all named words
save
-- -- Save the image
clear
-- -- Clear the display
key
-- n
-- Read a value from the keyboard
accept
delim -- -- Read a string delimited by delim into the tib.
dup
n -- n n
-- Duplicates TOS
1+
x --y
-- Increment TOS
1-
x -- y
-- Decrement TOS
swap
x y -- y x
-- Exchange TOS and NOS
drop
x y -- x
-- Drop TOS
and
x y -- z
-- Bitwise AND
or
x y -- z
-- Bitwise OR
xor
x y -- z
-- Bitwise XOR
@
a -- n
-- Fetch a value from address
!
n a -- -- Store a value to address
+
x y -- z
-- Add two numbers
-
x y -- z
-- Subtract two numbers
*
x y -- z
-- Multiply two numbers
/mod
x y -- z q -- Divide and get remainder
<<
x y -- z
-- Bitwise left shift
>>
x y -- z
-- Bitwise right shift
nip
x y -- y
-- Drop NOS
over
x y -- x y x
-- Put a copy of NOS on top of the stack
2drop
x y -- -- Drop TOS and NOS
not
x -- y
-- Same as -1 xor.
rot
x y z -- y z x
-- Rotate top three items on the stack
-rot
x y z -- z x y
-- Rotate top three items on stack twice
tuck
x y -- y x y
-- Put a copy of TOS under NOS
2dup
x y -- x y x y
-- Duplicate both TOS and NOS
on
a -- -- Set an address to -1
off
a -- -- Set an address to 0
/
x y -- z
-- Divide two numbers
mod
x y -- z
-- Divide and get remainder only
neg
x -- y
-- Invert the sign of a number
execute
a -- -- a
Call a function at address
.
n -- -- Display a number
"
"string" -- a
-- Parse for a string
compare
$ptr $ptr -- flag
-- Compare two strings
in
p -- n
-- Read a value from a port
out
n p -- -- Write a value to a port
wait
-- -- Wait for a hardware event
'
"name" -- a
-- Get the address of a named word
@+
a -- a+1 n -- Fetch a value from address a, return the value n and increment the address by 1
!+
n a -- a+1 -- Store n into address a, increment the address by 1
+!
n a -- -- Add n to the value at address a, storing the results in a.
-!
n a -- -- Subtract n from the value at address a, storing the results in a.
:is
a xt -- -- Assign vector at xt to address a
:devector
xt -- -- Remove the vector from xt
is
a "name" -- -- Set vector name to address a.
devector
"name" -- -- Restore a vector to its default state.
compile
a -- -- Compile a call to address
literal,
n -- -- Compile a value as a literal
redraw
--
--
Update the display
getLength
$ptr -- n
-- Get the length of a string
tempString
$ptr -- $ptr2
-- Mark a string as temporary
keepString
$ptr -- $ptr2
-- Mark a string as permanent
bye
-- -- Shutdown Ngaro VM
(remap-keys)
n -- n
-- Vector which allows replacing one key value with another.
.word
a --
--
Class handler for normal words. (Class id = 1)
.macro
a --
--
Class handler for macros. (Class id = 2)
.data
a --
--
Class handler for data structures. (Class id = 3)
with-class
a n --
--
Class dispatcher. N is the class id.
boot
--
--
Hook for custom startup code for turnkey applications.
s"
"string" -- a
-- Compile a string into a definition
[
-- -- Turn the compiler off
;
-- -- End a definition
;;
-- -- Compile an exit point into a definition
=if
x y -- -- Compare two number for equality. Jump to then if condition not met.
>if
x y -- -- Compare two number for greater than. Jump to then if condition not met.
<if
x y -- -- Compare two number for less than. Jump to then if condition not met.
!if
x y -- -- Compare two number for inequality. Jump to then if condition not met.
then
-- -- End a conditional
repeat
-- -- Begin an unconditional loop
again
-- -- Close an unconditional loop. Branch back to previous repeat.
0;
n --
n -- n
-- Exit a word if TOS is 0. If 0, drops TOS. Otherwise leaves TOS alone.
(
"... )"-- -- Start a comment.
push
n -- -- n
Push TOS to TORS.
pop
-- n
n --
Pop TORS back to TOS
vector
-- -- Make a word a vector. Must be the first word in the definition.
for
n -- -- a
Start a counted loop
next
-- n
--
a --
End a counted loop. Decreases counter by 1. If counter is 0, exit the loop. Otherwise jump back to for. Drops the counter on exiting.
[']
"name" -- a
-- Get the address of a word.
d->class
d -- a
--
Dictionary: Get class slot
d->xt
d -- a
--
Dictionary: Get XT slot
d->name
d -- a
--
Dictionary: Get name slot
tx
-- a
-- Text X coordinate (graphics mode)
ty
-- a
-- Text Y coordinate (graphics mode)
last
-- a
-- Pointer to the most recent dictionary entry
compiler
-- a
-- Holds compiler state
tib
-- a
-- Pointer to the text input buffer
update
-- a
-- Variable telling Retro to force a screen update.
fb
-- a
--
Address of framebuffer
fw
-- a
--
Width of framebuffer
fh
-- a
--
Height of framebuffer
#mem
-- a
--
Amount of memory



Appendix 1: Internal Words

In addition to the visible words, there are a large number of internal words which are visible only during the build process. The table below lists all of these words, and provides some descriptions and stack usage for them. All words in this table are vectors and can be replaced with alternatives by modules. Words in bold are visible in the finished image, but may have different naming there.

Word
Stack
Description
last
-- a
Pointer to the most recent dictionary entry
init-chain
-- a
Pointer to the most recent entry in the initialization chain
dup
x -- x x
Duplicates TOS
1+
x -- y
Increment TOS
1-
x -- y
Decrement TOS
swap
x  y -- y x
Exchange TOS and NOS
drop
x -- Drop TOS
and
x y -- z
Bitwise AND
or
x y -- z
Bitwise OR
xor
x y -- z
Bitwise XOR
@
a -- n
Fetch data from memory
!
n a -- Store data to memory
+
x y -- z
Add
-
x y -- z
Subtract
*
x y -- z
Multiply
/mod
x y -- z n
Divide and get remainder
<<
x y -- z
Bitwise left shift
>>
x y -- z
Bitwise right shift
out
x y -- Send a value to a port
in
x -- n
Read a value from a port
wait
-- Wait for an input event
nip
x y -- y
Drop the NOS
over
x y -- x y x
Put a copy of NOS on the top of the stack
2drop
x y -- Drop TOS and NOS
rot
x y z -- y z x
Rotate the top three values on the stack
-rot
x y z -- z x y
Rotate the top three values on the stack twice
tuck
x y -- y x y
Put a copy of TOS under the NOS
2dup
x y -- x y x y
Duplicate TOS and NOS
on
a -- Turn a variable on (set to -1)
off
a -- Turn a variable off (set to 0)
/
x y -- z
Divide
mod
x y -- z
Get remainder only
neg
x -- y
Negate TOS
execute
a -- Execute the code at address a
@+
a -- a+1 n
Fetch a value from address a, return the value n and increment the address by 1
!+
n a -- a+1
Store n into address a, increment the address by 1
+!
n a --
Add n to the value at address a, storing the results in a.
-!
n a --
Subtract n from the value at address a, storing the results in a.
tx
-- a
Text X coordinate (for framebuffer)
ty
-- a
Text Y coordinate (for framebuffer)
fb
-- a
Holds the address of the framebuffer
update
-- a
Enable/Disable video update (used by redraw)
redraw
-- Force video update (for framebuffer)
fb:cr
-- Move cursor to the next line (for framebuffer)
move
-- Move the cursor by one character (for framebuffer)
fb:emit
n -- Display a character (for framebuffer)
fb:home
-- Reset TX and TY to top-left corner of the display. This is used by fb:clear
(type)
a0 -- a1
Internal loop for type
type
a -- Display a string
(clear)
-- Internal loop for fb:clear (for framebuffer)
fb:clear
-- Clear the screen (for framebuffer)
tty:emit
c --
Display a character (for tty)
tty:cr
--
Move the cursor to the next line (for tty)
tty:home
--
Move the cursor to the top-left corner of the display (for tty)
tty:clear
--
Clear the screen (for tty)
emit
c --
Display a character
cr
--
Move the cursor to the next line
clear
--
Clear the display
home
--
Move the cursor to the top-left corner of the display
save
-- Save the image
bye
--
Shutdown RETRO
words
-- Display a list of all defined words
tib
-- a
Text Input Buffer
>in
-- a
Pointer into the tib
break-char
-- a
Holds the character used as a delimiter by accept.
(remap-keys)
--
Hook to remap key values before they can be processed. Called by key right before returning the value.
key
-- n
Read a single keypress
>tib
n --
Add a character to the tib
++
-- Increment >in
(eat-leading)
-- Eat leading keystrokes matching break-char. This is used by accept to ignore extra spaces at the start of a token.
(accept)
-- c
Internal loop used by accept.
accept
c --
Read a string delimited by c. Modifies break-char.
state
-- a
Compiler state (can be either on or off)
heap
-- a
Current memory location to compile code and/or data to
compiling?
--
Exit the word if the compiler state is true.
t-here
-- a
Return the current memory location
t-, n -- Compile a value into the current memory location
t-] -- Start the compiler
t-[ -- Suspend the compiler
t-;; -- Compile a ; instruction
t-; -- Compile a ; instruction and stop compiling
($,)
a0 -- a1
Internal loop used by $
$
a -- Compile a string into the definitions
create
"name" -- Create a new dictionary entry
(:)
--
Common code for t-: and t-macro:
t-: "name" -- Start a new definition (word class)
t-macro: "name" -- Start a new definition (macro class)
t-( "comment)" -- Start a comment
t-push n -- Compile a push instruction
t-pop -- n
Compile a pop instruction
$flag
-- a
Flag used by various string words
n=n
x y -- Sets $flag to 0 if the values do not match
get-set
a0 a1 -- x y
Get values from two strings
next-set
a0 a1 -- a2 a3
Increment two string pointers
(skim)
a0 a1 -- a2 a3
Internal loop used by compare
compare
a1 a2 -- f
Compare two strings
(strlen)
-- Internal loop used by getLength
getLength
-- Get the length of a string
STRINGS
-- a
Starting address for the permanent string buffer
SAFE
-- a
Used during creation of temporary strings. On invoking " this points to the temporary string buffer. It is incremented for each character in the string.
LATEST
-- a
Start address of the most recent permanent string
(reset-$)
--
Reset SAFE to the start of the temporary string buffer
(next)
--
Increment SAFE
(copy)
a0 -- a1
Copy a string into the temporary string buffer.
tempString
a0 -- a1
Move a string to the temporary buffer
keepString
a0 -- a1
Move a string to the permanent string buffer
t-"
"text" -- a
Get a string (interpret)
t-s"
"text" -- a
Get a string (compiler) and compile a pointer to it into the current defintion
#value
-- a
Internal value used for numeric words
#flag
-- a
Internal value used for numeric words
num
-- a
Internal value used for numeric words
num-ok
-- a
Internal value used for numeric words
negate?
-- a
Internal value used for numeric words
isdigit?
c -- f
Returns true if the character is a number. False if not.
char>digit
c -- n
Convert an ASCII character to a number
digit>char
n -- c
Convert a number to an ASCII character
isNegative?
a -- a+1
Sets negate? to true if the number starts with a - character
(convert)
a0 -- a1
Internal loop used by >number
>number
a -- n
Convert a string to a number
(isnumber)
a0 -- a1
Internal loop used by isnumber?
isnumber?
a -- f
Returns true if string a is a valid number.
number>digits
n -- c0 ... cN
Break a number into individual characters. Used by .
digits>screen
c0 ... cN -- Display a sequence of characters. Used by .
.
n -- Display a number
found
-- a
Search result flag
which
-- a
Pointer to the found dictionary header
d->class
d -- a
Dictionary: Get Class slot
d->xt
d -- a
Dictionary: Get XT slot
d->name
d -- a
Dictionary: Get Name slot
(search)
-- Internal loop used by search
search
a -- Search for a word in the dictionary.
t-=if x y -- Compile a !jump instruction
t->if x y -- Compile a <jump instruction
t-<if x y -- Compile a >jump instruction
t-!if x y -- Compile a =jump instruction
t-then -- Patch the last conditional jump
t-repeat -- Start an unconditional loop
t-again -- Close an unconditional loop
t-0; n -- n || n --
Compile a 0; instruction. Leaves n alone if n is non-zero, otherwise drops n.
t-'
"name" -- a
Return the address of a word. If the word is not found, display an error and return 0.
t-[']
"name" -- a
Get the address of a word and compile it into the current definition.
devector
"name" --
Remve the vector from name
is
a "name" --
Assign the vector named name to address a
:devector
xt --
Remove the vector from xt
:is
a xt --
Assign vector at xt to address a
compile
xt --
Compile a call to xt into the current definition
literal,
n --
Compile n into the current definition as a literal
cold
-- a
Variable determining whether or not to display the copytag.
copytag
-- a
Copyright / Bootup message. Used only by runonce.
boot
--
Hook for turnkey applications.
runonce
-- Runs when the image is loaded. Performs initializations, then calls boot.
nomatch
-- a
String to display when no match could be found. Used by notfound.
okmsg
-- a
String to display as the "ok" prompt
.word
a -- Word class handler
.macro
a -- Macro class handler
.data
n -- Data class handler
with-class
a -- Execute an XT with the proper class handler
notfound
-- Called when a lookup fails. Will display the nomatch string.
find-word
-- Search for a word in the dictionary. Used by listen.
find-number
-- Attempt to convert a token to a number. Used by listen.
ok
-- Display the ok prompt if not compiling. Used by listen.
listen
--
The main interpreter loop
initialize
--
Run each word in the initialization chain once
fw
-- a
Height of framebuffer
fh
-- a
Width of framebuffer
#mem
-- a
Amount of memory


Appendix 2: Ngaro Instruction Set

The MISC processor emulated by Ngaro has 31 instructions. Instruction encodings are trivial: one instruction per cell. A few (lit, call, and the branch forms) take an additional value from the following cell. All opcodes listed here are in decimal.

Opcode
Name
Data Stack
Address Stack
Description
0
NOP
--
--
Does nothing. Used for padding
1 |value|
LIT
-- n
-- Push a literal to the stack
2
DUP
n -- n n
-- Duplicate the top value on the stack
3
DROP
n -- -- Drop the top value off the stack
4
SWAP
x y -- y x
-- Exchange the top and second item on the stack
5
PUSH
n -- -- n
Push the top item on the data stack to the address stack
6
POP
-- n
n -- Pop the top item on the address stack to the data stack
7 |address|
CALL
-- -- a
Call a subroutine
8 |address|
JUMP
-- -- Branch unconditionally to a memory location
9
;
-- a -- Return from a subroutine
10 |address|
>JUMP
-- -- Conditional branch, if NOS is greater than TOS
11 |address| <JUMP
-- -- Conditional branch, if NOS is less than TOS
12 |address| !JUMP
-- -- Conditional branch, if NOS is not equal to TOS
13 |address| =JUMP
-- -- Conditional branch, if NOS is equal to TOS
14
@
a -- n
-- Fetch the value at the memory address in TOS
15
!
n a -- -- Store the value on the second stack location to the address in TOS
16
+
x y -- z
-- Add the top two values on the stack
17
-
x y -- z
-- Subtract the top two values on the stack
18
*
x y -- z
-- Multiply the top two values on the stack
19
/MOD
x y -- z q
-- Divide and get the remainder of the top two values on the stack
20
AND
x y -- z
-- Bitwise AND operation
21
OR
x y -- z
-- Bitwise OR operation
22
XOR
x y -- z
-- Bitwise XOR operation
23
<<
x y -- z
-- Shift bits left
24
>>
x y -- z
-- Shift bits right
25
0;
n -- n
n --
-- Exit a subroutine and drop TOS if TOS is 0. If TOS is not 0, do nothing.
26
1+
x -- y
-- Increase the value on the stack by 1
27
1-
x -- y
-- Decrease the value on the stack by 1
28
IN
p -- n
-- Read a value from a port
29
OUT
n p -- -- Send a value to a port
30
WAIT
-- -- Wait for the hardware to process an event.


Appendix 3: Cross Compiler Words

The cross compiler contains an assembler for the Ngaro instruction set, and some helper words that provide a simple machine forth implementation. These words are shown in the table below.

Word
Stack
Description
heap
-- a
Pointer into the target memory
origin
-- a
Address of the target memory
fid
-- n
File ID
log
-- n
Memory Map / Build Log File ID
lastop
-- n
Last compiled opcode (for optimizer)
$log
"string" --
Append "string" to the map/log file
\n
--
Append a newline to the map/log file
,
n --
Compile a value into the target memory
vm:
n "name" --
Create a new word that compiles its value into target memory
here
-- a
Return the value of the current location in the target memory.
nop,
--
Ngaro Instruction
lit,
--
Ngaro Instruction
dup,
--
Ngaro Instruction
drop,
--
Ngaro Instruction
swap,
--
Ngaro Instruction
push,
--
Ngaro Instruction
pop,
--
Ngaro Instruction
call,
--
Ngaro Instruction
jump,
--
Ngaro Instruction
;
--
Ngaro Instruction
>jump,
--
Ngaro Instruction
<jump,
--
Ngaro Instruction
!jump,
--
Ngaro Instruction
=jump,
--
Ngaro Instruction
@,
--
Ngaro Instruction
!,
--
Ngaro Instruction
+,
--
Ngaro Instruction
-,
--
Ngaro Instruction
*,
--
Ngaro Instruction
/mod,
--
Ngaro Instruction
and,
--
Ngaro Instruction
or,
--
Ngaro Instruction
xor,
--
Ngaro Instruction
<<,
--
Ngaro Instruction
>>,
--
Ngaro Instruction
0;
--
Ngaro Instruction
1+,
--
Ngaro Instruction
1-,
--
Ngaro Instruction
in,
--
Ngaro Instruction
out,
--
Ngaro Instruction
wait,
--
Ngaro Instruction
halt,
--
Bogus instruction which causes Ngaro to cease execution of the image.
begin
"name" --
Begin compilation of target image
end
--
End compilation of target image
main:
--
Main entry point in target
label:
"label" --
Create a label
#
n --
Compile a number into the image (lit, followed by a value)
$,
string-ptr --
Compile a string into the image
conditional
-- a
Common code for conditionals
=if
--
Conditional (equality)
<if
--
Conditional (less than)
>if
--
Conditional (greater than)
!if
--
Conditional (inequality)
then
a --
Close the previous conditional
:
"name" --
Begin a new word in the target image
'
"name" --
Get the address of a word in the target image. Compiles the address into the target image.
link
-- n
Link to the latest entry in the target dictionary
#entries
-- n
Number of entries in the target dictionary
word:
a "name" --
Create a new word in the target dictionary using the word class
macro:
a "name" --
Create a new word in the target dictionary using the macro class
data:
a "name" --
Create a new word in the target dictionary using the data class
repeat
-- a
Begin an unconditional loop
again
a --
End an unconditional loop
variable:
n "name" --
Create a variable with an initial value of n
variable
"name" --
Create a variable with an initial value of 0
patch-dictionary
--
Adjust the marked memory location to point to the most recent dictionary entry. This should come after all of the initial entries are created.
mark-dictionary
--
Mark the current location in the target image as the pointer to the most recent dictionary entry.
init-link
-- n
Link to the latest entry in the initialization chain
mark-init-chain
--
Mark the current location in the target image as the pointer to the most recent item in the initialization chain.
patch-init-chain
--
Adjust the marked memory location to point to the most recent entry in the initialization chain. This should come after all items have been added to the chain.
+init
a --
Add the word at address a to the initialization chain
TCE
-- n
Number of tail calls eliminated
+TCE
--
Increment the number of tail calls elimintated
;
--
Extend the ; instruction to eliminate tail calls.
cross-summary
--
Display a summary of the cross-compilation results. This does some very basic sanity checks (stack depth, etc)
MODULE:
"name" --
Specify the name of a module
AUTHOR:
"name" --
Specify the author of a module
NOTES:
"text" --
Provide notes about a module


Appendix 4: Building Retro

A4.1 Building on a Unix-like Platform

Retro should be buildable on most Unix-like systems with minimal effort.

Prerequisites:


Optional:


Preparation:


Decompress the RETRO package (rdev-YYYYMMDD.tar.gz).

zcat retro-YYYYMMDD.tar.gz | tar xv

Building:

Once the package has been decompressed, building it is easy:

cd retro-YYYYMMDD
./build

The build script will display a menu of targets that can be built. Make a selection and press enter.

A4.2 Building on Windows

Building Retro on Windows is possible, but significantly more involved than building on a Unix-like system. The instructions here show how to build Ngaro. Building a copy of the Retro image (forth.image) is not yet documented on Windows.


Prerequisites:


Process:


  1. Install Dev C++
  2. Unzip the SDL runtime and copy SDL.dll to C:\WINDOWS
    (This will allow the library to be used system wide)
  3. Unzip the SDL development libraries and copy the contents of i386-mingw32msvc\include\SDL to C:\Dev-C++\Include
  4. Copy the libraries from the i386-mingw32msvc\lib to C:\Dev-C++\Lib
  5. Create a new empty Dev C++ project and add the following files from retro-YYYYMMDD\ngaro\c-crc\sdl to it:
    1. endian.c
    2. functions.h
    3. loader.c
    4. ngaro_windows.c
    5. sdl_devices.c
    6. vm.c
    7. vm.h
  6. Go to Project -> Options and add the following to the Compiler commands text box:
    -DUSE_SDL
  7. Go to Project -> Options and add the following to the Linker commands:
    -lmingw32 -lSDLmain -lSDL -mwindows
  8. Compile everything