Dave's nasmjf Dev Log 23
Created: 2022-07-22
This is an entry in my developer’s log series written between December 2021 and August 2022 (started project in September). I wrote these as I completed my port of the JONESFORTH assembly language Forth interpreter.
Well, now that I can actually run jonesforth.f with this
interpreter port, it "only" remains to test the rest of
the words defined in Forth. Then I've got some cleaning
up to do in the assembly.
It looks like I'm roughly halfway through testing what
jonesforth.f provides. So there's quite a bit left.
Next up is CASE OF...ENDOF... ENDCASE which lets you
do different things based on comparison with a value on
the stack:
JONESFORTH VERSION 1
20643 CELLS REMAINING
OK
: foo CASE
1 OF ." One" ENDOF
2 OF ." Two" ENDOF
3 OF ." Three" ENDOF
ENDCASE ;
1 foo
One
2 foo
Two
3 foo
Three
That is, dare I say, quite readable compared to a lot of
the Forth we've seen so far.
Next is CFA>, which is "the opposite of >CFA." Let's see
about that. I'll use >CFA to get the code word pointer
address from the LATEST word address, then get back to
the original word address with CFA> and see if it's right:
HEX
LATEST @ DUP . ID.
957ED74 foo
LATEST @ >CFA DUP .
957ED7C
CFA> DUP . ID.
957ED74 foo
Of course, we can do this all in one go as well:
LATEST @ >CFA CFA> ID.
foo
What's wild is that >CFA can just do a little address
math and skip forward to the codeword. But CFA> has to
search the entire dictionary for the correct word.
I feel like you should be able to scan backward to the
word start. But I can see how it would be hard to be
entirely sure it's accurate (you could use the length
field as a kind of "checksum" for the name field, but
that wouldn't be a *guarantee*).
Oooooh, the next one is bound to be a new favorite: SEE
decompiles a word!
SEE foo
: foo 1 OVER = 0BRANCH ( 20 ) DROP S" One" TELL BRANCH ( 74 ) 2 OVER = 0BRANCH
( 20 ) DROP S" Two" TELL BRANCH ( 40 ) 3 OVER = 0BRANCH ( 24 ) DROP S" Three"
TELL BRANCH ( 8 ) DROP ;
That's awesome! I love how it prints the strings and
everthing.
The next thing are anonymous "no name" words. When you
define them with :NONAME, they leave the address of
their codeword on the stack. In Forth terminology, the
address of a codeword is an "execution token". In the
21st Century, we'd probably call them pointers.
Anyway, you can store an execution token like any other
value. And EXECUTE calls one:
:NONAME ." I have no name: " TELL CR ;
VALUE noname
noname .
160980384
." Hello" noname EXECUTE
HelloI have no name: .
S" Hello" noname EXECUTE
I have no name: Hello
Next we have THROW and CATCH. I was not expecting
exception handling!
The THROW statement is easy to use - just give it a
number to throw as the exception.
But CATCH needs some very specific stuff:
: <word2> ['] <word1> CATCH IF ... THEN ;
The ['] primitive gets the execution token of a word
(:NONAME uses it).
: foo ." Foo! " 13 = IF ." Bah!" 5 THROW THEN ." Done." ;
12 foo
Foo! Done.
13 foo
Foo! Bah!UNCAUGHT THROW 5
: test-foo ['] foo CATCH ?DUP IF ." Foo threw " . DROP THEN ." Test done." ;
12 test-foo
Foo! Done. Test done.
13 test-foo
Foo! Bah! Foo threw 5 Test done.
i spent another night reading through the implementation
of THROW. It's funny how *using* CATCH and THROW is
inversely proportional to the complexity of those two
words.
This is the first time something really interesting is
done with the return stack.
This is also an excellent example of a language feature
that seems like more trouble than it's worth in a small
demonstration. But when you actually need a feature like
this in a decent-sized codebase, it will seem trivial
compared to the alternatives.
Having said that, reading an implementation like THROW
is just *nuts*. Cool how it works, though.
Next is another one that takes advantage of the return
stack: PRINT-STACK-TRACE.
: foo PRINT-STACK-TRACE ;
foo
foo+0
: bar foo ;
: baz bar ;
baz
foo+0 bar+0 baz+0
Beautiful.
Then JONESFORTH has support for reading the commandline
arguments, which is very cool.
It's easy enough to use ARGC and ARGV, but I'm getting
to the point where the usability of bare Linux input for
the Forth "REPL" is pretty painful. I must have re-typed
my BEGIN WHILE REPEAT loop a dozen times.
So I'm doing two things:
1. Using CAPSLOCK while typing Forth commands.
2. Installed "rlwrap" (Readline wrapper).
rlwrap is super awesome and easy to use. I'll be darned
if I can figure out why `apk search rlwrap` doesn't work
in Alpine Linux. It shows up as a community package when
I do a web search.
But whatever, that's what source is for:
$ doas apk add readline-dev
$ lynx github.com...
$ tar -xf rlwrap...tar.gz
$ cd rlwrap...
$ ./configure
$ make
$ doas make install
Now I've added this alias:
alias f='rlwrap /home/dave/nasmjf/nasmjf'
And what a relief! I can use arrow keys for editing and
history, etc.
eeepc:~/nasmjf$ f
JONESFORTH VERSION 1
20643 CELLS REMAINING
OK
ARGC .
1
0 ARGV TELL
/home/dave/nasmjf/nasmjf
Again, that's my alias 'f' providing the full path to
the nasmjf executable we're seeing there as the 0th
argument.
Now for the simple loop it took at least a dozen tries
to get right (often due to simple typos and some of them
causing nasmjf to segfault!) and now I am *so* grateful
that rlwrap gives me command history.
eeepc:~/nasmjf$ f foo bar
JONESFORTH VERSION 1
20643 CELLS REMAINING
OK
ARGC .
3
: PRINT-ARGS 0 BEGIN DUP ARGC < WHILE DUP DUP . ARGV TELL CR 1+ REPEAT ;
PRINT-ARGS
0 /home/dave/nasmjf/nasmjf
1 foo
2 bar
Yes!
And by the way, I have no doubt I'll get better at it,
but writing that loop felt way harder than it should
have been. The way you have to keep DUPing because
even comparisons eat the values on the stack...
I get it. But I'm not sure I like it.
Also, having capslock on makes my GNU screen shortcuts
and vim both go crazy when i forget to toggle it off
when i'm done typing Forth. :-(
But rlwrap is the best. I *highly* recommend it for use
with this or any other application that doesn't have
command history, etc.
Related to arguments (because Linux leaves it all in our
application's memory in the stack memory space,
evidently) are the environment variables.
JONESFORTH defines ENVIRON to get the address. Also the
utility STRLEN to get the length of null-terminated
strings, a.k.a. "C strings".
ENVIRON @ DUP STRLEN TELL
SHELL=/bin/bash
Unlike the result from ARGV, ENVIRON just gives us the
address of the first item. So I had to DUP and STRLEN to
get the string ready for printing with TELL.
The next item is 4 bytes after that and so on.
I guess you could write a word that gets the number of
environment variables available and another that gets
one from a particular position.
I started writing those words, but it was late and I was
tired. Eventually, I just wanted to see more, so I
printed out a big chunk of memory where args and
environment are stored:
0 ARGV DROP 128 DUMP
BFECBA8E 2E 2F 6E 61 73 6D 6A 66 0 66 6F 6F 0 62 61 72 ./nasmjf.foo.bar
BFECBA9E 0 53 48 45 4C 4C 3D 2F 62 69 6E 2F 62 61 73 68 .SHELL=/bin/bash
BFECBAAE 0 43 48 41 52 53 45 54 3D 55 54 46 2D 38 0 54 .CHARSET=UTF-8.T
BFECBABE 45 52 4D 43 41 50 3D 53 43 7C 73 63 72 65 65 6E ERMCAP=SC|screen
BFECBACE 7C 56 54 20 31 30 30 2F 41 4E 53 49 20 58 33 2E |VT 100/ANSI X3.
BFECBADE 36 34 20 76 69 72 74 75 61 6C 20 74 65 72 6D 69 64 virtual termi
BFECBAEE 6E 61 6C 3A 44 4F 3D 5C 45 5B 25 64 42 3A 4C 45 nal:DO=\E[%dB:LE
BFECBAFE 3D 5C 45 5B 25 64 44 3A 52 49 3D 5C 45 5B 25 64 =\E[%dD:RI=\E[%d
I'll end this particular log file with an appropriate
sign-off with another new word definition:
JONESFORTH VERSION 1
20643 CELLS REMAINING
OK
BYE