proj-plbook-plChConcatenativeLangs

Difference between revision 15 and current revision

No diff available.

Concatenative Languages

A concatenative programming language is a point-free language in which all expressions are functions, and the concatenation of two programs denotes the composition of the functions denoted by the two programs. -- paraphrasing https://en.wikipedia.org/wiki/Concatenative_programming_language and http://web.archive.org/web/20111007025556/http://www.latrobe.edu.au/phimvt/joy/j02maf.html

In this chapter, we may also include languages that are similar to concatenative languages but are not actually concatenative.

Forth

features

local variables

Some Forth implementations have local variables.

" In some environments there may be more or fewer functions assigned to the call stack. In the Forth programming language, for example, ordinarily only the return address, counted loop parameters and indexes, and possibly local variables are stored on the call stack (which in that environment is named the return stack), although any data can be temporarily placed there using special return-stack handling code so long as the needs of calls and returns are respected; parameters are ordinarily stored on a separate data stack or parameter stack, typically called the stack in Forth terminology even though there is a call stack since it is usually accessed more explicitly. Some Forths also have a third stack for floating-point parameters. " -- [3]

"A Forth system is allowed to keep local variables on the return stack. This is reasonable, as local variables usually eliminate the need to use the return stack explicitly. So, if you want to produce a standard compliant program and you are using local variables in a word, forget about return stack manipulations in that word (refer to the standard document for the exact rules). " -- [4]

Adding a third stack to a Forth engine by VanNorman and Koopman says that the best use of a third stack in Forth would be for local variable (activation records, like C), but doesnt explain why this is better than just storing local variablens on the parameter stack or on the return stack

http://turboforth.net/resources/locals.html is an example of a third stack local variable implementation in Forth

https://forth-standard.org/standard/locals says "The storage resource ((for locals)) may be the return stack or may be implemented in other ways, such as in registers. The storage resource shall not be the data stack. Use of locals shall not restrict use of the data stack before or after the point of declaration. "

Stack Frames and Local Variables by George B Lyons (1985) or [5] is an example of a parameter stack implementation of local variables in Forth. I only skimmed it but it appears to be similar to (now)-standard activation records on a single stack.

Forth Dimension, volume XI, number 1 talks, amongst other thanks, about some techniques for adding local varibles to Forth. For example, on page 18, there is a description of an implementation that uses the return stack.

use of stacks

(I could be misunderstanding the following, but:) In Forth, there are typically two stacks, one called either just 'stack' or 'parameter stack', and the other called 'return stack'. The parameter stack tends to be much larger than the return stack. The return stack is the one organized with traditional 'stack frames', with return addresses, loop counters, and maybe local variables, so it looks the most like a C stack. Function calling parameters go on Forth's larger 'parameter stack'. The 'parameter stack' is manipulated much more frequently than the return stack.

retrospectives

opinions

primitives

" Appendix B A Glossary of Forth Primitives

The Forth language is based on an extensible, interactive compiler that creates code for a virtual stack machine. The virtual machine has two stacks. The Data Stack is used for expression evaluation and subroutine parameter passing. The Return Stack is used for subroutine return address saving and for loop control variable storage. The source code for Forth directly reflects the underlying stack machine, and so uses Reverse Polish Notation (RPN) to perform all operations using.

Forth programs are built as a hierarchy of subroutines. Each subroutine is called a "word" in Forth terminology. A program consists of a single Forth word which calls several other Forth words, and so on, forming a tree-structured program. At the lowest level, the leaves of the tree are invocations of Forth primitive words that manipulate the stacks and perform arithmetic.

Below is a glossary of the Forth primitive words found on stack machines discussed in this book. Most of these primitives are actually applicable to any program written in any language on a stack machine (for example, addition of the top two stack elements or swapping the order of the top two stack elements). Forth nomenclature is used in discussions to maintain consistency with an existing standard vocabulary for stack machine operation.

