arrayForth Coding Style
Comments belong in Shadows
Source code should be short and sweet. Comments generally belong in the shadow screen and not inline with the source code. And the purpose of a Shadow comment is not to explain what the definition does. You can get that by reading the source code. The Shadow should explain why, and how you might use the definition.
The arrayForth interpreter only compares the first 4-7 characters in a name when doing a dictionary search. Any characters beyond this should be considered documentation. So, for example, as far as arrayForth is concerned block and blocks are the same word.
Any distinguishing characters should be added to the front of a word rather than to the back. Instead of block? use ?block to suggest that the word leaves a flag on the stack.
While typing a word at the command line in arrayForth, each character typed is packed into the word on top of the stack which you can see in the lower left corner of the display. When you see an extra number appear on the stack you know you've gone too far. What you've typed won't fit into a single word.
Getting Along Without Else
Maybe you've noticed that arrayForth has if but not else. In most cases else compiles a redundant jump instruction. The simplest way to effect an else is to put a return (;) before then. Any code before ; is the if clause and code after then is the else clause. For example:
Test is expected to return a flag. If the flag is true then execute the commented 'if clause' and exit the word. If the flag is false then jump over the 'if clause' and execute the 'else clause'.
Efficient Tail Recursion
ArrayForth is recursive. You can make a recursive call to the word being defined by simply referencing its name. The operator ; will change that call into a jump. The jump avoids making a needless return, saving a return stack space and also saves the space that would have been taken up by the return opcode.
Here's an example of an endless loop:
Another way to look at a call followed by ; is that it's a goto. Don't get confused by the idea of recursion. Just know that if you don't need to return you can change a call to a jump by appending ;.
1 . + forever ;
Suppose you want to make an endless loop but it needs to be initialized first. Tail recursion sends control to the beginning of the word so everything is executed every time. The following is an endless loop similar to the last one, but with some initial values placed on the stack.
1 and 0 are placed on the stack once. Then the begin end executes an endless loop just like the tail recursive version above.
1 0 begin over . + end
begin leaves a pointer on the stack at compile time allowing another word such as end or until to compile a branch back to that address later. until compiles an if instruction that conditionally jumps back to the address left on the stack by begin. There is also -until which compiles the -if instruction instead of an if istruction. Here's an example:
begin -1 . + -until ;
Jump into the Middle of a Loop
Now here's an interesting trick. Suppose that for some reason you want your delay word to stop immediately if it's fed a negative number. You can jump into the middle of the loop like this:
begin -1 . +
The next instruction provides us with a counted loop. A number is placed on the return stack by for and counted down to zero by the next instruction. For example:
Inside the loop we see . which compiles a nop instruction, followed by .. which fills the current instruction word with nops. The next instruction is compiled into the following word of memory.
for . .. next ;
You may find yourself wanting to exit a for next loop early for some reason. Given a word named test that returns a true flag to trigger an early exit:
Note that in order to leave a for next loop early you need to pop the count off the return stack and discard it. Also the if instruction (and -if, until, -until) leaves the flag on the stack. Unless you can afford to abandon the flag (each time through the loop) you'll need to drop it in both forks of the conditional branch.
for test if drop pop drop ; then drop next ;
By the way, for is compiled as push begin and you can use push begin yourself in place of for should you find it useful. Note that other instructions can be placed between push and begin.
You remember that both begin and if leave pointers on the compiler's stack to allow address resolution by words such as end, next, until and then. while compiles the same if instruction that if compiles, but it places the pointer underneath the top of the stack. This allows whatever address was already there to be resolved first. Maybe an example will help:
See how this is a normal for next loop as long as test returns true? When next ends normally it pops the return stack and exits the word at ;. The loop is left early if test returns false. while jumps over next ; to then where the return stack is popped and dropped along with the flag. Of course there is also a -while in arrayForth.
for test while drop next ; then drop pop drop ;
while works with end as well. end then is roughly the same as repeat in classic Forth. Here's an example:
When a 0 (false) is presented to while control jumps to then where the flag is dropped and the word finished. As long as a non-zero value is presented to while the code labeled 'done while true' is executed and the loop counter is decremented. Note that a begin until loop always executes once, as does a for next loop. A begin while end then loop will only execute the code labeled 'done before the test' if the first value encountered by while is zero (false).
done before the test
done while true
-1 . + end then drop ;
Putting a literal on the stack using @p takes up 5 slots. A zero can be placed on the stack with 2 or 3 slots instead:
depending on whether you want to push a new zero onto the stack or just change the top of stack to zero. You can change a 0 to a -1 by inverting all the bits:
dup dup or
Note that the second example takes 5 slots, same as using @p. Sometimes though, you already have zero on the stack, in which case it takes one more slot to make it -1.
dup or -
dup dup or -
Change a 0 to a 1 with:
In fact this is how to shift in a 1, whatever was already on top of the stack. To shift in a zero, just:
- 2* -
or change a 1 to a zero with:
Be on the lookout for opportunities to save some space by converting one literal into another like this.
Save space by falling through
Sometimes you can save space by using multiple entry points. For example you can define:
Notice that you can get the word negate for free by letting abs fall through into negate:
-if - 1 . + then ;
- 1 . + then ;