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