FossilOrigin-Name: 0755da266c5fe041731ee32aeee5755f40a601085e8e8b28f7548d8d7b50066c
9.9 KiB
======================== An Introduction to RETRO
Getting Started
Building the VM
RETRO runs on a virtual machine. This has been implemented in many languages, and allows easy portability to most platforms. The primary implementation is in C.
Building it on FreeBSD, Linux or macOS is just a matter of running:
make
It'll build with the standard C compilers (tested with gcc and clang),
and requires a standard make
(tested with BSD and GNU variants).
When done you will have a few files of interest:
ngaImage Contains the RETRO system as a memory image
bin/repl Interactive interface, requires ngaImage in
the current directory
bin/rre Batch/Scripting interface; this is self-
contained and embeds the ngaImage into itself
during the build process
I symlink the bin/rre
to a location in my $PATH so I can refer to
it from anywhere in my environment.
Basic Interactions with the REPL
When you start RETRO via bin/repl
, you should see something like
the following:
RETRO 12 (rx-2017.11)
8388608 MAX, TIB @ 1025, Heap @ 9374
At this point you are at the listener, which reads and processes your input. You are now set to begin exploring RETRO.
To exit, run bye
:
bye
Basic Interactions with RRE
rre
(short for run retro and exit) is my preferred interface. It
takes a filename and runs the code in the file. E.g.,
rre example/99Bottles.forth
Source files for rre
are written in a literate format, with code
stored in Markdown-style fenced blocks. E.g.,
Define a word that returns the cube of a number.
~~~
:n:cube (n-n) dup dup * * ;
~~~
rre
adds significant features to the base language, including
support for keyboard input, file i/o, fetching resources via
gopher, and floating point support.
Exploring the Language
Names And Numbers
At a fundamental level, the RETRO language consists of whitespace delimited tokens.
The interpreter takes a look at the first character of the token. If
this matches a known prefix, the rest of the token is passed to a
function which handles that group of tokens. If no valid prefix is
found, RETRO tries to find the word in the dictionary. If successful
the information in the dictionary header is used to carry out the
actions specified in the name's definition. If this also fails,
RETRO calls err:notfound
to report the error.
A flowchart of the interpreter process:
+-------------+ +------------------------------------+
| Get a Token | ==> | Is first character a valid prefix? |
+-------------+ +------------------------------------+
| |
YES NO
| |
+-----------------------+ +---------------------------+
| Pass rest of token to | | Is entire token a name in |
| prefix handler | | the dictionary? |
+-----------------------+ +---------------------------+
| |
YES NO
| |
+------------------+ +-------------------+
| Push XT to stack | | Call err:notfound |
+------------------+ +-------------------+
|
+--------------------+
| Call class handler |
+--------------------+
In RETRO, the prefix handlers and class handlers are responsible for dealing with the tokens and words. This includes both interpret and compilation as necessary.
RETRO permits names to contain any characters other than space, tab, cr, dnd lf. Names are case sensitive, so the following are three different names from RETRO's perspective:
foo Foo FOO
Note that a name should not start with a prefix as prefixes are checked prior to dictionary lookups.
The Compiler
To create new functions, you use the compiler. This is generally
started by using the :
(pronounced colon) prefix. A simple
example:
:foo #1 #2 + putn ;
Breaking this apart:
:foo
RETRO sees that :
is a prefix and calls the handler for it. The
handler creates a new word named foo and starts the compiler.
#1
RETRO sees that #
is a prefix and calls the handler for it. The
handler converts the token to a number and then pushes the value to
the stack. Since the Compiler
is now active, it then pops the
value off the stack and compiles it into the current definition.
#2
And again, but compile a 2 instead of a 1.
RETRO does not find a +
prefix, so it searches the dictionary.
Upon finding +
, it pushes the address (xt) to the stack and
calls the corresponding class handler. The class handler for
normal words calls the code at the address if interpreting, or
compiles a call to it if the Compiler
is active.
putn
The process is repeated for putn
.
;
The last word has a slight difference. Like +
and putn
, this
is a word, not a prefixed token. But the class handler for this
one always calls the associated code. In this case, ;
is the
word which ends a definition and turns off the Compiler
.
Hyperstatic Global Environment
This now brings up an interesting subpoint. RETRO provides what is sometimes called a hyper-static global environment. This can be difficult to explain, so let's take a quick look at how it works:
:scale (x-y) A fetch * ;
>>> A ?
#1000 'A var<n>
:scale (x-y) A fetch * ;
#3 scale putn
>>> 3000
#100 A store
#3 scale putn
>>> 300
#5 'A var<n>
#3 scale putn
>>> 300
A fetch putn
>>> 5
Output is marked with >>>.
Note that we create two variables with the same name (A). The definition for scale still refers to the old variable, even though we can no longer directly manipulate it.
In a hyper-static global environment, functions continue to refer to the variables and functions that existed when they were defined. If you create a new variable or function with the same name as an existing one, it only affects future code.
Take advantage of this to reuse short names whenever it helps to make your code easier to understand.
Classes
Getting back to function creation, it's time for a clarification: in RETRO, the interpreter is unaware of how to handle a dictionary entry and has no concept of the difference between compiling and interpreting.
The actual work is handled by something we call class handlers.
Each dictionary header contains a variety of information:
+--------+------------------+
| Offset | Description |
+========+==================+
| 0 | link to previous |
+--------+------------------+
| 1 | class handler |
+--------+------------------+
| 2 | xt |
+--------+------------------+
| 3+ | name of function |
+--------+------------------+
When a token is found, the listener pushes the contents of the xt field to the stack and then calls the function that the class handler field points to.
This model differs from Forth (and most other languages) in that the interpreter is unaware of how tokens are handled. All actions are performed by the class handlers, which allows for easy addition of new categories of words and functionality.
Data Structures
You can create named data structures using d:create
, var
, var<n>
,
and const
.
Constants
These are the simplest data structure. The xt is set to a value, which is either left on the stack or compiled into a definition.
#100 'ONE-HUNDRED const
By convention, constants in RETRO should have names in all uppercase.
Variables
A variable is a named pointer to a memory location holding a value that may change over time. RETRO provides two ways to create a variable:
'A var
The first, using var
, creates a name and allocates one cell for storage.
The memory is initialized to zero.
#10 'B var<n>
The second, var<n>
, takes a value from the stack, and creates a name,
allocates one cell for storage, and then initializes it to the value
specified.
This is cleaner than doing:
'A var
#10 &A store
Custom Structures
You can also create custom data structures by creating a name, and allocating space yourself. For instance:
'Test d:creat
#10 , #20 , #30 ,
This would create a data structure named Test
, with three values,
initialized to 10, 20, and 30. The values would be stored in
consecutive memory locations.
If you want to allocate a buffer, you could use allot
here:
'Buffer d:create
#2048 allot
The use of allot
reserves space, but does not initialize the space.
Strings
In addition to the basic data structures above, RETRO also provides support for string data.
Creating a string simply requires using the '
prefix:
'this_is_a_string
'__this_string_has_leading_and_trailing_spaces__
When creating strings, RETRO uses a floating, rotating buffer for temporary strings. Strings created in a definition are considered permanent.
Note the use of underscores in place of spaces. Since strings are handled by a prefix handler, the token can not contain whitespace characters. RETRO will convert the underscores into spaces by default.
You can obtain the length of a string using s:length
:
'this_is_a_string s:length
Comparisons
Strings can be compared using s:eq?
:
'test_1 'test_2 s:eq? putn
0 'test_3 'test_3 s:eq? putn -1
The comparisons are case sensitive.
============= To Be Continued ...