retroforth/doc/An_Introduction_To_Retro.md
crc 719c5ceef8 start work on an introduction to the language
FossilOrigin-Name: 0755da266c5fe041731ee32aeee5755f40a601085e8e8b28f7548d8d7b50066c
2017-10-22 02:14:48 +00:00

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 ...