6502.org Forum  Projects  Code  Documents  Tools  Forum
It is currently Thu Nov 21, 2024 1:51 pm

All times are UTC




Post new topic Reply to topic  [ 16 posts ]  Go to page 1, 2  Next
Author Message
 Post subject: The coolest words
PostPosted: Sat Mar 20, 2021 5:29 pm 
Offline

Joined: Sun Apr 26, 2020 3:08 am
Posts: 357
After watching the video on "What is Forth" and saw the way a couple of words were used, intrigued me.

One of the words basically created a string constant using a colon definition.

S" has got to be the coolest way of storing and retrieving a string constant I have seen. Sure beats the heck out of what I was doing before.

I have seen examples where a word definition used .", but then to get to the start of the string, a value was added to skip past the rest of the header. ." already showed how to prevent a string from being compiled, all I had to do was expand upon that idea. Works great.

Any other cool words or routines anyone wants to share? Just a general description of what it does and how it benefits over regular ways is all that is needed. It doesn't have to be a part of any standard as it can be converted on this end.

Already have these words:
DOER/MAKE
IS
RECURSE
WHERES?
CASE - compact version that does not use OF, ENDOF, ENDCASE
.PHRASE - prints a paragraph of text using word wrap and delimited by a return mark.
SEE/DECODE/DECOMPILE


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Sat Mar 20, 2021 11:48 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8543
Location: Southern California
Quote:
CASE - compact version that does not use OF, ENDOF, ENDCASE

You can make it more efficient with of being a primitive. This is from my '816 Forth assembly-language source code:
Code:
        HEADER "of", NOT_IMMEDIATE   ; ( n1 n2 -- )    if n1 =  n2
_of:    PRIMITIVE                    ; ( n1 n2 -- n1 ) if n1 <> n2
        LDA   0,X          ; Take top stack item,
        CMP   2,X          ; compare to next stack item, & if
        BNE   ofend        ; not equal, get ready to try next OF.
        INX4               ; Otherwise, drop the top two stack items,
                           ; skip the branch address, and execute what's
        JMP   bump         ; inside the OF, until you get to ENDOF.
 ofend: INX2               ; If there wasn't a match, drop the top stack
        JMP   branch+2     ; item and go on to check the next OF.
 ;-------------------
