Okay, I think I understand how the multiple
DOES> is working. Possibly. Maybe. At least with Tali Forth.
A brief review (based on Brad's
http://www.bradrodriguez.com/papers/moving3.htm):
CREATE adds a word to the dictionary that by default creates a header in the dictionary and associated code that looks something like this:
In classical Forth terms, the JSR instruction lives at the Code Field Address (CFA) and the <address> part is a the Parameter Field Address (PFA). Except that this is a Subroutine Threaded Forth (STC), and so there isn't that distinction. Instead, the Execution Token (xt) simply points at the JSR instruction, and DOVAR knows to jump over the <address> part before it returns. By default, the whole thing just pushes that address to the stack.
Now, a simple
DOES> after a
CREATE is an immediate word that installs two parts in our new word: A component used to define the new words (traditionally named
(DOES), but not with Tali) and a JSR to DODOES, a runtime component for the newly defined words that they will later jump to. After that, the actual code after the
DOES> is compiled.
So when we use a
CREATE/DOES> construct to define a new word, it basically is just a jump to the DODOES routine of it's creator. This may sound a bit complicated, but is required to move stuff around on the Return Stack to get back where we started.
Now for the double
DOES>. Let's start with a simple example:
Code: Select all
: aaa create does> drop 0 does> drop 1 ;
The
DROP is required because by default, remember, we get the address back of where the code starts. If we disassemble this and strip out the
CREATE part and the underflow checks, we get:
Code: Select all
20 A6 88 jsr 88A6 ; (DOES)
20 03 B7 jsr B703 ; DODOES ; <-- BBB #1
E8 inx ; DROP
E8 inx
CA dex ; PUSH
CA dex
74 00 stz 0,x ; 0
74 01 stz 1,x
20 A6 88 jsr 88A6 ; (DOES)
20 03 B7 jsr B703 ; DODOES ; <-- BBB #2
E8 inx ; DROP
E8 inx
CA dex ; PUSH
CA dex
A9 01 lda #01 ; 1
95 00 sta 0,x
74 01 stz 1,x
60 rts
This is what we would expect: Subroutine jump to
(DOES), subroutine jump to
DODOES, and the actual code ... which contains, of course, the same thing again, but with 1 instead of 0 (both are hard-coded words in Tali, inlined here automatically because they are small). As an aside, note the stack thrashing INX INX DEX DEX combination.
Twice. Sigh.
Anyway. What happens when we define a new word, say BBB?
We start at the top, and create a new word, adding a JSR to the first (!)
DODOES jump. Using
SEE, we can confirm this:
Code: Select all
see bbb
nt: 19D5 xt: 19E0 NN
size (decimal): 3
19E0 20 97 19
19E0 jsr 1997
The JSR to $1997 is in fact our first DODOES, the one marked with "BBB #1". The important thing is that we have just defined BBB, and then we
stop.
The real magic happens when we actually run BBB. What this does (no pun intended) is jumps to the first DODOES, just where the arrow is at "BBB #1". This runs the payload, putting 0 on the stack --
but then continues to where it hits another JSR to the runtime
(DOES) word. This does what it is supposed to: Figures out which
CREATE it belongs to, and changes that JSR to it's own DODOES one instruction down (marked with "BBB #2"). And then it stops. We can confirm this with another
SEE:
Code: Select all
see bbb
nt: 19D5 xt: 19E0 NN
size (decimal): 3
19E0 20 AC 19
19E0 jsr 19AC
Notice what has changed: Only the payload address. BBB still lives at the same spot, but when it runs, it now jumps to the second DODOES (marked "BBB #2"). Since there is no third
DOES> in this case, that is now the action it takes for now and evermore.
To sum up: This is self-modifying code.

Which is sort of why Forth is the Deadpool of computer languages ...