Ed:
Good point regarding the automatic adjustment of an evaluation stack. Pushing and popping elements from the hidden registers of the evaluation stack should be performed explicitly in order to preserve the expected behavior of the
LDA and
STA instructions. Therefore, pushing should be performed using the
DUP LDA instruction sequence, and popping should be performed using the
STA ROT instruction sequence. These two sequences could be wrapped in appropriately named macros to make using the evaluation stack a bit easier. Both
DUP and
ROT are single byte, single cycle instructions, and when used this way, they will essentially function like some of the prefix instructions.
Mike:
I've read Loeliger's and Brody's books, Brad's tutorials, but that is about as close as I've ever come to FORTH. Perhaps I've missed something obvious, so I'm looking to you, Jeff, and others to clear up any misconceptions that I've had while working to determine the best instructions to design and implement to support a FORTH VM with the M65C02A.
From Brad's website and the Heart of FORTH page, the three core operations of a DTC/ITC FORTH inner interpreter are defined as shown below:
Code:
ITC DTC
================================================================================
NEXT: W <= (IP++) -- Ld *Code_Fld ; W <= (IP++) -- Ld *Code_Fld
PC <= ((W)) -- Jump Dbl Indirect; PC <= (W) -- Jump Indirect
================================================================================
ENTER: (RSP--) <= IP -- Push IP on RS ;(RSP--) <= IP -- Push IP on RS
IP <= W + 2 -- => Param_Fld ; IP <= W + 2 -- => Param_Fld
;NEXT
W <= (IP++) -- Ld *Code_Fld ; W <= (IP++) -- Ld *Code_Fld
PC <= ((W)) -- Jump Dbl Indirect; PC <= (W) -- Jump Dbl Ind
================================================================================
EXIT:
IP <= (++RSP) -- Pop IP frm RS ; IP <= (++RSP)-- Pop IP frm RS
;NEXT
W <= (IP++) -- Ld *Code_Fld ; W <= (IP++) -- Ld *Code_Fld
PC <= ((W)) -- Jump Dbl Indirect; PC <= (W) -- Jump Dbl Ind
================================================================================
What I've noticed is that NEXT is the instruction sequence through which both ENTER and EXIT complete. In Loeliger's book, he goes to great pain to terminate ENTER/EXIT through NEXT. In the preceding side-by-side pseudo-code description of these operations, it is clear that the only difference between the ITC NEXT and the DTC NEXT is the double indirection on W to reach the machine code that must be executed. I have reasoned that the M65C02A ENT instruction pushes IP onto the RS (return stack) and advances the W to point to the param field. I am assuming the following FORTH dictionary structure for which the Code Field is a two byte field for both ITC and DTC.
Code:
struct FORTH_word_t {
uint8_t Len;
uint8_t [Max_Name_Len] Name;
uint16_t *Link;
uint8_t [2] Code_Fld;
uint8_t [Code_Len] Param_Fld;
}
It is my understanding that the ITC code field points to ENTER if the word is a secondary or it points to NEXT if it is a primitive. In either case the double indirection on the code field locates the correct inner interpreter operation/routine/instruction. In the ITC case, the inner interpreter's NEXT is built from the
IND NXT instruction sequence, and NEXT is built from
ENT IND NXT.
It is my understanding that the DTC code field contains either CALLs/JMPs to ENTER (secondaries) or into the machine code in the param field (primitives). Thus, the DTC code field is filled with
ENT NXT (secondaries) or
NXT NOP (primitives).
In either case, EXIT from a primitive can be implemented as
OSY PLI NXT or
OSY PLI IND NXT (Pull IP from RS followed by NEXT). (The
OSY prefix instruction forces the use of Y register instead of S for stack operations. I think that FORTH's EXECUTE needs to be able to pull IP from the parameter/data stack (PS/DS) (where S is the PSP), so I've opted to use the
OSY prefix instruction to select the RS instead of the PS/DS.)
I've not implemented the IP/W module, but I expect
ENT to be implemented much like a normal 16-bit push instruction with the IP <= W + 2 operation taking place in parallel using a dedicated increment by 2 function within the module. Thus, I expect the cycle count for
ENT to come in at 3 cycles. I expect
NXT to use 2 cycles for each level indirect operation and to use the same increment by 2 function to adjust IP. Therefore, I expect DTC NEXT (
NXT) to come in at 5 cycles and ITC NEXT (
IND NXT) to come in at 8 cycles.
The preceding discussion should explain my thinking on the FORTH VM inner interpreter support instructions:
NXT,
ENT,
PLI. Any comments or corrections of misconceptions on my part are welcome.
From my work with Transputers and my stack machines, I think that many of the parameter stack operations can be performed more efficiently using the stack relative addressing modes. I realized that I had declared to new instructions
INW dp and
DEW dp. With the
SIZ prefix implemented by the ALU and microprogram, there is no need for these two instructions. I think that I will re-purpose them as instructions for adjusting S. In other words,
INW dp may be better used as
ASP #imm (Add #imm to S) and
DEW dp may be better used as
SSP #imm (Subtract #imm from S).
As I understand FORTH
@ it is used to read a memory location whose address is the pointer in the TOS element of the PS/DS. Also the pointer is removed from the PS/DS and the value read is pushed onto the PS/DS. Using stack relative addressing, FORTH
@ can be implemented as follows:
Code:
SIZ LDA (0,S)
SIZ STA 0,S
As I understand FORTH
! it is used to write a memory location whose address is the pointer in the TOS element of the PS/DS with the data taken from the NOS element. Also both the pointer and data are removed from the PS/DS. Using the
SIZ prefix instruction, FORTH
! can be implemented as follows:
Code:
SIZ PLX ; Pull pointer from PS/DS
SIZ STX dp ; Store into zero page pointer location
SIZ PLA ; Pull data from PS/DS
SIZ STA (dp) ; Store data using zero page pointer
I will provide some additional code fragments later today, but I've got head off to work now.
take your pick.