Each Forth word is followed by a "stack picture" on the same line. This stack picture shows the input parameters and output parameters on the Forth Data Stack for the word being described. The values on the left of the "_" indicate the input parameters while those to the right of the "_" indicate output parameters. Each parameter list is ordered with the topmost stack element to the right. Notation in the stack lists is as follows: N1, N2, N3, etc. indicate single-precision integers. D1, D2, etc. indicate double-precision integers, which take up two elements on the data stack. ADDR indicates an address, which may be thought of as a pointer value. FLAG is an integer which is false if zero, true if non-zero. A more detailed glossary of Forth is Haydon's All About Forth (1983).

0 - 0 Push the integer 0 onto the stack.

0< N1 - FLAG Return a true FLAG if N1 is negative.

0= N1 - FLAG Return a true FLAG if N1 is zero.

0 N1 - FLAG Return a true FLAG if N1 is greater than zero.

0BRANCH N1 - If N1 is false (value is 0) perform a branch to the address in the next program cell, otherwise continue.

1+ N1 - N2 Add one to N1, returning N2.

1- N1 - N2 Subtract one from N1, returning N2.

2+ N1 - N2 Add two to N1, returning N2.

2* N1 - N2 Multiply N1 by two, returning N2.

2/ N1 - N2 Divide N1 by two, returning N2.

4+ N1 - N2 Add four to N1, returning N2.

< N1 N2 - FLAG Return a true FLAG if N1 is less than N2.

<> N1 N2 - FLAG Return a true FLAG if N1 is not equal to N2.

N1 N2 - FLAG

Return a true FLAG if N1 equals N2.

R N1 - Push N1 onto the return stack.

> N1 N2 - FLAG Return a true FLAG if N1 is greater than N2.

! N1 ADDR - Store N1 at location ADDR in program memory.

+ N1 N2 - N3 Add N1 and N2, giving sum N3.

+! N1 ADDR - Add N1 to the value pointed to by ADDR.

- Define the start of a subroutine. The primitive [CALL] is compiled every time this subroutine is reference by other definitions.

; - Perform a subroutine return and end the definition of a subroutine. The primitive [EXIT] is compiled.

?DUP N1 - N1 N1 ( if N1 non-zero ) N1 - N1 ( if N1 is zero ) Conditionally duplicate the input N1 if it is non-zero.

@ ADDR - N1< Fetch the value at location ADDR in program memory, returning N1.

ABS N1 - N2 Take the absolute value of N1 and return the result N2.

AND N1 N2 - N3 Perform a bitwise AND on N1 and N2, giving result N3.

BRANCH - Perform an unconditional branch to the compiled in-line address.

D! D1 ADDR - Store the double-precision value D1 at the two memory words starting at ADDR.

D+ D1 D2 - D3 Return the double precision sum of D1 and D2 as D3.

D@ ADDR - D1 Fetch the double precision value D1 from memory starting at address ADDR.

DDROP D1 - Drop the double-precision integer D1.

DDUP D1 - D1 D1 Duplicate D1 on the stack.

DNEGATE D1 - D2 Return D2, which is the two's complement of D1.

DROP N1 - Drop N1 from the stack.

DSWAP D1 D2 - D2 D1 Swap the top two double-precision numbers on the stack.

DUP N1 - N1 N1 Duplicate N1, returning a second copy of it on the stack.

I - N1 Return the index of the currently active loop.

I' - N1 Return the limit of the currently active loop.

J - N1 Return the index of the outer loop in a nested loop structure.

LEAVE - Set the loop counter on the return stack equal to the loop limit to force an exit from the loop.

LIT - N1 Treat the compiled in-line value as an integer constant, and push it onto the stack as N1.

NEGATE N1 - N2 Return N2, which is the two's complement of N1 NOP - Do nothing.

NOT FLAG1 - FLAG2 Synonym for 0=. Takes the inverse of a flag value.

OR N1 N2 - N3 Perform a bitwise OR on N1 and N2, giving result N3.

OVER N1 N2 - N1 N2 N1 Push a copy of the second element on the stack, N1, onto the top of the stack.

