colorful rat Ratfactor.com > Dave's Repos

meow5

A stack-based pure inlining concatenative programming language written in NASM assembly
git clone http://ratfactor.com/repos/meow5/meow5.git

meow5/log07.txt

Download raw file: log07.txt

1 Welcome back! In the last log, I got string literals 2 working with the 'quote' word. 3 4 One thing lead to another and I ended up solving a lot 5 of different problems so that now I can define words 6 not just from "primitives" (machine code words written 7 in assembler), but also words composed from *other* 8 non-primitive words. 9 10 So yeah, you can only make programs that print strings, 11 but they are real programs. 12 13 Okay, so now the fun TODOs! 14 15 [ ] Pretty-print meta info about word! 16 [ ] Loop through dictionary, list all names 17 [ ] Loop through dictionary, pretty-print all 18 19 (And I'm not being sarcastic or something - I'm really 20 looking forward to these.) 21 22 First, I need general number handling! I need to parse 23 numbers from input and I need to be able to print 24 numbers as strings. 25 26 [ ] New word: str2num (ascii to integer) 27 [ ] New word: num2str (integer to ascii) 28 29 I'd also like to have the ability to intuitively append 30 strings. Ideally: 31 32 "There are " 5 " chickens." 33 "Your name is '" username "'?" 34 35 (I also don't have variables yet, so the username one is 36 even more hypothetical. But you get the idea.) 37 38 My new 'str2num' word seems to work. I realized I could 39 use the exit status of the epplication since it is 40 already popping a number from the stack (which I'd 41 completely forgotten about, ha ha). 42 43 I updated my `mr` (meow5 run) alias to print the exit 44 status: 45 46 alias mr=`./build.sh run ; echo "Exit status: $?"' 47 48 The current parse/print number radix is set in a new 49 "variable" currently set to decimal on startup: 50 51 mov dword [radix], 10 52 53 And created these words to manage the radix: 54 55 radix (takes number from stack) 56 dec - sets radix to 10 57 hex - sets radix to 16 58 oct - sets radix to 8 59 bin - sets radix to 2 60 61 Now I'll try str2num out with a decimal number: 62 63 42 exit 64 65 $ mr 66 Exit status: 42 67 68 And I'll mess with it by giving it a hex number without 69 updating the radix to hex: 70 71 ff exit 72 73 $ mr 74 Could not find word "ff" while looking in IMMEDIATE mode. 75 Exit status: 1 76 77 Now I'll compact the results a bit to not take up too 78 much room here: 79 80 hex ff exit <-- hexadecimal 81 Exit status: 255 82 83 84 oct 100 exit <-- octal 85 Exit status: 64 86 87 bin 101010 exit <-- binary 88 Exit status: 42 (uh...seems right) 89 bin 11111111 exit 90 Exit status: 255 (yup, that's definitely right) 91 92 Alright! Now this is getting fun. It'll be even better 93 when it's taking input from STDIN to be fully 94 interactive. 95 96 Number printing is interesting. So I could just have it 97 print numbers when requested, like Forth's DOT (.) word. 98 But to print some mixed strings and numbers would look 99 like this: 100 101 "You own " print 16 printnum " snakes." print 102 103 And like I wrote above in this log, I'd rather have 104 strings and numbers automatically appended like this: 105 106 "You own " 16 " snakes." print 107 108 But I'm not sure if that's a good idea or not because 109 I'm going to either have to further complicate my 110 interpreter (it's already doing look-ahead for quotes so 111 I don't have to have a space like in Forth: " foo") or 112 some up with something else. 113 114 One thing I've thought about is having interpolated 115 strings. As long as I have some unambiguous symbol, I 116 could have it pop numbers off the stack and append them 117 to the string as I go: 118 119 16 "You own $ snakes." print 120 121 I planed to have escapes anyway, so \$ for a literal '$' 122 is no problem. 123 124 Okay, I like that. 125 126 So I need to write num2str now and then add number 127 interpolation to my 'quote' word. This is gonna be cool! 128 129 Next night: Wrote num2str. I've got a division error. 130 Here's how I'm testing: 131 132 ; test num2str 133 push 42 ; num 134 push dword [free] ; store here 135 CALLWORD num2str 136 pop eax ; chars written 137 138 First, let's double-check the input: 139 140 (gdb) break num2str 141 528 pop ebp ; address of string destination 142 num2str () at meow5.asm:529 143 529 pop eax ; number 144 num2str () at meow5.asm:530 145 530 mov ecx, 0 ; counter of digit characters 146 531 mov ebx, [var_radix] 147 532 mov edx, 0 148 (gdb) p/a $ebp 149 $1 = 0x804b114 <data_area> <-- addr for string storage 150 (gdb) p $eax 151 $2 = 42 <-- num to convert 152 (gdb) p $ebx 153 $3 = 10 <-- radix 154 155 That's all good: 156 157 string storage addr: 0x804b114 158 num to convert: 42 159 radix: 10 160 161 Now we'll start converting. I divide the number by the 162 radix. The remainder is the digit that was less than the 163 radix (so 0-9 for dec, 0-f for hex, etc.). The result of 164 the division is the rest of the number. 165 166 (gdb) s 167 num2str.divide_next () at meow5.asm:534 168 534 idiv ebx ; eax / ebx = eax, remainder in edx 169 (gdb) p $eax 170 $6 = 4 <-- quotient 171 (gdb) p $ebx 172 $7 = 10 <-- radix (double-checking) 173 (gdb) p $edx 174 $8 = 2 <-- remainder (digit to convert) 175 176 Yup, that looks good. We've got the first digit (working 177 lowest to highest) to convert to a character (2) and the 178 rest of the number with the first digit gone (4). 179 180 I convert to the character equivalent and store that on 181 the stack temporarily. This is because I convert 182 starting from the least significant digit, but we want 183 the string to have the number in the expected order, 184 starting with the greatest significant digit. 185 186 (gdb) s 187 535 cmp edx, 9 ; digit bigger than 9? (radix allows a-z) 188 536 jg .toalpha ; yes, convert to 'a'-'z' 189 537 add edx, '0' ; no, convert to '0'-'9' 190 538 jmp .store_char 191 num2str.store_char () at meow5.asm:542 192 542 push edx ; put on stack (pop later to reverse order) 193 543 inc ecx 194 544 cmp eax, 0 ; are we done converting? 195 545 jne .divide_next ; no, loop 196 197 Now it's on to the next division: 198 199 Program received signal SIGFPE, Arithmetic exception. 200 num2str.divide_next () at meow5.asm:534 201 534 idiv ebx ; eax / ebx = eax, remainder in edx 202 203 There it is. Well, I'm not dividing by 0, so that's 204 good. But then I remember idiv is signed division, and 205 I'm not doing signed integers just yet. Maybe that's it? 206 207 Ah, a quick look at a reference: 208 209 "32-bit division with IDIV requires EAX be 210 sign-extended into EDX." 211 212 Yeah, I don't want to do signed division anyway because 213 I don't have the ability to parse or print negative 214 numbers (yet?) so I should really be using DIV anyway: 215 216 Program received signal SIGFPE, Arithmetic exception. 217 218 Nope! I know this isn't a divide-by-zero problem... 219 220 Oh, I needed to read a bit more: 221 222 "DIV Always divides the 64 bits value accross 223 EDX:EAX by a value." 224 225 Oh, right, I need to clear EDX to 0 in my division loop 226 because it currently holds the character to convert. It 227 makes total sense that the answer is overflowing. I was 228 dividing by a huge 64-bit number the second time around! 229 230 So I just need to clear edx before my division: 231 232 533 mov edx, 0 ; div actually divides edx:eax / ebx! 233 534 div ebx ; eax / ebx = eax, remainder in edx 234 535 cmp edx, 9 ; digit bigger than 9? (radix allows a-z) 235 (gdb) p $eax 236 $1 = 0 237 (gdb) p $edx 238 $2 = 4 239 240 That's got it! The final digit (remainder after 241 division) is 4 and there's nothing left to divide after 242 that (quotient is 0). 243 244 Once it sees the rest of the number is now 0, I store 245 the string at the address provided and return the number 246 of characters written (digits in radix) on the stack: 247 248 554 push eax ; return num chars written 249 end_num2str () at meow5.asm:149 250 149 mov eax, [return_addr] ; RETURN 251 (gdb) p $eax 252 $1 = 2 253 (gdb) x/s $ebp 254 0x804b114 <data_area>: "42" 255 256 Correct! Two characters were written, and the string is 257 "42", my value in decimal! 258 259 Since I can now, I'll make the test print real nice and 260 use the exit value to show the number of digits written 261 (to make sure it's correct): 262 263 ; test num2str 264 CALLWORD decimal 265 push dword 42 ; num 266 push dword [free] ; store here 267 CALLWORD num2str 268 PRINTSTR "Answer: " 269 push dword [free] ; print from here 270 CALLWORD print 271 CALLWORD newline 272 CALLWORD exit ; stack still has chars written 273 274 $ mr 275 Answer: 42 276 Exit status: 2 277 278 Correct. Let's see some others: 279 280 CALLWORD oct 281 push dword 64 282 283 Answer: 100 284 Exit status: 3 285 286 CALLWORD hex 287 push dword 3735928559 288 289 Answer: deadbeef 290 Exit status: 8 291 292 CALLWORD bin 293 push dword 7 294 295 Answer: 111 296 Exit status: 3 297 298 CALLWORD bin 299 push dword 257 300 301 Answer: 100000001 302 Exit status: 9 303 304 Looks good! Now I'm kinda wishing the interpreter was 305 interactive because that would have been way more fun 306 than changing the assembly test for each run. But 307 that'll be coming up soon enough. 308 309 After sleeping on it, I'm really excited about the idea 310 of string interpolation using '$' as a placeholder. So 311 I'll make that the next todo: 312 313 [ ] Add "$" placeholders so the 'quote' word so it 314 can interpolate numbers from the stack into the 315 string. 316 317 I suspect the hard part will be remembering to push the 318 stack values in the opposite order that they appear in 319 the string...so we'll see what that's like in practice. 320 321 Okay, I've got it working, but only just barely. It 322 feels incredibly fragile. In particular, the interpreter 323 has become way too complicated for my taste. Juggling 324 registers willy-nilly and using the stack when I run out 325 of places to put things is just sloppy. 326 327 It does work. Cleaned up example: 328 329 42 "The answer is $." print 330 42 hex "The answer is 0x$ in hex." print 331 42 bin "The answer is $ in computer." print 332 42 oct "The answer is $ in octal." print 333 334 Output: 335 336 The answer is 42. 337 The answer is 0x2a in hex. 338 The answer is 101010 in computer. 339 The answer is 52 in octal. 340 341 Which is really cool. But I need to get my mess under 342 control. 343 344 For one thing, I've been using the registers very 345 inconsistantly because I don't have any plan regarding 346 where to put stuff. 347 348 This article has helped a ton: 349 350 https://www.swansontec.com/sregisters.html 351 352 I'm not sure I'm going to re-write EVERYTHING to 353 follow these guidelines, but new stuff definitely will 354 and I'm going to immediately switch the 'quote' word to 355 use the esi and edi registers! 356 357 Using esi and edi in the 'quote' word helped make the 358 usage much clearer and simplified some of the address 359 manipulation considerably (the source and destination 360 pointers aren't always moving in sync because escapes 361 and placeholders in the source string may expand to a 362 different number of characters in the destination 363 string. 364 365 Another thing that makes the interpreter so fragile is 366 the way I have to juggle registers and the stack around 367 to check for a valid numeric literal when an input token 368 doesn't match any of the exisitng words. 369 370 So what I'm thinking is that just like quote, I'll test 371 for tokens that start with a number and handle them 372 _before_ trying to find them in the dictionary. 373 374 Consequently, I won't be able to start any word or 375 variable names with a number. (Like Forth allows.) 376 In exchange, it should simplify the interpreter a fair 377 amount in some messy areas. 378 379 [ ] Break out 'number' into separate word that does 380 pre-check like 'quote'. 381 382 This will also give it a nice symetry with the way I'm 383 handling string literals (my first big departure from 384 pur Forth syntax). 385 386 Three sleepy nights later: Well, it works...but I've now 387 exposed another problem. 388 389 This is what the top of my interpreter loop looks like: 390 391 get_next_token: 392 CALLWORD eat_spaces ; skip whitespace 393 CALLWORD quote ; handle any string literals 394 CALLWORD number ; handle any number literals 395 CALLWORD get_token 396 397 It's nice and neat and readable. The problem is that it 398 doesn't work unless you have an optional quote followed 399 by an optional number followed by a non-string, 400 non-quote token. 401 402 Two strings in a row or a number followed by a string or 403 various other combos won't work. 404 405 Here's what needs to happen: 406 407 interpreter loop: 408 eat spaces 409 if quote 410 make string literal 411 restart interpreter loop 412 if number 413 make number literal 414 restart interpreter loop 415 if token 416 find/execute/compile 417 restart interpreter loop 418 else 419 error 420 421 So as much as I liked having 'quote' and 'number' handle 422 all aspects of those respective inputs, it's just making 423 everying too interdependant. I need to break them up 424 into even smaller words and give more control back to 425 the interpreter where it belongs. 426 427 So more refactoring awaits me before I can do the fun 428 stuff. Ah well, this is where the "slow and steady" 429 approach wins - I know I'll eventually push through this 430 kinda boring housekeeping stuff. 431 432 Next night: Progress made on that refactor. Then there's 433 a segfault. So that's what I'll work on tomorrow night. 434 435 Next night: Looks like the segfault is happening in old 436 code (specifically inline, but because of wrong 437 calculations in semicolon ";"). Since the old code won't 438 just start failing for no reason, this has to be due to 439 bad input from all the changes I've made in the outer 440 interpreter loop. 441 442 Hard to track down with GDB with breakpoints and watch 443 statements. So I added some print debugging and sure 444 enough: 445 446 COLON here: 0804a290 447 inlining print 448 SEMICOLON there: 0804b2e6 449 450 That's the problem. 'colon' is trying to save the 451 machine code start address for 'meow' on the stack and 452 'semicolon' should be getting that same value, but it's 453 not. It's while executing this, by the way: 454 455 : meow "Meow." print ; 456 457 The botched semicolon tail writing is why trying to use 458 'meow' in 'meow5' fails - the calculations are wildly 459 wrong (it thinks the machine code length is -4144). 460 461 So I need to figure out where I'm leaving something on 462 the stack. But how? 463 464 Oh, I've got an idea, That wrong value from the stack 465 looks like an address. I'll fire up GDB and see what's at 466 that address. Maybe that will help. 467 468 (gdb) x 0x0804b2e6 469 0x804b2e6: 0x776f654d 470 471 Hmmm. Looks like a string? 472 473 (gdb) x/s 0x0804b2e6 474 0x804b2e6: "Meow." 475 476 Aha! Interesting. It's the address from the string 477 literal before the print. What's that doing there? In 478 compile mode, 'quote' should be compiling that address 479 into the new word's machine code memory, not on the 480 stack. 481 482 Oh, right, I knew that would come back to haunt me. At 483 some point, my run/compile logic got lost (could be a 484 dozen commits ago by now). I'm currently compiling the 485 instruction to push the string's address *and* I'm also 486 actually pushing the string's address every single time. 487 So this has actually been broken for a while and my 488 refactoring has (almost?) nothing to do with it. 489 490 This is why I need a set of proper regression tests 491 ASAP. Well, I'll get there soon. Gotta be able to take 492 input before I can do that! 493 494 Anyway, time to add (back) some conditional jumps in 495 'quote'! 496 497 The answer is 42. 498 The answer is 0x2a in hex. 499 The answer is 101010 in computer. 500 The answer is 52 in octal. 501 Meow.Meow.Meow.Meow.Meow. 502 503 Much better. 504 505 And I was reminded that I had been in the middle of 506 adding escape sequences to string literals! Let's try 507 them out: 508 509 42 "I paid \$$ for beans\\cheese.\nOkay?\n" print 510 511 $ mr 512 I paid $42 for beans\cheese. 513 Okay? 514 515 Wow, that was super satisfying. 516 517 Okay, I think *now* I'm finally ready to make those 518 introspective word-printing words! 519 520 Next night: Uh, so I didn't think far enough ahead. I 521 made some really nice string making and printing 522 facilities, but I can't use them easily from assembly. 523 Nor can I define something as complex as a "dictionary" 524 printing word (to use the Forth term for all the defined 525 words.) 526 527 Hmmm... this is a quandary. 528 529 Okay, it wasn't so bad. I just needed to write an 530 additional word 'printnum' (with macro PRINTNUM_CODE) to 531 help it out. 532 533 After that, this is all it takes to write an 'inspect' 534 that takes a word tail address and prints the word name 535 and bytes of machine code (from the tail metadata): 536 537 DEFWORD inspect 538 pop esi ; get tail addr 539 lea eax, [esi + T_NAME] 540 push eax 541 PRINT_CODE 542 PRINTSTR ": " 543 mov eax, [esi + T_CODE_LEN] 544 push esi ; preserve tail addr 545 ; param 1: num to be stringified 546 push eax 547 PRINTNUM_CODE 548 PRINTSTR " bytes " 549 NEWLINE_CODE 550 ENDWORD inspect, 'inspect', (IMMEDIATE) 551 552 553 Here it is printing the word 'find': 554 555 "find" find inspect 556 557 find: 58 bytes 558 559 Neat! 560 561 Oh, and I guess I can show my new 'printnum' in 562 standalone action: 563 564 bin 11101111 hex printnum 565 566 ef 567 568 Nice. 569 570 +--------------------------------------------------------+ 571 | | 572 | Just so it's clear I'm not trying to trick anybody, | 573 | the interpreter input is still hard-coded into | 574 | memory, so that calling line really looks like this | 575 | in assembly: | 576 | | 577 | db 'bin 11101111 hex printnum ' | 578 | | 579 | I'm just trying to make the log readable, so I leave | 580 | off the 'db' and quotes. | 581 | | 582 | I'll take input on STDIN very soon (next!). | 583 | | 584 +--------------------------------------------------------+ 585 586 And now I can find out how big a "meow" printing 587 function is in machine code (minus the "Meow." string 588 itself, because that's stored elsewhere. And also the 589 combined five meows: 590 591 "meow" find inspect 592 "meow5" find inspect 593 594 meow: 38 bytes 595 meow5: 190 bytes 596 597 There we go. So even repeating the entire printing 598 routine five times is still under 200 bytes of machine 599 code. When's the last time you shed a tear over 200 600 bytes? 601 602 Okay, now I just need a loop to call inspect for every 603 defined word and I'm all set. 604 605 I'm not sure I want to use the term "dictionary" from 606 Forth (or "word", for that matter). Hmm, I guess I can 607 avoid the issue by just calling it 'inspect_all'... 608 609 Almost there on inspect_all. It seems something has 610 left the token str addr on the stack and it's getting in 611 the way of popping the link addr and ruining everything. 612 At least, that's what it looks like. 613 614 Next night: So what I woke up thinking in the morning is 615 that if I'm not maintaining my stack correctly between 616 words, there are three ways I could debug this: 617 618 1. Painfully, with GDB stepping through...a lot. 619 620 2. Write a DEBUG_STACK macro that non-destructively 621 prints the values on the stack from anywhere within 622 the assembly. 623 624 3. Write a script (perhaps in awk?) that checks the 625 push/pop balance by following all of the called or 626 inlined code and incrementing a counter for each push 627 and decrementing for each pop. 628 629 Obviously I'm trying to avoid Option 1. 630 631 Option 3 sounds neat, but I'm not sure how to *display* 632 that information so that it helps me track down the 633 problem - some words push more than they pop and 634 vice-versa. It'd still be a lot of manual checking. 635 636 Option 2 seems generally useful anyway, so I'm going to 637 give that a shot and hope that peppering it about the 638 program will help me figure out what's going on. 639 640 Hmmm...well, printing the stack at the Meow5 language 641 level is no problem, but printing it at the assembly 642 level is a big problem. I've got a chicken-and-egg 643 problem where I need to preserve the registers it uses 644 AND preserve the stack. 645 646 I could preserve the registers in a purpose-built bit of 647 memory. 648 649 Well, I think I did that right. Here's the whole ugly 650 thing: 651 652 section .bss 653 ds_eax: resb 4 654 ds_ebx: resb 4 655 ds_ecx: resb 4 656 ds_edx: resb 4 657 ds_esi: resb 4 658 ds_edi: resb 4 659 section .text 660 %macro DEBUG_STACK 0 661 mov [ds_eax], eax 662 mov [ds_ebx], ebx 663 mov [ds_ecx], ecx 664 mov [ds_edx], edx 665 mov [ds_esi], esi 666 mov [ds_edi], edi 667 PRINTSTR "Stack: " 668 PRINTSTACK_CODE 669 NEWLINE_CODE 670 mov eax, [ds_eax] 671 mov ebx, [ds_ebx] 672 mov ecx, [ds_ecx] 673 mov edx, [ds_edx] 674 mov esi, [ds_esi] 675 mov edi, [ds_edi] 676 %endmacro 677 678 But talk about chicken-and-egg problem. Or is it more 679 of a "who watches the watchmen?" thing? Or maybe there's 680 an even better expression for it, but my problem is that 681 I think I'm acutally calling whatever is doing bad stack 682 things WHILE trying to print the stack... :-( 683 684 Okay, it's worse than even that. I don't know how I've 685 gotten away with it this long, but I've been using a 686 bunch of words mid-assembly to debug stuff...and a lot 687 of them aren't safe for that (like 'newline') because 688 they don't preserve the registers. 689 690 So it's possible that by trying to observe what's going 691 on for the 'inspect_all' feature, I've been ruining it! 692 693 I have to remember that most of my words are only safe 694 if I'm treating them as if they're being called from the 695 language - transfering values on the stack and not 696 giving a hoot what happens to the registers in between! 697 698 Okay, I think I got it. No idea how this got here, but 699 I had an extra pop for no reason I could understand in 700 'printnum'! I have no idea how it got there, but that 701 would definitely mess things up: 702 703 pop esi ; get preserved <-----?????? 704 705 I think the lesson is to test all new words more 706 thoroughly before moving on! 707 708 Okay, can we _finally_ see the output of 'inspect_all'? 709 710 $ mr 711 Meow. Meow. Meow. Meow. Meow. 712 meow5: 190 bytes IMMEDIATE COMPILE 713 meow: 38 bytes IMMEDIATE COMPILE 714 inspect_all: 359 bytes IMMEDIATE 715 inspect: 340 bytes IMMEDIATE 716 ps: 177 bytes IMMEDIATE COMPILE 717 printmode: 100 bytes IMMEDIATE COMPILE 718 printnum: 116 bytes IMMEDIATE COMPILE 719 number: 306 bytes IMMEDIATE COMPILE 720 decimal: 10 bytes IMMEDIATE COMPILE 721 bin: 10 bytes IMMEDIATE COMPILE 722 oct: 10 bytes IMMEDIATE COMPILE 723 hex: 10 bytes IMMEDIATE COMPILE 724 radix: 6 bytes IMMEDIATE COMPILE 725 str2num: 95 bytes IMMEDIATE COMPILE 726 quote: 247 bytes IMMEDIATE COMPILE 727 num2str: 58 bytes IMMEDIATE COMPILE 728 ;: 150 bytes COMPILE RUNCOMP 729 return: 7 bytes IMMEDIATE 730 :: 137 bytes IMMEDIATE 731 copystr: 18 bytes IMMEDIATE COMPILE 732 get_token: 55 bytes IMMEDIATE 733 eat_spaces: 38 bytes IMMEDIATE COMPILE 734 find: 58 bytes IMMEDIATE 735 is_runcomp: 5 bytes IMMEDIATE COMPILE 736 get_flags: 7 bytes IMMEDIATE COMPILE 737 inline: 25 bytes IMMEDIATE 738 print: 33 bytes IMMEDIATE COMPILE 739 newline: 26 bytes IMMEDIATE COMPILE 740 strlen: 16 bytes IMMEDIATE COMPILE 741 exit: 8 bytes IMMEDIATE COMPILE 742 Goodbye. 743 Exit status: 0 744 745 Yay! At last! Every word in the system with the number 746 of bytes of machine code and mode(s). 747 748 And with the help of the shell, we can sort by size. 749 750 $ ./meow5 | awk ' 751 /^.*: [0-9]+/ {t=t+$2; print $2, $1} 752 END{print "Total bytes:", t} 753 ' | sort -n 754 755 5 is_runcomp: 756 6 radix: 757 7 get_flags: 758 7 return: 759 8 exit: 760 10 bin: 761 10 decimal: 762 10 hex: 763 10 oct: 764 16 strlen: 765 18 copystr: 766 25 inline: 767 26 newline: 768 33 print: 769 38 eat_spaces: 770 38 meow: 771 55 get_token: 772 58 find: 773 58 num2str: 774 95 str2num: 775 100 printmode: 776 116 printnum: 777 137 :: 778 150 ;: 779 177 ps: 780 190 meow5: 781 247 quote: 782 306 number: 783 340 inspect: 784 359 inspect_all: 785 786 Total bytes: 2655 787 788 (Command line formatted for semi-readability and total 789 bytes separated out for clarity.) 790 791 So the smallest word is 'is_runcomp', which tests for 792 the RUNCOMP flag in a mode and pushes the result. 793 794 The largest word is not a surprise to me, 'inspect_all', 795 which contains 'inspect' and puts a loop around it. In 796 turn, 'inspect' includes a bunch of string and number 797 printing code. 798 799 So let's see how I did: 800 801 [x] Pretty-print meta info about word! 802 [ ] Loop through dictionary, list all names 803 [x] Loop through dictionary, pretty-print all 804 [x] New word: str2num (ascii to integer) 805 [x] New word: num2str (integer to ascii) 806 [x] Add "$" placeholders so the 'quote' word so it 807 can interpolate numbers from the stack into the 808 string. 809 [x] Break out 'number' into separate word that does 810 pre-check like 'quote'. 811 812 I forgot about a loop that just prints all the word 813 names. And 'number' works a little differently now than 814 I'd envisioned. But I'm gonna call this part done for 815 now and close out this log file.