(INX2 and INX4 are just two and four INX's.)

The following is from the "shadow screen" file (which is really just another text file, but instead of having the Forth assembly-language source code, it has more explanations and what the Forth source would look like if I were compiling a Forth from a metacompiler.

Code:
The structure:

  CASE                                  \ the word CASE compiles nothing
     n1 OF  actions1  ENDOF
     n2 OF  actions2  ENDOF
     n3 OF  actions3  ENDOF
     (default actions)
  ENDCASE                               \ ENDCASE only compiles a DROP and the
                                        \ address needed at each ENDOF

 can be compiled without the internal " of ", but it becomes far less efficient,
 both memory- and speedwise.  It would be as if you wrote:

  n1 OVER = IF DROP  actions1  ELSE
  n2 OVER = IF DROP  actions2  ELSE
  n3 OVER = IF DROP  actions3  ELSE
  (default actions)
  THEN THEN THEN

 Paranthetical note:  Default actions don't have to be all at the end.  For
 example, you could have thee comparisons, and if none of those are met, do
 some other processing before continuing on to the fourth comparison.  This can
 be done at various places along the CASE structure, as many as you want.

 Using the " of " internal, each line saves three cells (six bytes), in addition
 to running faster.  Clearly, it doesn't take much to justify having the " of "
 primitive.  OF and IF both compile an internal ( of  and 0branch ) plus a
 branch address.  Both ENDOF and ELSE compile the internal " branch ", plus the
 branch address.  The OVER , = , and DROP on every line are extra.  The book
 Thinking Forth, on page 239, says the CASE statement offers no efficiency
 improvement in either execution speed or memory used.  This is clearly false
 when you have the internal " of ".  The book is not very favorable to the CASE
 structure, but the X3J14 commitee thought highly enough of it to put it in the
 ANS standard Forth!


\ CASE below just puts the structure-check number (6) on the stack at compile
\ time so OF and ENDCASE could check for illegal constructs, and an initialized
\ number (0) of how many branch addresses ENDCASE will have to fill in at the
\ ENDOFs.  CASE does not actually compile anything.


Not shown here:
 of     (internal)


: CASE   ?COMP          \ (compile only)                 TF239-240  ANS_CORE_EXT
  6 0                           ;  IMMEDIATE


\ OF below compiles the internal of , which compares the two numbers on the top
\ of the stack.  If the two are equal, it drops them, and increments the IP to
\ the first cell after the two-cell OF instruction.  The second cell of this OF
\ instruction is the address of the next OF statement , which the IP would be
\ set to if the match failed.  If the match does fail, of would only drop the
\ very top stack cell so the next of could continue with the next value check.


: OF     ?COMP          \ (compile only)                            ANS_CORE_EXT
   DUP >R  PICK  6 ?PAIRS  COMPILE of               \ Related:  SET_OF  RANGE_OF
   HERE 0 ,    R> 1+  7         ;  IMMEDIATE



\ After making sure it's in the right kind of structure (by using ?PAIRS), ENDOF
\ below compiles the branch to the first cell after the ENDCASE.  Actually, it
\ only compiles the branch internal, then leaves room for the address to be
\ compiled by ENDCASE later on, when that address is known.  So that ENDCASE
\ will know where to put the address of the cell following it, the address
\ following the ENDOF is left on the stack, and n, the number of addresses left
\ by ENDOFs in the structure, is incremented by each "of" so ENDCASE will know
\ how many cells on the stack at compile time are addresses that it needs to
\ fill in.  ENDOF also stores the address of the following cell in the last
\ previous of (compiled by OF ) so that if the test fails at run time, that of
\ will send the IP to the right place to check the next possibility.


         ( 6 [...] of's_branch_operand_adr n 7 -- 6 [...] ENDOF's_branch_adr n )
: ENDOF  ?COMP                    \ (compile only)                  ANS_CORE_EXT
   7 ?PAIRS                       \ OF's compiler security # is 7.  CASE's is 6.
   >R                             \ Hold n, the number of previous ENDOFs.
      COMPILE branch   HERE 0 ,   \ Get branch's operand adr, & allot its field.
      >R                          \ Hold ENDOF's branch operand adr.
         HERE SWAP !              \ Fill in of's operand to point to next inst.
      R>
   R>                ;  IMMEDIATE


\ I would like to call them END_OF and END_CASE, but ANS and earlier Forths call
\ them ENDOF and ENDCASE without the _ character.  Since these are so common, I
\ won't change them.

<snip>

\ ENDCASE below first reaches back in the stack for the case structure number,
\ which is 6 in this case.  The ROLL reaches back behind all the addresses put
\ on the stack by all the ENDOFs that preceded the ENDCASE, and ?PAIRS makes
\ sure ENDCASE goes in the right kind of structure.  DROP is compiled to drop
\ the number that would have failed all the OF comparisons (at run time) if it
\ actually got to the ENDCASE instead of being routed around it by an ENDOF at
\ the end of one of the normal statements.  (Remember that if encountered,
\ ENDCASE wants to drop a cell off the stack even if it's after some "else"-type
\ code after the last ENDOF.  For this reason, you might need to put an
\ otherwise-unnecessary DUP in that part of the code.  Sometimes DUMMYCELL,
\ which just puts a 0 on the stack, is the more readable way to show what's
\ happening in that situation.)
\ Next in the compilation process, the ?DO...LOOP puts the address of the cell
\ following the ENDCASE after each of the ENDOFs in the structure so the IP can
\ skip the rest of the CASE statement after executing an ENDOF.


: ENDCASE  ?COMP                \ (compile only)    ( casecode adr...adr n -- )
   DUP 1+ ROLL  6 ?PAIRS                                         \  ANS_CORE_EXT
   COMPILE DROP
   0  ?DO  HERE SWAP !  LOOP    ;  IMMEDIATE

Then I also have SET_OF and RANGE_OF. The internal set_of (compiled by SET_OF) is a primitive, but I have not written range_of (compiled by RANGE_OF) as a primitive.
Code:
         HEADER "set_of", NOT_IMMEDIATE
_set_of: PRIMITIVE
         LDA    0,X
         STA    N+2     ; Store in N+2 the number of cells to compare, and
         INX2           ; remove that number from the stack.
         STZ    N       ; Zero N & N+1.  Two bytes get zeroed, which is ok.
                        ;   Non-0 will get put in N later if we find a match.
         TXY            ; We can't do LDA (N+2),X, so get ready to use Y.
         LDA    (N+2),Y ; Get the number for SET_OF to compare to.
         LSR    N+2     ; Number of comparisons = number of bytes divided by 2.
         LDY    N+2     ; Set up to loop enough times to compare all the numbrs.
 s_o1:     CMP  0,X     ; Compare the next number on the stack.
           BNE  s_o2    ; If there's no match, skip putting something in N.
             STX  N     ; (N got zeroed before this loop.)
 s_o2:     INX2         ; Drop the top of stack to be ready for next comparison.
           DEY          ; Decrement the number of comparisons left to be done.
         BNE    s_o1    ; If there's one or more left, loop back again.

         LDY    N
         BEQlong branch+2 ; If there was no match, skip past ENDOF for next OF .

         INX2           ; If match was found, drop the input number, and
         JMP    bump    ; bump the IP past the address cell following set_of.
 ;-------------------
(BEQlong is just a BNE around a JMP to the desired label.)

The "shadow screen" file portion for it is as follows:
Code:
\ { and } below are primarily for use with SET_OF .  You can start a series of
\ stack entries with {, and end it with }.  The final } puts a number on TOS
\ telling how many items  got added to the stack, not including itself, since
\ the {.  This way, code can appropriately handle an arbitrary number of stack
\ entries.  { and } have been valuable for other uses besides with SET_OF too.
\ The idea for SET_OF came from FD May/Jun '97, but I modified it (including the
\ SET[ to { and ]SET-OF to } SET_OF ) to be of wider usefulness, making the {
\ and } useful anywhere you want to correctly handle an undertermined number of
\ inputs on the stack, whether compiling, interpreting, or executing.

: {   ( -- )    R>   DEPTH >R          >R    ;  \ R>...>R preserve the return
: }   ( -- n )  R>   DEPTH R> -  SWAP  >R    ;  \ adr's position in return stk.


\ Actually, it looks like { and } would only be one byte longer as primitives!


\ The idea for SET_OF below is from FD, May/Jun '97.  It's nice for situations
\ where you want something like

   DUP  14 =
   OVER 15 = OR
   OVER 18 = OR
   OVER 20 = OR
   OVER 25 = OR
   OVER 65 = OR
   IF bla bla bla ELSE ...

\ but in a CASE statement.  Normally in a case statement, you'd have to test for
\ each of these numbers, and then have the same code between the OF and ENDOF
\ that follows each.  You can't give OF more than one number to compare to, so
\ SET_OF takes care of that.  The situation above now all fits on the first OF
\ line of the example

   CASE
      { 14 15 18 20 25 65 } SET_OF   bla bla bla   ENDOF
                                OF                 ENDOF
                                OF                 ENDOF
      if-no-match-then-do-this-other-stuff
   ENDCASE

\ SET_OF allows a type of OF that can compare to several possible values that
\ can result in the same action.  The values can be literals, or they can be
\ fetched or calculated between the { and the } .   set_of is the internal
\ compiled by SET_OF.

\ Notice that { and } are not limited to have only literals between them.  You
\ can just as easily have some of the values calculated on the fly, as long as
\ you keep in mind that the number of comparisons that will be made by SET_OF
\ will be the difference between the stack depth immediately before } minus the
\ stack depth at { .  You cannot, for example, remove a previously existing cell
\ from the stack to calculate one of the comparison values unless you
\ specifically take action to correct the resulting stack-depth discrepancy.



: set_of                \ Syn: { 13 16 20 24 } SET_OF ... ENDOF       (internal)
   DUP 1+ PICK          \ Copy the argument that went into CASE
                        \ ^ arg T1 T2 ... Tn n arg
   FALSE  ROT 0         \ ^ arg T1 T2 ... Tn arg FALSE n 0
   DO                   \ 1st time into loop: ^ arg T1 T2 ... Tn arg FALSE
      OVER 3ROLL = OR
   LOOP                         \ After last loop: ^ arg arg flag
   ?EXIT   NOT          ;       \ If flag was false, change one copy
                                \ of arg so they don't match.


\ SET_OF works very much like OF .  It does not make sure you preceded it with
\ { ... } .  I may decide to add that to the compiler security in the future.


: SET_OF   ( CASECODE n -- CASECODE ADR n+1 OFCODE )          \ (FD May/Jun '97)
   ?COMP                                                    \ Related:  RANGE_OF
   >R R@ PICK 6 ?PAIRS
      COMPILE set_of  COMPILE of
      HERE 0 ,
   R> 1+     7          ;       IMMEDIATE


\ Then in FD Jan/Feb '98, someone suggested RANGE_OF to add to SET_OF above.  It
\ lets you compare to a range of values.  Like BETWEEN in ANS Forth, the range
\ includes the limit numbers.  range_of is the internal compiled by RANGE_OF .
\ Similar to SET_OF , it doesn't matter where the input limits for RANGE_OF come
\ from.  OF , SET_OF , and RANGE_OF can be mixed in the same CASE statement.


: range_of      ( Syn:   14 20 RANGE_OF ... ENDOF  )                \ (internal)
   2PICK -ROT RANGE
   IF   DUP EXIT
   THEN DUP NOT         ;


: RANGE_OF ( CASECODE n -- CASECODE ADR n+1 OFCODE )          \ (FD Jan/Feb '98)
   ?COMP                                                      \ Related:  SET_OF
   >R R@ PICK 6 ?PAIRS
      COMPILE range_of
      COMPILE of
      HERE 0 ,
   R> 1+     7          ;       IMMEDIATE

I've used SET_OF more than RANGE_OF, but neither of them very much. You'll definitely want of and the other CASE-statement words though.

I might add more good stuff later.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Sun Mar 21, 2021 9:08 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 895
IamRob wrote:
After watching the video on "What is Forth" and saw the way a couple of words were used, intrigued me.

One of the words basically created a string constant using a colon definition.

S" has got to be the coolest way of storing and retrieving a string constant I have seen. Sure beats the heck out of what I was doing before.

I have seen examples where a word definition used .", but then to get to the start of the string, a value was added to skip past the rest of the header. ." already showed how to prevent a string from being compiled, all I had to do was expand upon that idea. Works great.

Any other cool words or routines anyone wants to share? Just a general description of what it does and how it benefits over regular ways is all that is needed. It doesn't have to be a part of any standard as it can be converted on this end.


Fleet forth doesn't have S" . It has the word " to compile a counted string. It can be used to compile an inline string like so:
Code:
: HEXAPUMA   " HMS HEXAPUMA ROYAL MANTICORAN NAVY." ;  OK
CR HEXAPUMA COUNT TYPE
HMS HEXAPUMA ROYAL MANTICORAN NAVY. OK

or save a string in the PAD when interpreting.
Code:
" NOW IS THE WINTER OF OUR DISCONTENT"  OK
.S 22484  OK
CR COUNT TYPE
NOW IS THE WINTER OF OUR DISCONTENT OK
PAD COUNT CR TYPE
NOW IS THE WINTER OF OUR DISCONTENT OK
CONSOLE

Speaking of cool words, I think COMPILE and [COMPILE] are better than POSTPONE .
Fleet Forth has a word, DOS that takes a counted string and sends it to the currently selected disk drive as a disk command. The word DOS" sends an inline string as a command or it can compile an inline disk command.
Code:
: DOS"
   [COMPILE] "
   STATE @
   IF
      COMPILE
   THEN
   DOS ; IMMEDIATE

I don't see how this could be done as simply with POSTPONE .

How about coroutines?
Code:
// COROUTINES
: CO   2R> SWAP 2>R ;



Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Sun Mar 21, 2021 11:06 pm 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8543
Location: Southern California
Quote:
Speaking of cool words, I think COMPILE and [COMPILE] are better than POSTPONE .

COMPILE and [COMPILE] are the older ones, and they're the ones I've used. I guess I see advantages and disadvantages. I put these and POSTPONE as well in my '816 Forth, but must confess I've never even tested POSTPONE.

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Mon Mar 22, 2021 1:16 am 
Offline

Joined: Sun Apr 26, 2020 3:08 am
Posts: 357
JimBoyd wrote:

Speaking of cool words, I think COMPILE and [COMPILE] are better than POSTPONE .
Fleet Forth has a word, DOS that takes a counted string and sends it to the currently selected disk drive as a disk command. The word DOS" sends an inline string as a command or it can compile an inline disk command.
Code:
: DOS"
   [COMPILE] "
   STATE @
   IF
      COMPILE
   THEN
   DOS ; IMMEDIATE

I don't see how this could be done as simply with POSTPONE .

My definition of POSTPONE is just [COMPILE] [COMPILE] ; IMMEDIATE which basically just takes the place of [COMPILE].

But I read somewhere that POSTPONE can do both. The only difference is the COMPILE works on non-IMMEDIATE words and [COMPILE] works on IMMEDIATE words. THEN follows your COMPILE example above, which gets compiled at run-time. I don't follow this reasoning.

Quote:
How about coroutines?
Code:
// COROUTINES
: CO   2R> SWAP 2>R ;

I need some context with this one in how it would be used. What is the return address of CO getting swapped with? Another return address?


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Mon Mar 22, 2021 2:28 am 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8543
Location: Southern California
IamRob wrote:
But I read somewhere that POSTPONE can do both. The only difference is the COMPILE works on non-IMMEDIATE words and [COMPILE] works on IMMEDIATE words. THEN follows your COMPILE example above, which gets compiled at run-time. I don't follow this reasoning.
Yeah, I didn't think POSTPONE was really necessary.

Quote:
Quote:
How about coroutines?
Code:
// COROUTINES
: CO   2R> SWAP 2>R ;

I need some context with this one in how it would be used. What is the return address of CO getting swapped with? Another return address?

I have a lot of those combinations to save program space and improve performance. I listed probably most of them at viewtopic.php?p=72734#p72734 . The CO word above defined as a secondary would have very limited usefulness if any though, because your return address gets changed. I have RSWAP defined in my '816 Forth as:
Code:
       HEADER "RSWAP", NOT_IMMEDIATE   ; Swaps the two top return-stack cells.
RSWAP: PRIMITIVE
       LDA   1,S
       STA   N
       LDA   3,S
       STA   1,S
       LDA   N
       STA   3,S
       GO_NEXT
 ;-------------------
which executes much, much faster and doesn't mess up where it comes back to for the next instruction.

Edit: BarryM pointed out that I could make it four bytes shorter and four cycles faster the following way. I don't know why I didn't do it that way to start. In the '02 Forth, Y has a special use; but it wouldn't matter on the '816.
Code:
       HEADER "RSWAP", NOT_IMMEDIATE   ; Swaps the two top return-stack cells.
RSWAP: PRIMITIVE
       INDEX_16
       PLA
       PLY
       PHA
       PHY
       INDEX_8
       GO_NEXT
 ;-------------------

A lot of others were even more trivial to implement, like merging I and @ (where the loop index is intentionally an address) into I@ like this:
Code:
        HEADER "I@", NOT_IMMEDIATE      ; Same as  I @
Ifetch: PRIMITIVE
        LDY   #<0
        LDA   (1,S),Y
        JMP   PUSH
 ;-------------------
or merging I and + into I+ like this:
Code:
        HEADER "I+", NOT_IMMEDIATE      ; Same as  I +
I_add:  PRIMITIVE
        LDA   1,S
        CLC
        ADC   0,X
        JMP   PUT
 ;-------------------

(These are written for the 65816 which is much shorter than for the 6502, and I realize most readers are using the '02 instead; but this makes it easier to see what's going on, since there aren't a lot of lines for handling 16-bit quantities 8 bits at a time.)

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Mon Mar 22, 2021 9:20 pm 
Offline

Joined: Sun Apr 26, 2020 3:08 am
Posts: 357
Found another really short but really cool word while searching other Forths.

Someone had re-defined the semi-colon word and added a TRACE function. The .S function was introduced and as each word exits, it prints to the screen either: (The depth of the stack plus any values left on the stack), " Stack is Empty" or "Stack Underflow".

All automatic so one doesn't have to type the word to execute, followed by .S every time.


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Tue Mar 23, 2021 8:14 am 
Offline
User avatar

Joined: Fri Aug 30, 2002 1:09 am
Posts: 8543
Location: Southern California
IamRob wrote:
Found another really short but really cool word while searching other Forths.

Someone had re-defined the semi-colon word and added a TRACE function. The .S function was introduced and as each word exits, it prints to the screen either: (The depth of the stack plus any values left on the stack), " Stack is Empty" or "Stack Underflow".

All automatic so one doesn't have to type the word to execute, followed by .S every time.

I had considered doing a similar thing by making it so I could swap out the NEXT routine on the fly for other ones that do things like interactive single-stepping or almost-full-speed real-time instruction trace. I never really needed it though, so I never have gone through with it. I've very seldom even needed .S .

_________________
http://WilsonMinesCo.com/ lots of 6502 resources
The "second front page" is http://wilsonminesco.com/links.html .
What's an additional VIA among friends, anyhow?


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Tue Mar 23, 2021 7:41 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 895
IamRob wrote:
JimBoyd wrote:

Speaking of cool words, I think COMPILE and [COMPILE] are better than POSTPONE .
Fleet Forth has a word, DOS that takes a counted string and sends it to the currently selected disk drive as a disk command. The word DOS" sends an inline string as a command or it can compile an inline disk command.
Code:
: DOS"
   [COMPILE] "
   STATE @
   IF
      COMPILE
   THEN
   DOS ; IMMEDIATE

I don't see how this could be done as simply with POSTPONE .

My definition of POSTPONE is just [COMPILE] [COMPILE] ; IMMEDIATE which basically just takes the place of [COMPILE].

But I read somewhere that POSTPONE can do both. The only difference is the COMPILE works on non-IMMEDIATE words and [COMPILE] works on IMMEDIATE words. THEN follows your COMPILE example above, which gets compiled at run-time. I don't follow this reasoning.


Actually, [COMPILE] will compile anything, immediate or not. Here is the source in my Forth:
Code:
: [COMPILE]  ( -- )
   ?COMP ' , ; IMMEDIATE

?COMP aborts with an error message if STATE is not compiling. [COMPILE] parses the following word in the text stream ( with ' ) and compiles the parsed word. It does not matter if the parsed word is immediate or not.

COMPILE , on the other hand, is not even an immediate word. Here is the definition in my Forth:
Code:
: COMPILE
   ?COMP R> DUP 2+ >R @ , ;

Here is the definition of IF
Code:
: IF  ( -- >SYS )
   COMPILE ?BRANCH
   >MARK ; IMMEDIATE

And this is what gets compiled:
Code:
SEE IF
IF IMMEDIATE
 2D23 1F68 COMPILE
 2D25  CE6    ?BRANCH
 2D27 2CC4 >MARK
 2D29  B6F EXIT
8
 OK

COMPILE does not do anything when IF is compiled. When IF is executed ( while defining a new word ), it compiles the address of ?BRANCH into the word being defined. Lets call this word the 'new definition'.
Here is the source for WHILE in my Forth:
Code:
: WHILE  ( ?SYS -- >SYS ?SYS )
   [COMPILE] IF  2SWAP ; IMMEDIATE

and what it looks like:
Code:
SEE WHILE
WHILE IMMEDIATE
 2D55 2D21 IF IMMEDIATE
 2D57 1739 2SWAP
 2D59  B6F EXIT
6
 OK


Now let's take a closer look at my tricky use of COMPILE and branching.
Code:
: DOS"
   [COMPILE] "
   STATE @
   IF
      COMPILE
   THEN
   DOS ; IMMEDIATE


Since the word " is immediate, [COMPILE] is used to compile it. IF and THEN compile a conditional branch around the word COMPILE . This is what DOS" looks like:
Code:
SEE DOS"
DOS" IMMEDIATE
 429E 35D7 " IMMEDIATE
 42A0  84D STATE
 42A2 12BF @
 42A4  CE6 ?BRANCH 42AA
 42A8 1F68 COMPILE
 42AA 2919    DOS
 42AC  B6F EXIT
10
 OK

If interpreting, " parses the string in the text stream up to a delimiting " character and stores it as a counted string at PAD then leaves the address of PAD on the data stack.
STATE @ leaves a zero on the stack so ?BRANCH branches around COMPILE and DOS is executed.
If compiling, " compiles (") into the new definition and parses the string as before. This time the string is compiled into the new definition as an inline string.
STATE @ leaves a -1 ( TRUE ) on the data stack so ?BRANCH does not branch over COMPILE . COMPILE is executed and compiles DOS into the new definition and advances IP past DOS . Suppose I wanted a word to reset the current disk drive so I would not have to type this:
Code:
DOS" UJ"

It doesn't save much typing, but it's just an example. Here is how it would be defined:
Code:
: DRESET
   DOS" UJ" ;

And here is what it would look like to my decompiler:
Code:
SEE DRESET
DRESET
 5760 1A8C (") UJ
 5765 2919 DOS
 5767  B6F EXIT
9
 OK

As I understand it, POSTPONE tries to encompase the behavior of both COMPILE and [COMPILE] . It tests to see if the following word , the parsed word, is immediate. If it is then the parsed word is compiled into the new definition. If the following word is not immediate then POSTPONE compiles something like COMPILE and the parsed word.
Here is a definition for POSTPONE that I threw together for demonstration purposes:
Code:
: POSTPONE
   ' DUP >NAME C@ $40 AND 0=
   IF
      COMPILE COMPILE
   THEN
   , ; IMMEDIATE

and some sample useage:
Code:
: IF2
   POSTPONE IF ; IMMEDIATE

Code:
SEE IF2
IF2 IMMEDIATE
 5773 2D21 IF IMMEDIATE
 5775  B6F EXIT
4
 OK

Code:
: IF3
   POSTPONE ?BRANCH >MARK ; IMMEDIATE

Code:
SEE IF3
IF3 IMMEDIATE
 5781 1F68 COMPILE
 5783  CE6    ?BRANCH
 5785 2CC4 >MARK
 5787  B6F EXIT
8
 OK

There is another reason I don't like POSTPONE . My assembler's control flow words are immediate, but they don't have to be since assembly occurs during interpretation. Here is the source for the assembler's WHILE
Code:
: WHILE  ( A1 C1 -- A2 C2 A1 C1 )
   [COMPILE] IF 2SWAP ; IMMEDIATE

That [COMPILE] in the definition will have the same effect independent of the immediacy of the assembler's IF . The assembler's IF gets compiled either way.
Suppose I had POSTPONE in my Forth permanently and used it in place of [COMPILE] and COMPILE.
The source for the assembler's WHILE would be:
Code:
: WHILE  ( A1 C1 -- A2 C2 A1 C1 )
   POSTPONE IF 2SWAP ; IMMEDIATE

If the assembler's control flow words were not immediate, the assembler's WHILE would have to be rewritten to remove POSTPONE and it isn't the only word that would need rewritten.


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Tue Mar 23, 2021 7:51 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 895
GARTHWILSON wrote:

The CO word above defined as a secondary would have very limited usefulness if any though, because your return address gets changed. I have RSWAP defined in my '816 Forth as:
Code:
       HEADER "RSWAP", NOT_IMMEDIATE   ; Swaps the two top return-stack cells.
RSWAP: PRIMITIVE
       LDA   1,S
       STA   N
       LDA   3,S
       STA   1,S
       LDA   N
       STA   3,S
       GO_NEXT
 ;-------------------
which executes much, much faster and doesn't mess up where it comes back to for the next instruction.


The purpose of CO is to swap the return addresses. It's a co-routine word. If I had RSWAP in my system, I would define CO like this:
Code:
: CO
   RSWAP ;

If I wanted to make CO a primitive, since Fleet Forth is an ITC Forth, I would have to write it to swap the top of the return stack with IP.
Here are two words used for a trivial example:
Code:
: COROUTINE1
   BEGIN
      CR ." ROUTINE ONE HERE!"
      CO DONE?
   UNTIL ;
: COROUTINE2
   BEGIN
      CR ." SECOND COROUTINE!"
      CO DONE?
   UNTIL ;

And a portion of the system log showing their execution:
Code:
' COROUTINE1 >BODY >R COROUTINE2
SECOND COROUTINE!
SECOND COROUTINE!
ROUTINE ONE HERE!
SECOND COROUTINE!
ROUTINE ONE HERE!
SECOND COROUTINE!
ROUTINE ONE HERE!
SECOND COROUTINE!
ROUTINE ONE HERE!

or they can be launched another way:
Code:
: ROUTINES
   COROUTINE1  COROUTINE2 ;

And a sample run:
Code:
ROUTINES
ROUTINE ONE HERE!
SECOND COROUTINE!
SECOND COROUTINE!
ROUTINE ONE HERE!
SECOND COROUTINE!
ROUTINE ONE HERE!
SECOND COROUTINE!
ROUTINE ONE HERE!
SECOND COROUTINE!
ROUTINE ONE HERE!

A trivial example to be sure.
Here is something useful. Fleet Forth has the word RB ( restore base ). Among other places, it is used in the definition of NUMBER? .
Code:
: RB  ( -- )
   BASE @ R> 2>R  CO
   R> BASE ! ;

The behavior of a word like RB is fairly straight forward. perform the part before CO right now. Perform the part after CO when this word's caller exits.
Here is a walkthrough of how RB works:
Code:
   BASE @ R> 2>R

The value in BASE is fetched then the return address of RB's caller is moved from the return stack to the data stack. Since 2>R preserves the order of the values moved to the return stack, after 2>R executes, the value of BASE will be tucked under the return address of RB's caller on the return stack.
Code:
: CO   2R> SWAP 2>R ;

When at the start of CO , there are three values of immediate interest on the return stack. The value of BASE , the return address of RB's caller, and the return address of RB .
Code:
   2R> SWAP 2>R ;

CO swaps the two return addresses. When CO exits, it exits into RB's caller at the point just after RB .

When RB's caller finishes and exits, it exits into RB just after CO .
Code:
   R> BASE ! ;

The original value of BASE saved to the return stack by RB is now the top value of the return stack. It is pulled off the return stack and stored back in BASE . RB exits into the word that called RB's caller.

Another useful place for CO is in the definition of WITH-WORDS , a handy utility word in Fleet Forth.
WITH-WORDS works like this:
Code:
: NAMEX  <DO THIS ONCE> WITH-WORDS <DO THIS ONCE FOR EACH WORD IN THE CONTEXT VOCABULARY> ;

The thread of Forth after WITH-WORDS gets executed once for each word in the context vocabulary. The only values on the data stack when that thread executes are whatever was on the data stack prior to the execution of WITH-WORDS and the NFA of the word for which this thread is executed.


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Thu Mar 25, 2021 5:51 am 
Offline
User avatar

Joined: Sun Jun 30, 2013 10:26 pm
Posts: 1949
Location: Sacramento, CA, USA
JimBoyd wrote:
Here is something useful. Fleet Forth has the word RB ( restore base ). Among other places, it is used in the definition of NUMBER? .
Code:
: RB  ( -- )
   BASE @ R> 2>R  CO
   R> BASE ! ;

The behavior of a word like RB is fairly straight forward. perform the part before CO right now. Perform the part after CO when this word's caller exits.

That's too cool for school!

_________________
Got a kilobyte lying fallow in your 65xx's memory map? Sprinkle some VTL02C on it and see how it grows on you!

Mike B. (about me) (learning how to github)


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Sun Apr 11, 2021 6:23 pm 
Offline

Joined: Sun May 13, 2018 5:49 pm
Posts: 255
I think RB and CO are my two new favorite words.
I've always saved and restored the base myself in words that needed to change it, but that solution is a lot more elegant. I especially enjoy how simple both words are. RB is also, by far, the most practical use of co-routines I have seen.


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Sat May 01, 2021 1:22 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 895

I cover coroutines in greater depth here.


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Sat May 01, 2021 1:29 am 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 895

I tested CO and RB on Durex Forth for the C64. Durex Forth is an STC Forth. Both words needed a slight redefinition because Durex Forth didn't have 2>R and 2R> .
Code:
: CO   R> R> SWAP >R >R ;
: RB   R> BASE @ >R >R  CO  R> BASE ! ;

These and the GREET HELLO examples worked as expected.


Top
 Profile  
Reply with quote  
 Post subject: Re: The coolest words
PostPosted: Wed Aug 28, 2024 11:40 pm 
Offline

Joined: Fri May 05, 2017 9:27 pm
Posts: 895
CO in my Forth has been a primitive for some time. Here is an excerpt from my Forth kernel's source. It was built with a metacompiler.
The three sections of code below are presented in the reverse order they appear in the kernel source. I felt this would make the source for CO easier to follow.
Code:
CODE CO  (  R: ADR1 -- ADR2 )
         ( IP: ADR2 -- ADR1 )
   PLA  TAY  PLA
   (CO) JMP  END-CODE

   LABEL (CO)    ( IN THE BODY OF >FORTH)
   N STA
   IP 1+ LDA  PHA
   IP    LDA  PHA
   N LDA  IP STY
   ' EXIT @ 4 + JMP  ( THIS JMP)

   ( JUMPS HERE)
   IP 1+ STA
   ( FALLS INTO NEXT)

The following hand translation is provided for those not familiar or comfortable with RPN assembly.
Code:
CODE CO  (  R: ADR1 -- ADR2 )
         ( IP: ADR2 -- ADR1 )
   PLA
   TAY
   PLA
   JMP (CO)
   END-CODE

   LABEL (CO)    ( IN THE BODY OF >FORTH)
   STA N
   LDA IP+1
   PHA
   LDA IP
   PHA
   LDA N
   STY IP
   JMP EXIT4   ( THIS JMP)

   ( JUMPS HERE)
   LABEL EXIT4
   STA IP+1
   ( FALLS INTO NEXT)

As I mentioned previously, CO swaps IP with the top of the return stack.
By taking advantage of what was already in another primitive, the SUBRoutine >FORTH , the primitive version of CO is smaller than the high level version.


Top
 Profile  
Reply with quote  
Display posts from previous:  Sort by  
Post new topic Reply to topic  [ 16 posts ]  Go to page 1, 2  Next

All times are UTC


Who is online

Users browsing this forum: No registered users and 12 guests


You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot post attachments in this forum

Search for:
Jump to:  
cron