PICK ... N1 - ... N2 Copy the N1'th element deep in the data stack to the top. In Forth-83, 0 PICK is equivalent to DUP , and 1 PICK is equivalent to OVER .

R> - N1 Pop the top element of the return stack, and push it onto the data stack as N1.

R@ - N1 Copy the top Return Stack word N1 onto the Data Stack.

ROLL ... N1 - ... N2 Pull the N1'th element deep in the data stack to the top, closing the hole left in the stack. In Forth-83, 1 ROLL is equivalent to SWAP , and 2 ROLL is equivalent to ROT.

ROT N1 N2 N3 - N2 N3 N1 Pull the third element down in the stack onto the top of the stack.

S-D N1 - D2 Sign extend N1 to occupy two words, making it a double precision integer D2.

SWAP N1 N2 - N2 N1 Swap the order of the top two stack elements.

U< U1 U2 - FLAG Return a true FLAG if U1 is less than U2 when compared as unsigned integers.

U U1 U2 - FLAG Return a true FLAG if U1 is greater than U2 when compared as unsigned integers.

U* N1 N2 - D3 Perform unsigned integer multiplication on N1 and N2, yielding the unsigned double precision result D3.

U/MOD D1 N2 - N3 N4 Perform unsigned integer division on D1 and N2, yielding the quotient N4 and the remainder N3.

XOR N1 N2 - N3 Perform a bitwise exclusive OR on N1 and N2, giving result N3.

" -- http://users.ece.cmu.edu/~koopman/stack_computers/appb.html

LITERAL LEAVE KEY? KEY J INVERT INCLUDED INCLUDE-FILE IMMEDIATE IF I HOLD HEX HERE GET-ORDER GET-CURRENT F~ FVARIABLE FTANH FTAN FSWAP FSQRT FSINH FSINCOS FSIN FS. FROUND FROT FREE FOVER FORTH-WORDLIST FORTH FNEGATE FMIN FMAX FM/MOD FLUSH-FILE FLOOR FLOG FLOATS FLOAT+ FLNP1 FLN FLITERAL FIND FILL FILE-STATUS FILE-SIZE FILE-POSITION FEXPM1 FEXP FE. FDUP FDROP FDEPTH FCOSH FCOS FCONSTANT FATANH FATAN2 FATAN FASINH FASIN FALSE FALOG FALIGNED FALIGN FACOSH FACOS FABS F@ F>D F< F0= F0< F/ F. F- F+ F F* F! EXIT EXECUTE EVALUATE ERASE ENVIRONMENT? ENDOF ENDCASE EMIT ELSE EKEY? EKEY>CHAR EKEY DUP DUMP DU< DROP DOES> DO DNEGATE DMIN DMAX DFLOATS DFLOAT+ DFALIGNED DFALIGN DF@ DF! DEPTH DELETE-FILE DEFINITIONS DECIMAL DABS D>S D>F D= D< D2/ D2* D0= D0< D.R D. D- D+ CS-ROLL CS-PICK CREATE-FILE CREATE CR COUNT CONSTANT COMPILE, COMPARE CODE CMOVE> CMOVE CLOSE-FILE CHARS CHAR+ CHAR CELLS CELL+ CATCH CASE C@ C, C" C! BYE BLOCK BLK BLANK BL BIN BEGIN BASE AT-XY ASSEMBLER AND ALSO ALLOT ALLOCATE ALIGNED ALIGN AHEAD AGAIN ACCEPT ABS ABORT" ABORT @ ?DUP ?DO ? >R >NUMBER >IN >FLOAT >BODY > = <> <# < ;CODE ; :NONAME : 2VARIABLE 2SWAP 2ROT 2R@ 2R> 2OVER 2LITERAL 2DUP 2DROP 2CONSTANT 2@ 2>R 2/ 2* 2! 1- 1+ 0> 0= 0<> 0< /STRING /MOD / .S .R .( ." . -TRAILING - , +LOOP +! + */MOD */ * (LOCAL) ( ' #S #> # ! ok

Opinions:

Implementations:

Retrospectives:

Forth Tutorials and books:

i think "defining words", which are created with create / does>, is just OOP for forth, where classes have a single method; create defines a class, the stuff after create and before does is the initializer, and the stuff after does> is the method. before calling the method, at runtime the address of the instance data is pushed onto the stack, which is the anaology of the first argument to methods being the receiver ('self') in Python. -- me

"my mind was immediately blown away by the following passage right at the top of system.fth, the part of pForth implemented in Forth on top of the C interpreter:

( 41 word drop ; immediate ( That was the definition for the comment word. ) ( Now we can add comments to what we are doing! )

What this does is define a word (Forth's name for a function) called "(". "(" is executed at compile time (as directed by IMMEDIATE). It tells the compiler to read bytes from the source file (that's what the word called, um, WORD is doing), until a ")" – ASCII 41 – is found. Those bytes are then ignored (the pointer to them is removed from the stack with DROP). So effectively, everything inside "( … )" becomes a comment.

...

 conditional primitives
IF ( -- f orig ) ?comp compile 0branch conditional_key >mark ; immediate
THEN ( f orig -- ) swap ?condition >resolve ; immediate
BEGIN ( -- f dest ) ?comp conditional_key <mark ; immediate
AGAIN ( f dest -- ) compile branch swap ?condition <resolve ; immediate
UNTIL ( f dest -- ) compile 0branch swap ?condition <resolve ; immediate
AHEAD ( -- f orig ) compile branch conditional_key >mark ; immediate

Conditional primitives?! Looks like conditional primitives aren't – they define them here. This COMPILE BRANCH business modifies the code of a function that uses IF or THEN, at compile time. THEN – one part of the conditional – writes (RESOLVEs) a branch offset to a point in code saved (MARKed) by IF, the other part of the conditional.

...

Sort of understood how Forth code was represented and interpreted. Code is this array of "execution tokens" – function pointers, numbers and a few built-ins like branches, basically. A Forth interpreter keeps an instruction pointer into this array (ip), a data stack (ds), and a return stack (rs), and does this:

while(true) { switch(*ip) { arithmetics (+,-,*...): case PLUS: ds.push(ds.pop() + ds.pop()); ++ip; stack manipulation (drop,swap,rot...): case DROP: ds.pop(); ++ip; literal numbers (1,2,3...): case LITERAL: ds.push(ip[1]); ip+=2; control flow: case COND_BRANCH: if(!ds.pop()) ip+=ip[1]; else ip+=2; case RETURN: ip = rs.pop(); user-defined words: save return address & jump default: rs.push(ip+1); ip = *ip; } }

That's it, pretty much. Similar, say, to the virtual stack machine used to implement Java. One difference is that compiling a Forth program is basically writing to the code array in a WYSIWYG fashion. COMPILE SOMETHING simply appends the address of the word SOMETHING to the end of the code array. So does plain SOMETHING when Forth is compiling rather than interpreting, as it is between a colon and a semicolon, that is, when a word is defined.

So

DRAW-RECTANGLE 2DUP UP RIGHT DOWN LEFT ;

simply appends {&2dup,&up,&right,&down,&left,RETURN} to the code array.

...

I went on to sort of understand CREATE/DOES> – a further extension of this compile-time self-modifying code business that you use to "define defining words" (say, CONSTANT, VARIABLE, or CLASS). The CREATE part says what should be done when words (say, class names) are defined by your new defining word. The DOES> part says what should be done when those words are used. For example:

CONSTANT CREATE , DOES> @ ; \ usage example: 7 CONSTANT DAYS-IN-WEEK DAYS-IN-WEEK 2 + . \ should print 9

CREATE means that every time CONSTANT is called, a name is read from the source file (similarly to what WORD would have done). Then a new word is created with that name (as a colon would have done). This word records the value of HERE – something like sbrk(0), a pointer past the last allocated data item. When the word is executed, it pushes the saved address onto the data stack, then calls the code after DOES>. The code after CREATE can put some data after HERE, making it available later to the DOES> part.

With CONSTANT, the CREATE part just saves its input (in our example, 7) – the comma word does this: *HERE++ = ds.pop(); The DOES> part then fetches the saved number – the @ sign is the fetch word: ds.push( *ds.pop() );

CONSTANT works somewhat similarly to a class, CREATE defining its constructor and DOES> its single method:

class Constant def initialize(x) @x=x end def does() @x end end daysInWeek = Constant.new(7) print daysInWeek.does() + 2

…But it's much more compact on all levels.

Another example is defining C-like structs. Stripped down to their bare essentials (and in Forth things tend to be stripped down to their bare essentials), you can say that:

struct Rectangle { int width; int height; };

…simply gives 8 (the structure size) a new name Rectangle, and gives 0 and 4 (the members' offsets) new names, width and height. Here's one way to implement structs in Forth:

struct cell field width cell field height constant rectangle

\ usage example: \ here CREATE is used just for allocation create r1 rectangle allot \ r1=HERE; HERE+=8 2 r1 width ! 3 r1 height !

area dup width @ swap height @ * ; r1 area . \ should print 6

CELL is the size of a word; we could say "4 field width" instead of "cell field width" on 32b machines. Here's the definition of FIELD:

 : field ( struct-size field-size -- new-struct-size )
    create over , +
    does> @ +
 ;

Again, pretty compact. The CREATE part stores the offset, a.k.a current struct size (OVER does ds.push(ds[1]), comma does *HERE++=ds.pop()), then adds the field size to the struct size, updating it for the next call to FIELD. The DOES> part fetches the offset, and adds it to the top of the stack, supposedly containing the object base pointer, so that "rect width" or "rect height" compute &rect.width or &rect.height, respectively. Then you can access this address with @ or ! (fetch/store). STRUCT simply pushes 0 to the top of the data stack (initial size value), and at the end, CONSTANT consumes the struct size:

struct \ data stack: 0 cell ( ds: 0 4 ) field width ( ds: 4 ) cell ( ds: 4 4 ) field height ( ds: 8 ) constant rectangle ( ds: as before STRUCT )

You can further extend this to support polymorphic methods – METHOD would work similarly to FIELD, fetching a function pointer ("execution token") through a vtable pointer and an offset kept in the CREATEd part. A basic object system in Forth can thus be implemented in one screen (a Forth code size unit – 16 lines x 64 characters).

" -- [26]

" no-s 1 day ago [–]

>> So you're kind of left to figure out all the little idioms and best practices for stack management all on your own.

That was my experience learning Forth in the 80's. Initially I tried to add compositional features I was familiar with from APL and Lisp. I raged against the limitations of the cell and schemed to use a typed stack. Eventually I became one with PAD and was able to ALLOT peace to my code. At some point I ranted on how the idiom of counted strings could be generalized to all sorts of useful sin and had an epiphany. 2ROT on!

reply " -- https://news.ycombinator.com/item?id=23457740

" theamk 2 days ago [–]

For me, the main problem with forth is a lack of names -- most languages (functional or imperative or even declarative) assign names to things -- things like function parameters, temporary values and so on.

Even Prolog, which is as far from traditional structural program as one can go, usually has descriptive names for unbound variables.

Compared to this, Forth is very name-terse. You get "function names" at best, and nothing else. This really makes programs much harder to understand, as it requires one to remember much more things while reading the code.

reply " -- https://news.ycombinator.com/item?id=23453784

"Forth only works well when used properly: as a very easy to extend DSL." -- jacquesm

" c2the3rd 2 days ago [–]

Why not say "Every ~~Forth~~ program is its own DSL for accomplishing its work."? For moderately complicated programs in any language you choose, it can take a long time to grok how the literal code relates to solving the conceptual problem. No language can build in every abstraction, and no programmer has time to learn them all.

I think you are close to part of an answer, but it isn't because Forth and Lisp expect one to do more work than other languages. If anything, they expect one to do less. The problem is programmers feel lost because there is no way to differentiate the bedrock of the language from higher abstractions. C has operators and statements and keywords that tell you there is nothing "underneath" what you are looking at. With Forth, everything is words. With Lisp, everything is lists.

reply "

some notes on Forth implementations:

kazinator 23 hours ago [-]

> Now onto the new instruction:

Adding a special VM instruction for such operations as inheriting from a class is a strikingly bad design decision.

You want to minimize the proliferation of VM instructions as much as possible.

A rule of thumb is: is it unreasonable to compile into a function call? Or else, is it likely to be heavily used in inner loops, requiring the fastest possible dispatch? If no, don't add an instruction for it. Compile it into an ordinary call of a run-time support function.

You're not going to inherit from a class hundreds of millions of times in some hot loop where the application spends 80% of its time; and if someone contrives such a thing, they don't necessarily deserve the language level support for cutting their run-time down.

reply

munificent 22 hours ago [-]

This is a good point. One of the things I have to balance with the book is teaching the optimal way to do something without dragging through reader through a large amount of verbose, grungy code. So sometimes (but not too often) I take a simpler approach even if it's not the best one.

With this VM, we have plenty of opcode space left, so it's natural to just add another instruction for inheritance, even if it does mean that the bytecode dispatch loop doesn't fit in the instruction cache quite as well.

I'll think about this some more. I'm very hesitant to make sweeping changes to the chapter (I want nothing more in life than for the book to be done), but this would be a good place to teach readers this fundamental technique of compiling language constructs to runtime function calls.

reply

---

" The Classical Forth Registers

The classical Forth model has five "virtual registers." These are abstract entities which are used in the primitive operations of Forth. NEXT, ENTER, and EXIT were defined earlier in terms of these abstract registers.

Each of these is one cell wide -- i.e., in a 16-bit Forth, these are 16-bit registers. (There are exceptions to this rule, as you will see later.) These may not all be CPU registers. If your CPU doesn't have enough registers, some of these can be kept in memory. I'll describe them in the order of their importance; i.e., the bottom of this list are the best candidates to be stored in memory.

W is the Working register. It is used for many things, including memory reference, so it should be an address register; i.e., you must be able to fetch and store memory using the contents of W as the address. You also need to be able to do arithmetic on W. (In DTC Forths, you must also be able to jump indirect using W.) W is used by the interpreter in every Forth word. In a CPU having only one register, you would use it for W and keep everything else in memory (and the system would be incredibly slow).

IP is the Interpreter Pointer. This is used by every Forth word (through NEXT, ENTER, or EXIT). IP must be an address register. You also need to be able to increment IP. Subroutine threaded Forths don't need this register.

PSP is the Parameter Stack (or "data stack") Pointer, sometimes called simply SP. I prefer PSP because SP is frequently the name of a CPU register, and they shouldn't be confused. Most CODE words use this. PSP must be a stack pointer, or an address register which can be incremented and decremented. It's also a plus if you can do indexed addressing from PSP.

RSP is the Return Stack Pointer, sometimes called simply RP. This is used by colon definitions in ITC and DTC Forths, and by all words in STC Forths. RSP must be a stack pointer, or an address register which can be incremented and decremented.

If at all possible, put W, IP, PSP, and RSP in registers. The virtual registers that follow can be kept in memory, but there is usually a speed advantage to keeping them in CPU registers.

X is a working register, not considered one of the "classical" Forth registers, even though the classical ITC Forths need it for the second indirection. In ITC you must be able to jump indirect using X. X may also be used by a few CODE words to do arithmetic and such. This is particularly important on processors that cannot use memory as an operand. For example, ADD on a Z80 might be (in pseudo-code)

   POP W   POP X   X+W -> W   PUSH W 

Sometimes another working register, Y, is also defined.

UP is the User Pointer, holding the base address of the task's user area. UP is usually added to an offset, and used by high-level Forth code, so it can be just stored somewhere. But if the CPU can do indexed addressing from the UP register, CODE words can more easily and quickly access user variables. If you have a surplus of address registers, use one for UP. Single-task Forths don't need UP.

...

Use of the Hardware Stack

Most CPUs have a stack pointer as part of their hardware, used by interrupts and subroutine calls. How does this map into the Forth registers? Should it be the PSP or the RSP?

The short answer is, it depends. It is said that the PSP is used more than the RSP in ITC and DTC Forths. If your CPU has few address registers, and PUSH and POP are faster than explicit reference, use the hardware stack as the Parameter Stack.

On the other hand, if your CPU is rich in addressing modes -- and allows indexed addressing -- there's a plus in having the PSP as a general-purpose address register. In this case, use the hardware stack as the Return Stack.

Sometimes you do neither! The TMS320C25's hardware stack is only eight cells deep -- all but useless for Forth. So its hardware stack is used only for interrupts, and both PSP and RSP are general-purpose address registers. (ANS Forth specifies a minimum of 32 cells of Parameter Stack and 24 cells of Return Stack; I prefer 64 cells of each.)

...

Top-Of-Stack in Register

Forth's performance can be improved considerably by keeping the top element of the Parameter Stack in a register!

...

If you have at least six cell-size CPU registers, I recommend keeping the TOS in a register. I consider TOS more important than UP to have in register, but less important than W, IP, PSP, and RSP. (TOS in register performs many of the functions of the X register.) It's useful if this register can perform memory addressing. PDP-11s, Z8s, and 68000s are good candidates.

Nine of the 19 IBM PC Forths studied by Guy Kelly [KEL92] keep TOS in register.

...

What about buffering two stack elements in registers? When you keep the top of stack in a register, the total number of operations performed remains essentially the same. A push remains a push, regardless of whether it is before or after the operation you're performing. On the other hand, buffering two stack elements in registers adds a large number of instructions -- a push becomes a push followed by a move. Only dedicated Forth processors like the RTX2000 and fantastically clever optimizing compilers can benefit from buffering two stack elements in registers.

Some examples

Here are the register assignments made by Forths for a number of different CPUs. Try to deduce the design decisions of the authors from this list.

             Figure 5. Register Assignments
            W     IP    PSP   RSP   UP     TOS   

8086[1] BX SI SP BP memory memory [LAX84] 8086[2] AX SI SP BP none BX [SER90] 68000 A5 A4 A3 A7=SP A6 memory [CUR86] PDP-11 R2 R4 R5 R6=SP R3 memory [JAM80] 6809 X Y U S memory memory [TAL80] 6502 Zpage Zpage X SP Zpage memory [KUN81] Z80 DE BC SP IX none memory [LOE81] Z8 RR6 RR12 RR14 SP RR10 RR8 [MPE92] 8051 R0,1 R2,3 R4,5 R6,7 fixed memory [PAY90]

[1] F83. [2] Pygmy Forth.

"SP" refers to the hardware stack pointer. "Zpage" refers to values kept in the 6502's memory page zero, which are almost as useful as -- sometimes more useful than -- values kept in registers; e.g., they can be used for memory addressing. "Fixed" means that Payne's 8051 Forth has a single, immovable user area, and UP is a hard-coded constant.

Narrow Registers

Notice anything odd in the previous list? The 6502 Forth -- a 16-bit model -- uses 8-bit stack pointers!

It is possible to make PSP, RSP, and UP smaller than the cell size of the Forth. This is because the stacks and user area are both relatively small areas of memory. Each stack may be as small as 64 cells in length, and the user area rarely exceeds 128 cells. You simply need to ensure that either a) these data areas are confined to a small area of memory, so a short address can be used, or b) the high address bits are provided in some other way, e.g., a memory page select.

In the 6502, the hardware stack is confined to page one of RAM (addresses 01xxh) by the design of the CPU. The 8-bit stack pointer can be used for the Return Stack. The Parameter Stack is kept in page zero of RAM, which can be indirectly accessed by the 8-bit index register X. (Question for the advanced student: why use the 6502's X, and not Y? Hint: look at the addressing modes available.) " -- https://www.bradrodriguez.com/papers/moving1.htm

---

Is 4th dynamic/latebound, that is, can you at runtime decide which words to define and what their definitions are, or decide to redefine a word with a new definition that was decided only at runtime? I'm thinking probably yes but I'm not sure. If so this should be mentioned somewhere in my notes because it's pretty different from compiling to assembly.

---

One thing to remember about 4th is that because it is a stack machine instead of a register machine, and because The stack is not necessarily added to /cleared between it's analog of "function calls", a "function call" (word call) is cheaper than an actual function call because you Don't have to save and restore registers and you don't necessarily have to mess with the stack. this means that decomposing a word into smaller words has less performance implications than decomposing a function into smaller functions. This is not quite the same as inlining either because there is that jump to the word, and in exchange you get to reuse the code of the word rather than repeating it at each call site.



Joy

Retrospectives:

Variants/implementations:

Links:

Cat

Kitten

http://kittenlang.org/

https://github.com/evincarofautumn/kitten

Factor

http://factorcode.org/

"Factor is a concatenative, stack-based programming language with high-level features including dynamic types, extensible syntax, macros, and garbage collection. " -- [27]

Opinions:

Tutorials:

Stacker

Stacker is a toy Forth-like language invented to demonstrate LLVM.

Its built-in words are (quoting from http://llvm.org/releases/1.1/docs/Stacker.html#lexicon ):

Logical: These words provide the logical operations for comparing stack operands. The words are: < > <= >= = <> true false.

Bitwise: These words perform bitwise computations on their operands. The words are: 1 XOR AND NOT

Arithmetic: These words perform arithmetic computations on their operands. The words are: ABS NEG + - * / MOD */ ++ -- MIN MAX

Stack: These words manipulate the stack directly by moving its elements around. The words are: DROP DUP SWAP OVER ROT DUP2 DROP2 PICK TUCK

Memory: These words allocate, free, and manipulate memory areas outside the stack. The words are: MALLOC FREE GET PUT

Control: These words alter the normal left to right flow of execution. The words are: IF ELSE ENDIF WHILE END RETURN EXIT RECURSE

I/O: These words perform output on the standard output and input on the standard input. No other I/O is possible in Stacker. The words are: SPACE TAB CR >s >d >c <s <d <c.

DSSP

A Forth-like language, notable for being the language of the Setun ternary computer. A comparison between Forth and DSSP, and the motivation of the differences, may be found at Sidorov and Shumakov. DSSP and Forth. Compare Analysis.

Om

http://www.om-language.org/

Other 'Forth-like' languages

Not sure if these are all concatenative:

T0

https://t1lang.github.io/NorthSec-20190516.pdf

Mouse

http://www.retroprogramming.com/2012/08/mouse-language-for-microcomputers-by.html

Play

https://www.play-lang.dev/

Dawn

https://www.dawn-lang.org/

Popr

https://github.com/HackerFoo/poprc https://hackerfoo.com/presentations/ttpl_slides.html https://www.hackerfoo.com/posts/popr-tutorial-0-dot-machines.html

Uiua

Lists and discussions of concatenative languages

"Lots of post-Forth links at the Concatenative Languages Wiki. It’s interesting how Forth was basically reinvented (from CS-theoretical foundations) as Joy, and then lots of other languages have come from that merger.

I’m of the opinion that a really-useable Forth needs, at a minimum, stack discipline. Factor is an example — each word specifies its effect on the stack, and the compiler rejects code that doesn’t abide by that. Without that, it’s way, way too easy to mess up the stack and crash badly in ways that are horrible to debug.

The other great innovation (from Joy, I think) is “quotes”, which are nested sequences of words. At a stroke this gives you block structure, much nicer control structures, and anonymous functions and FP. I suspect these are pretty easy to add to a “classic” Forth, though I haven’t tried." -- snej


Footnotes:

1.