https://github.com/akkartik/mu/blob/master/apps/tile/environment.mu
   1 type environment {
   2   screen: (handle screen)
   3   functions: (handle function)
   4   sandboxes: (handle sandbox)
   5   nrows: int
   6   ncols: int
   7   code-separator-col: int
   8 }
   9 
  10 fn initialize-environment _env: (addr environment) {
  11   var env/esi: (addr environment) <- copy _env
  12   # initialize some predefined function definitions
  13   var functions/eax: (addr handle function) <- get env, functions
  14   create-primitive-functions functions
  15   # initialize first sandbox
  16   var sandbox-ah/eax: (addr handle sandbox) <- get env, sandboxes
  17   allocate sandbox-ah
  18   var sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
  19   initialize-sandbox sandbox
  20   # initialize screen
  21   var screen-ah/eax: (addr handle screen) <- get env, screen
  22   var _screen/eax: (addr screen) <- lookup *screen-ah
  23   var screen/edi: (addr screen) <- copy _screen
  24   var nrows/eax: int <- copy 0
  25   var ncols/ecx: int <- copy 0
  26   nrows, ncols <- screen-size screen
  27   var dest/edx: (addr int) <- get env, nrows
  28   copy-to *dest, nrows
  29   dest <- get env, ncols
  30   copy-to *dest, ncols
  31   var repl-col/ecx: int <- copy ncols
  32   repl-col <- shift-right 1
  33   dest <- get env, code-separator-col
  34   copy-to *dest, repl-col
  35 }
  36 
  37 fn draw-screen _env: (addr environment) {
  38   var env/esi: (addr environment) <- copy _env
  39   var screen-ah/eax: (addr handle screen) <- get env, screen
  40   var _screen/eax: (addr screen) <- lookup *screen-ah
  41   var screen/edi: (addr screen) <- copy _screen
  42   var dest/edx: (addr int) <- get env, code-separator-col
  43   var tmp/eax: int <- copy *dest
  44   clear-canvas env
  45   tmp <- add 2  # repl-margin-left
  46   move-cursor screen, 3, tmp  # input-row
  47 }
  48 
  49 fn initialize-environment-with-fake-screen _self: (addr environment), nrows: int, ncols: int {
  50   var self/esi: (addr environment) <- copy _self
  51   var screen-ah/eax: (addr handle screen) <- get self, screen
  52   allocate screen-ah
  53   var screen-addr/eax: (addr screen) <- lookup *screen-ah
  54   initialize-screen screen-addr, nrows, ncols
  55   initialize-environment self
  56 }
  57 
  58 #############
  59 # Iterate
  60 #############
  61 
  62 fn process _self: (addr environment), key: grapheme {
  63 $process:body: {
  64   var self/esi: (addr environment) <- copy _self
  65   var sandbox-ah/eax: (addr handle sandbox) <- get self, sandboxes
  66   var _sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
  67   var sandbox/edi: (addr sandbox) <- copy _sandbox
  68   var rename-word-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-cursor-word
  69   var rename-word-mode?/eax: (addr word) <- lookup *rename-word-mode-ah?
  70   compare rename-word-mode?, 0
  71   {
  72     break-if-=
  73 #?     print-string 0, "processing sandbox rename\n"
  74     process-sandbox-rename sandbox, key
  75     break $process:body
  76   }
  77   var define-function-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-function
  78   var define-function-mode?/eax: (addr word) <- lookup *define-function-mode-ah?
  79   compare define-function-mode?, 0
  80   {
  81     break-if-=
  82 #?     print-string 0, "processing function definition\n"
  83     var functions/ecx: (addr handle function) <- get self, functions
  84     process-sandbox-define sandbox, functions, key
  85     break $process:body
  86   }
  87 #?   print-string 0, "processing sandbox\n"
  88   process-sandbox self, sandbox, key
  89 }
  90 }
  91 
  92 fn process-sandbox _self: (addr environment), _sandbox: (addr sandbox), key: grapheme {
  93 $process-sandbox:body: {
  94   var self/esi: (addr environment) <- copy _self
  95   var sandbox/edi: (addr sandbox) <- copy _sandbox
  96   var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
  97   var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
  98   var cursor-word-ah/ebx: (addr handle word) <- get cursor-call-path, word
  99   var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 100   var cursor-word/ecx: (addr word) <- copy _cursor-word
 101   compare key, 0x445b1b  # left-arrow
 102   $process-sandbox:key-left-arrow: {
 103     break-if-!=
 104 #?     print-string 0, "left-arrow\n"
 105     # if not at start, move left within current word
 106     var at-start?/eax: boolean <- cursor-at-start? cursor-word
 107     compare at-start?, 0  # false
 108     {
 109       break-if-!=
 110 #?       print-string 0, "cursor left within word\n"
 111       cursor-left cursor-word
 112       break $process-sandbox:body
 113     }
 114     # if current word is expanded, move to the rightmost word in its body
 115     {
 116       var cursor-call-path/esi: (addr handle call-path-element) <- get sandbox, cursor-call-path
 117       var expanded-words/edx: (addr handle call-path) <- get sandbox, expanded-words
 118       var curr-word-is-expanded?/eax: boolean <- find-in-call-paths expanded-words, cursor-call-path
 119       compare curr-word-is-expanded?, 0  # false
 120       break-if-=
 121       # update cursor-call-path
 122 #?       print-string 0, "curr word is expanded\n"
 123       var self/ecx: (addr environment) <- copy _self
 124       var functions/ecx: (addr handle function) <- get self, functions
 125       var body: (handle line)
 126       var body-ah/eax: (addr handle line) <- address body
 127       function-body functions, cursor-word-ah, body-ah
 128       var body-addr/eax: (addr line) <- lookup *body-ah
 129       var first-word-ah/edx: (addr handle word) <- get body-addr, data
 130       var final-word-h: (handle word)
 131       var final-word-ah/eax: (addr handle word) <- address final-word-h
 132       final-word first-word-ah, final-word-ah
 133       push-to-call-path-element cursor-call-path, final-word-ah
 134       # move cursor to end of word
 135       var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 136       var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 137       var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 138       var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 139       cursor-to-end cursor-word
 140       break $process-sandbox:body
 141     }
 142     # if at first word, look for a caller to jump to
 143     $process-sandbox:key-left-arrow-first-word: {
 144       var prev-word-ah/edx: (addr handle word) <- get cursor-word, prev
 145       var prev-word/eax: (addr word) <- lookup *prev-word-ah
 146       compare prev-word, 0
 147       break-if-!=
 148       $process-sandbox:key-left-arrow-first-word-and-caller: {
 149 #?         print-string 0, "return\n"
 150         {
 151           var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
 152           var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 153           var next-cursor-element-ah/edx: (addr handle call-path-element) <- get cursor-call-path, next
 154           var next-cursor-element/eax: (addr call-path-element) <- lookup *next-cursor-element-ah
 155           compare next-cursor-element, 0
 156           break-if-= $process-sandbox:key-left-arrow-first-word-and-caller
 157           copy-object next-cursor-element-ah, cursor-call-path-ah
 158         }
 159         var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 160         var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 161         var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 162         var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 163         cursor-word <- copy _cursor-word
 164       }
 165     }
 166     # then move to end of previous word
 167     var prev-word-ah/edx: (addr handle word) <- get cursor-word, prev
 168     var prev-word/eax: (addr word) <- lookup *prev-word-ah
 169     {
 170       compare prev-word, 0
 171       break-if-=
 172 #?       print-string 0, "move to previous word\n"
 173       cursor-to-end prev-word
 174 #?       {
 175 #?         var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 176 #?         var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 177 #?         var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 178 #?         var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 179 #?         var cursor-word/ebx: (addr word) <- copy _cursor-word
 180 #?         print-string 0, "word at cursor before: "
 181 #?         print-word 0, cursor-word
 182 #?         print-string 0, "\n"
 183 #?       }
 184       var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 185       decrement-final-element cursor-call-path
 186 #?       {
 187 #?         var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 188 #?         var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 189 #?         var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 190 #?         var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 191 #?         var cursor-word/ebx: (addr word) <- copy _cursor-word
 192 #?         print-string 0, "word at cursor after: "
 193 #?         print-word 0, cursor-word
 194 #?         print-string 0, "\n"
 195 #?       }
 196     }
 197     break $process-sandbox:body
 198   }
 199   compare key, 0x435b1b  # right-arrow
 200   $process-sandbox:key-right-arrow: {
 201     break-if-!=
 202     # if not at end, move right within current word
 203     var at-end?/eax: boolean <- cursor-at-end? cursor-word
 204     compare at-end?, 0  # false
 205     {
 206       break-if-!=
 207 #?       print-string 0, "a\n"
 208       cursor-right cursor-word
 209       break $process-sandbox:body
 210     }
 211     # if at final word, look for a caller to jump to
 212     {
 213       var next-word-ah/edx: (addr handle word) <- get cursor-word, next
 214       var next-word/eax: (addr word) <- lookup *next-word-ah
 215       compare next-word, 0
 216       break-if-!=
 217       var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
 218       var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 219       var next-cursor-element-ah/ecx: (addr handle call-path-element) <- get cursor-call-path, next
 220       var next-cursor-element/eax: (addr call-path-element) <- lookup *next-cursor-element-ah
 221       compare next-cursor-element, 0
 222       break-if-=
 223       copy-object next-cursor-element-ah, cursor-call-path-ah
 224       break $process-sandbox:body
 225     }
 226     # otherwise, move to the next word
 227     var next-word-ah/edx: (addr handle word) <- get cursor-word, next
 228     var next-word/eax: (addr word) <- lookup *next-word-ah
 229     {
 230       compare next-word, 0
 231       break-if-=
 232 #?       print-string 0, "b\n"
 233       cursor-to-start next-word
 234       # . . cursor-word now out of date
 235       var cursor-call-path/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
 236       increment-final-element cursor-call-path
 237       # Is the new cursor word expanded? If so, it's a function call. Add a
 238       # new level to the cursor-call-path for the call's body.
 239       $process-sandbox:key-right-arrow-next-word-is-call-expanded: {
 240 #?         print-string 0, "c\n"
 241         {
 242           var expanded-words/eax: (addr handle call-path) <- get sandbox, expanded-words
 243           var curr-word-is-expanded?/eax: boolean <- find-in-call-paths expanded-words, cursor-call-path
 244           compare curr-word-is-expanded?, 0  # false
 245           break-if-= $process-sandbox:key-right-arrow-next-word-is-call-expanded
 246         }
 247         var callee-h: (handle function)
 248         var callee-ah/edx: (addr handle function) <- address callee-h
 249         var functions/ebx: (addr handle function) <- get self, functions
 250         callee functions, next-word, callee-ah
 251         var callee/eax: (addr function) <- lookup *callee-ah
 252         var callee-body-ah/eax: (addr handle line) <- get callee, body
 253         var callee-body/eax: (addr line) <- lookup *callee-body-ah
 254         var callee-body-first-word/edx: (addr handle word) <- get callee-body, data
 255         push-to-call-path-element cursor-call-path, callee-body-first-word
 256         # position cursor at left
 257         var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 258         var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 259         var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 260         var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 261         cursor-to-start cursor-word
 262 #?         print-string 0, "d\n"
 263         break $process-sandbox:body
 264       }
 265     }
 266     break $process-sandbox:body
 267   }
 268   compare key, 0xa  # enter
 269   {
 270     break-if-!=
 271     # toggle display of subsidiary stack
 272     toggle-cursor-word sandbox
 273     break $process-sandbox:body
 274   }
 275   compare key, 0xc  # ctrl-l
 276   $process-sandbox:new-line: {
 277     break-if-!=
 278     # new line in sandbox
 279     append-line sandbox
 280     break $process-sandbox:body
 281   }
 282   # word-based motions
 283   compare key, 2  # ctrl-b
 284   $process-sandbox:prev-word: {
 285     break-if-!=
 286     # jump to previous word at same level
 287     var prev-word-ah/edx: (addr handle word) <- get cursor-word, prev
 288     var prev-word/eax: (addr word) <- lookup *prev-word-ah
 289     {
 290       compare prev-word, 0
 291       break-if-=
 292       cursor-to-end prev-word
 293       var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 294       decrement-final-element cursor-call-path
 295       break $process-sandbox:body
 296     }
 297     # if previous word doesn't exist, try to bump up one level
 298     {
 299       var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
 300       var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 301       var caller-cursor-element-ah/ecx: (addr handle call-path-element) <- get cursor-call-path, next
 302       var caller-cursor-element/eax: (addr call-path-element) <- lookup *caller-cursor-element-ah
 303       compare caller-cursor-element, 0
 304       break-if-=
 305       # check if previous word exists in caller
 306       var caller-word-ah/eax: (addr handle word) <- get caller-cursor-element, word
 307       var caller-word/eax: (addr word) <- lookup *caller-word-ah
 308       var word-before-caller-ah/eax: (addr handle word) <- get caller-word, prev
 309       var word-before-caller/eax: (addr word) <- lookup *word-before-caller-ah
 310       compare word-before-caller, 0
 311       break-if-=
 312       # if so jump to it
 313       drop-from-call-path-element cursor-call-path-ah
 314       decrement-final-element cursor-call-path-ah
 315       break $process-sandbox:body
 316     }
 317   }
 318   compare key, 6  # ctrl-f
 319   $process-sandbox:next-word: {
 320     break-if-!=
 321 #?     print-string 0, "AA\n"
 322     # jump to previous word at same level
 323     var next-word-ah/edx: (addr handle word) <- get cursor-word, next
 324     var next-word/eax: (addr word) <- lookup *next-word-ah
 325     {
 326       compare next-word, 0
 327       break-if-=
 328 #?       print-string 0, "BB\n"
 329       cursor-to-end next-word
 330       var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 331       increment-final-element cursor-call-path
 332       break $process-sandbox:body
 333     }
 334     # if next word doesn't exist, try to bump up one level
 335 #?     print-string 0, "CC\n"
 336     var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
 337     var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 338     var caller-cursor-element-ah/ecx: (addr handle call-path-element) <- get cursor-call-path, next
 339     var caller-cursor-element/eax: (addr call-path-element) <- lookup *caller-cursor-element-ah
 340     compare caller-cursor-element, 0
 341     break-if-=
 342 #?     print-string 0, "DD\n"
 343     copy-object caller-cursor-element-ah, cursor-call-path-ah
 344     break $process-sandbox:body
 345   }
 346   # line-based motions
 347   compare key, 1  # ctrl-a
 348   $process-sandbox:start-of-line: {
 349     break-if-!=
 350     # move cursor up past all calls and to start of line
 351     var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 352     drop-nested-calls cursor-call-path-ah
 353     move-final-element-to-start-of-line cursor-call-path-ah
 354     # move cursor to start of word
 355     var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 356     var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 357     var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 358     cursor-to-start cursor-word
 359     # this works as long as the first word isn't expanded
 360     # but we don't expect to see zero-arg functions first-up
 361     break $process-sandbox:body
 362   }
 363   compare key, 5  # ctrl-e
 364   $process-sandbox:end-of-line: {
 365     break-if-!=
 366     # move cursor up past all calls and to start of line
 367     var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 368     drop-nested-calls cursor-call-path-ah
 369     move-final-element-to-end-of-line cursor-call-path-ah
 370     # move cursor to end of word
 371     var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 372     var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
 373     var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
 374     cursor-to-end cursor-word
 375     # this works because expanded words lie to the right of their bodies
 376     # so the final word is always guaranteed to be at the top-level
 377     break $process-sandbox:body
 378   }
 379   compare key, 0x15  # ctrl-u
 380   $process-sandbox:clear-line: {
 381     break-if-!=
 382     # clear line in sandbox
 383     initialize-sandbox sandbox
 384     break $process-sandbox:body
 385   }
 386   # if cursor is within a call, disable editing hotkeys below
 387   var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 388   var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 389   var next-cursor-element-ah/eax: (addr handle call-path-element) <- get cursor-call-path, next
 390   var next-cursor-element/eax: (addr call-path-element) <- lookup *next-cursor-element-ah
 391   compare next-cursor-element, 0
 392   break-if-!= $process-sandbox:body
 393   # - remaining keys only work at the top row outside any function calls
 394   compare key, 0x7f  # del (backspace on Macs)
 395   $process-sandbox:backspace: {
 396     break-if-!=
 397     # if not at start of some word, delete grapheme before cursor within current word
 398     var at-start?/eax: boolean <- cursor-at-start? cursor-word
 399     compare at-start?, 0  # false
 400     {
 401       break-if-!=
 402       delete-before-cursor cursor-word
 403       break $process-sandbox:body
 404     }
 405     # otherwise delete current word and move to end of prev word
 406     var prev-word-ah/eax: (addr handle word) <- get cursor-word, prev
 407     var prev-word/eax: (addr word) <- lookup *prev-word-ah
 408     {
 409       compare prev-word, 0
 410       break-if-=
 411       cursor-to-end prev-word
 412       delete-next prev-word
 413       var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 414       decrement-final-element cursor-call-path
 415     }
 416     break $process-sandbox:body
 417   }
 418   compare key, 0x20  # space
 419   $process-sandbox:space: {
 420     break-if-!=
 421 #?     print-string 0, "space\n"
 422     # if cursor is at start of word, insert word before
 423     {
 424       var at-start?/eax: boolean <- cursor-at-start? cursor-word
 425       compare at-start?, 0  # false
 426       break-if-=
 427       var prev-word-ah/eax: (addr handle word) <- get cursor-word, prev
 428       append-word prev-word-ah
 429       var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 430       decrement-final-element cursor-call-path
 431       break $process-sandbox:body
 432     }
 433     # if start of word is quote and grapheme before cursor is not, just insert it as usual
 434     # TODO: support string escaping
 435     {
 436       var first-grapheme/eax: grapheme <- first-grapheme cursor-word
 437       compare first-grapheme, 0x22  # double quote
 438       break-if-!=
 439       var final-grapheme/eax: grapheme <- grapheme-before-cursor cursor-word
 440       compare final-grapheme, 0x22  # double quote
 441       break-if-=
 442       break $process-sandbox:space
 443     }
 444     # if start of word is '[' and grapheme before cursor is not ']', just insert it as usual
 445     # TODO: support nested arrays
 446     {
 447       var first-grapheme/eax: grapheme <- first-grapheme cursor-word
 448       compare first-grapheme, 0x5b  # '['
 449       break-if-!=
 450       var final-grapheme/eax: grapheme <- grapheme-before-cursor cursor-word
 451       compare final-grapheme, 0x5d  # ']'
 452       break-if-=
 453       break $process-sandbox:space
 454     }
 455     # otherwise insert word after and move cursor to it for the next key
 456     # (but we'll continue to track the current cursor-word for the rest of this function)
 457     append-word cursor-word-ah
 458     var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 459     increment-final-element cursor-call-path
 460     # if cursor is at end of word, that's all
 461     var at-end?/eax: boolean <- cursor-at-end? cursor-word
 462     compare at-end?, 0  # false
 463     break-if-!= $process-sandbox:body
 464     # otherwise we're in the middle of a word
 465     # move everything after cursor to the (just created) next word
 466     var next-word-ah/eax: (addr handle word) <- get cursor-word, next
 467     var _next-word/eax: (addr word) <- lookup *next-word-ah
 468     var next-word/ebx: (addr word) <- copy _next-word
 469     {
 470       var at-end?/eax: boolean <- cursor-at-end? cursor-word
 471       compare at-end?, 0  # false
 472       break-if-!=
 473       var g/eax: grapheme <- pop-after-cursor cursor-word
 474       add-grapheme-to-word next-word, g
 475       loop
 476     }
 477     cursor-to-start next-word
 478     break $process-sandbox:body
 479   }
 480   compare key, 0xe  # ctrl-n
 481   $process:rename-word: {
 482     break-if-!=
 483     # TODO: ensure current word is not a function
 484     # rename word at cursor
 485     var new-name-ah/eax: (addr handle word) <- get sandbox, partial-name-for-cursor-word
 486     allocate new-name-ah
 487     var new-name/eax: (addr word) <- lookup *new-name-ah
 488     initialize-word new-name
 489     break $process-sandbox:body
 490   }
 491   compare key, 4  # ctrl-d
 492   $process:define-function: {
 493     break-if-!=
 494     # define function out of line at cursor
 495     var new-name-ah/eax: (addr handle word) <- get sandbox, partial-name-for-function
 496     allocate new-name-ah
 497     var new-name/eax: (addr word) <- lookup *new-name-ah
 498     initialize-word new-name
 499     break $process-sandbox:body
 500   }
 501   # otherwise insert key within current word
 502   var g/edx: grapheme <- copy key
 503   var print?/eax: boolean <- real-grapheme? key
 504   $process-sandbox:real-grapheme: {
 505     compare print?, 0  # false
 506     break-if-=
 507     add-grapheme-to-word cursor-word, g
 508     break $process-sandbox:body
 509   }
 510   # silently ignore other hotkeys
 511 }
 512 }
 513 
 514 # collect new name in partial-name-for-cursor-word, and then rename the word
 515 # at cursor to it
 516 # Precondition: cursor-call-path is a singleton (not within a call)
 517 fn process-sandbox-rename _sandbox: (addr sandbox), key: grapheme {
 518 $process-sandbox-rename:body: {
 519   var sandbox/esi: (addr sandbox) <- copy _sandbox
 520   var new-name-ah/edi: (addr handle word) <- get sandbox, partial-name-for-cursor-word
 521   # if 'esc' pressed, cancel rename
 522   compare key, 0x1b  # esc
 523   $process-sandbox-rename:cancel: {
 524     break-if-!=
 525     clear-object new-name-ah
 526     break $process-sandbox-rename:body
 527   }
 528   # if 'enter' pressed, perform rename
 529   compare key, 0xa  # enter
 530   $process-sandbox-rename:commit: {
 531     break-if-!=
 532 #?     print-string 0, "rename\n"
 533     # new line
 534     var new-line-h: (handle line)
 535     var new-line-ah/eax: (addr handle line) <- address new-line-h
 536     allocate new-line-ah
 537     var new-line/eax: (addr line) <- lookup *new-line-ah
 538     initialize-line new-line
 539     var new-line-word-ah/ecx: (addr handle word) <- get new-line, data
 540     {
 541       # move word at cursor to new line
 542       var cursor-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 543       var cursor/eax: (addr call-path-element) <- lookup *cursor-ah
 544       var word-at-cursor-ah/eax: (addr handle word) <- get cursor, word
 545 #?       print-string 0, "cursor before at word "
 546 #?       {
 547 #?         var cursor-word/eax: (addr word) <- lookup *word-at-cursor-ah
 548 #?         print-word 0, cursor-word
 549 #?         print-string 0, "\n"
 550 #?       }
 551       move-word-contents word-at-cursor-ah, new-line-word-ah
 552       # copy name to word at cursor
 553       copy-word-contents-before-cursor new-name-ah, word-at-cursor-ah
 554 #?       print-string 0, "cursor after at word "
 555 #?       {
 556 #?         var cursor-word/eax: (addr word) <- lookup *word-at-cursor-ah
 557 #?         print-word 0, cursor-word
 558 #?         print-string 0, "\n"
 559 #?         var foo/eax: int <- copy cursor-word
 560 #?         print-int32-hex 0, foo
 561 #?         print-string 0, "\n"
 562 #?       }
 563 #?       print-string 0, "new name word "
 564 #?       {
 565 #?         var new-name/eax: (addr word) <- lookup *new-name-ah
 566 #?         print-word 0, new-name
 567 #?         print-string 0, "\n"
 568 #?         var foo/eax: int <- copy new-name
 569 #?         print-int32-hex 0, foo
 570 #?         print-string 0, "\n"
 571 #?       }
 572     }
 573     # prepend '=' to name
 574     {
 575       var new-name/eax: (addr word) <- lookup *new-name-ah
 576       cursor-to-start new-name
 577       add-grapheme-to-word new-name, 0x3d  # '='
 578     }
 579     # append name to new line
 580     chain-words new-line-word-ah, new-name-ah
 581     # new-line->next = sandbox->data
 582     var new-line-next/ecx: (addr handle line) <- get new-line, next
 583     var sandbox-slot/edx: (addr handle line) <- get sandbox, data
 584     copy-object sandbox-slot, new-line-next
 585     # sandbox->data = new-line
 586     copy-handle new-line-h, sandbox-slot
 587     # clear partial-name-for-cursor-word
 588     clear-object new-name-ah
 589 #?     var cursor-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 590 #?     var cursor/eax: (addr call-path-element) <- lookup *cursor-ah
 591 #?     var word-at-cursor-ah/eax: (addr handle word) <- get cursor, word
 592 #?     print-string 0, "cursor after rename: "
 593 #?     {
 594 #?       var cursor-word/eax: (addr word) <- lookup *word-at-cursor-ah
 595 #?       print-word 0, cursor-word
 596 #?       print-string 0, " -- "
 597 #?       var foo/eax: int <- copy cursor-word
 598 #?       print-int32-hex 0, foo
 599 #?       print-string 0, "\n"
 600 #?     }
 601     break $process-sandbox-rename:body
 602   }
 603   #
 604   compare key, 0x7f  # del (backspace on Macs)
 605   $process-sandbox-rename:backspace: {
 606     break-if-!=
 607     # if not at start, delete grapheme before cursor
 608     var new-name/eax: (addr word) <- lookup *new-name-ah
 609     var at-start?/eax: boolean <- cursor-at-start? new-name
 610     compare at-start?, 0  # false
 611     {
 612       break-if-!=
 613       var new-name/eax: (addr word) <- lookup *new-name-ah
 614       delete-before-cursor new-name
 615     }
 616     break $process-sandbox-rename:body
 617   }
 618   # otherwise insert key within current word
 619   var print?/eax: boolean <- real-grapheme? key
 620   $process-sandbox-rename:real-grapheme: {
 621     compare print?, 0  # false
 622     break-if-=
 623     var new-name/eax: (addr word) <- lookup *new-name-ah
 624     add-grapheme-to-word new-name, key
 625     break $process-sandbox-rename:body
 626   }
 627   # silently ignore other hotkeys
 628 }
 629 }
 630 
 631 # collect new name in partial-name-for-function, and then define the last line
 632 # of the sandbox to be a new function with that name. Replace the last line
 633 # with a call to the appropriate function.
 634 # Precondition: cursor-call-path is a singleton (not within a call)
 635 fn process-sandbox-define _sandbox: (addr sandbox), functions: (addr handle function), key: grapheme {
 636 $process-sandbox-define:body: {
 637   var sandbox/esi: (addr sandbox) <- copy _sandbox
 638   var new-name-ah/edi: (addr handle word) <- get sandbox, partial-name-for-function
 639   # if 'esc' pressed, cancel define
 640   compare key, 0x1b  # esc
 641   $process-sandbox-define:cancel: {
 642     break-if-!=
 643     clear-object new-name-ah
 644     break $process-sandbox-define:body
 645   }
 646   # if 'enter' pressed, perform define
 647   compare key, 0xa  # enter
 648   $process-sandbox-define:commit: {
 649     break-if-!=
 650 #?     print-string 0, "define\n"
 651     # create new function
 652     var new-function: (handle function)
 653     var new-function-ah/ecx: (addr handle function) <- address new-function
 654     allocate new-function-ah
 655     var _new-function/eax: (addr function) <- lookup *new-function-ah
 656     var new-function/ebx: (addr function) <- copy _new-function
 657     var dest/edx: (addr handle function) <- get new-function, next
 658     copy-object functions, dest
 659     copy-object new-function-ah, functions
 660     # set function name to new-name
 661     var new-name/eax: (addr word) <- lookup *new-name-ah
 662     var dest/edx: (addr handle array byte) <- get new-function, name
 663     word-to-string new-name, dest
 664     # move final line to body
 665     var body-ah/eax: (addr handle line) <- get new-function, body
 666     allocate body-ah
 667     var body/eax: (addr line) <- lookup *body-ah
 668     var body-contents/ecx: (addr handle word) <- get body, data
 669     var final-line-storage: (handle line)
 670     var final-line-ah/eax: (addr handle line) <- address final-line-storage
 671     final-line sandbox, final-line-ah
 672     var final-line/eax: (addr line) <- lookup *final-line-ah
 673     var final-line-contents/eax: (addr handle word) <- get final-line, data
 674     copy-object final-line-contents, body-contents
 675     #
 676     copy-unbound-words-to-args functions
 677     #
 678     var empty-word: (handle word)
 679     copy-handle empty-word, final-line-contents
 680     construct-call functions, final-line-contents
 681     # clear partial-name-for-function
 682     var empty-word: (handle word)
 683     copy-handle empty-word, new-name-ah
 684     # update cursor
 685     var final-line/eax: (addr line) <- lookup final-line-storage
 686     var cursor-call-path-ah/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
 687     allocate cursor-call-path-ah  # leak
 688     initialize-path-from-line final-line, cursor-call-path-ah
 689     break $process-sandbox-define:body
 690   }
 691   #
 692   compare key, 0x7f  # del (backspace on Macs)
 693   $process-sandbox-define:backspace: {
 694     break-if-!=
 695     # if not at start, delete grapheme before cursor
 696     var new-name/eax: (addr word) <- lookup *new-name-ah
 697     var at-start?/eax: boolean <- cursor-at-start? new-name
 698     compare at-start?, 0  # false
 699     {
 700       break-if-!=
 701       var new-name/eax: (addr word) <- lookup *new-name-ah
 702       delete-before-cursor new-name
 703     }
 704     break $process-sandbox-define:body
 705   }
 706   # otherwise insert key within current word
 707   var print?/eax: boolean <- real-grapheme? key
 708   $process-sandbox-define:real-grapheme: {
 709     compare print?, 0  # false
 710     break-if-=
 711     var new-name/eax: (addr word) <- lookup *new-name-ah
 712     add-grapheme-to-word new-name, key
 713     break $process-sandbox-define:body
 714   }
 715   # silently ignore other hotkeys
 716 }
 717 }
 718 
 719 # extract from the body of the first function in 'functions' all words that
 720 # aren't defined in the rest of 'functions'. Prepend them in reverse order.
 721 # Assumes function body is a single line for now.
 722 fn copy-unbound-words-to-args _functions: (addr handle function) {
 723   # target
 724   var target-ah/eax: (addr handle function) <- copy _functions
 725   var _target/eax: (addr function) <- lookup *target-ah
 726   var target/esi: (addr function) <- copy _target
 727   var dest-ah/edi: (addr handle word) <- get target, args
 728   # next
 729   var functions-ah/edx: (addr handle function) <- get target, next
 730   # src
 731   var line-ah/eax: (addr handle line) <- get target, body
 732   var line/eax: (addr line) <- lookup *line-ah
 733   var curr-ah/eax: (addr handle word) <- get line, data
 734   var curr/eax: (addr word) <- lookup *curr-ah
 735   {
 736     compare curr, 0
 737     break-if-=
 738     $copy-unbound-words-to-args:loop-iter: {
 739       # is it a number?
 740       {
 741         var is-int?/eax: boolean <- word-is-decimal-integer? curr
 742         compare is-int?, 0  # false
 743         break-if-!= $copy-unbound-words-to-args:loop-iter
 744       }
 745       # is it a pre-existing function?
 746       var bound?/ebx: boolean <- bound-function? curr, functions-ah
 747       compare bound?, 0  # false
 748       break-if-!=
 749       # is it already bound as an arg?
 750       var dup?/ebx: boolean <- arg-exists? _functions, curr  # _functions = target-ah
 751       compare dup?, 0  # false
 752       break-if-!= $copy-unbound-words-to-args:loop-iter
 753       # push copy of curr before dest-ah
 754       var rest-h: (handle word)
 755       var rest-ah/ecx: (addr handle word) <- address rest-h
 756       copy-object dest-ah, rest-ah
 757       copy-word curr, dest-ah
 758       chain-words dest-ah, rest-ah
 759     }
 760     var next-ah/ecx: (addr handle word) <- get curr, next
 761     curr <- lookup *next-ah
 762     loop
 763   }
 764 }
 765 
 766 fn bound-function? w: (addr word), functions-ah: (addr handle function) -> _/ebx: boolean {
 767   var result/ebx: boolean <- copy 1  # true
 768   {
 769     ## numbers
 770     # if w == "+" return true
 771     var subresult/eax: boolean <- word-equal? w, "+"
 772     compare subresult, 0  # false
 773     break-if-!=
 774     # if w == "-" return true
 775     subresult <- word-equal? w, "-"
 776     compare subresult, 0  # false
 777     break-if-!=
 778     # if w == "*" return true
 779     subresult <- word-equal? w, "*"
 780     compare subresult, 0  # false
 781     break-if-!=
 782     ## strings/arrays
 783     # if w == "len" return true
 784     subresult <- word-equal? w, "len"
 785     compare subresult, 0  # false
 786     break-if-!=
 787     ## files
 788     # if w == "open" return true
 789     subresult <- word-equal? w, "open"
 790     compare subresult, 0  # false
 791     break-if-!=
 792     # if w == "read" return true
 793     subresult <- word-equal? w, "read"
 794     compare subresult, 0  # false
 795     break-if-!=
 796     # if w == "slurp" return true
 797     subresult <- word-equal? w, "slurp"
 798     compare subresult, 0  # false
 799     break-if-!=
 800     # if w == "lines" return true
 801     subresult <- word-equal? w, "lines"
 802     compare subresult, 0  # false
 803     break-if-!=
 804     ## screens
 805     # if w == "fake-screen" return true
 806     subresult <- word-equal? w, "fake-screen"
 807     compare subresult, 0  # false
 808     break-if-!=
 809     # if w == "print" return true
 810     subresult <- word-equal? w, "print"
 811     compare subresult, 0  # false
 812     break-if-!=
 813     # if w == "move" return true
 814     subresult <- word-equal? w, "move"
 815     compare subresult, 0  # false
 816     break-if-!=
 817     # if w == "up" return true
 818     subresult <- word-equal? w, "up"
 819     compare subresult, 0  # false
 820     break-if-!=
 821     # if w == "down" return true
 822     subresult <- word-equal? w, "down"
 823     compare subresult, 0  # false
 824     break-if-!=
 825     # if w == "left" return true
 826     subresult <- word-equal? w, "left"
 827     compare subresult, 0  # false
 828     break-if-!=
 829     # if w == "right" return true
 830     subresult <- word-equal? w, "right"
 831     compare subresult, 0  # false
 832     break-if-!=
 833     ## hacks
 834     # if w == "dup" return true
 835     subresult <- word-equal? w, "dup"
 836     compare subresult, 0  # false
 837     break-if-!=
 838     # if w == "swap" return true
 839     subresult <- word-equal? w, "swap"
 840     compare subresult, 0  # false
 841     break-if-!=
 842     # return w in functions
 843     var out-h: (handle function)
 844     var out/eax: (addr handle function) <- address out-h
 845     callee functions-ah, w, out
 846     var found?/eax: (addr function) <- lookup *out
 847     result <- copy found?
 848   }
 849   return result
 850 }
 851 
 852 fn arg-exists? _f-ah: (addr handle function), arg: (addr word) -> _/ebx: boolean {
 853   var f-ah/eax: (addr handle function) <- copy _f-ah
 854   var f/eax: (addr function) <- lookup *f-ah
 855   var args-ah/eax: (addr handle word) <- get f, args
 856   var result/ebx: boolean <- word-exists? args-ah, arg
 857   return result
 858 }
 859 
 860 # construct a call to `f` with copies of exactly its args
 861 fn construct-call _f-ah: (addr handle function), _dest-ah: (addr handle word) {
 862   var f-ah/eax: (addr handle function) <- copy _f-ah
 863   var _f/eax: (addr function) <- lookup *f-ah
 864   var f/esi: (addr function) <- copy _f
 865   # append args in reverse
 866   var args-ah/eax: (addr handle word) <- get f, args
 867   var dest-ah/edi: (addr handle word) <- copy _dest-ah
 868   copy-words-in-reverse args-ah, dest-ah
 869   # append name
 870   var name-ah/eax: (addr handle array byte) <- get f, name
 871   var name/eax: (addr array byte) <- lookup *name-ah
 872   append-word-at-end-with dest-ah, name
 873 }
 874 
 875 fn word-index _words: (addr handle word), _n: int, out: (addr handle word) {
 876 $word-index:body: {
 877   var n/ecx: int <- copy _n
 878   {
 879     compare n, 0
 880     break-if-!=
 881     copy-object _words, out
 882     break $word-index:body
 883   }
 884   var words-ah/eax: (addr handle word) <- copy _words
 885   var words/eax: (addr word) <- lookup *words-ah
 886   var next/eax: (addr handle word) <- get words, next
 887   n <- decrement
 888   word-index next, n, out
 889 }
 890 }
 891 
 892 fn toggle-cursor-word _sandbox: (addr sandbox) {
 893 $toggle-cursor-word:body: {
 894   var sandbox/esi: (addr sandbox) <- copy _sandbox
 895   var expanded-words/edi: (addr handle call-path) <- get sandbox, expanded-words
 896   var cursor-call-path/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
 897 #?   print-string 0, "cursor call path: "
 898 #?   dump-call-path-element 0, cursor-call-path
 899 #?   print-string 0, "expanded words:\n"
 900 #?   dump-call-paths 0, expanded-words
 901   var already-expanded?/eax: boolean <- find-in-call-paths expanded-words, cursor-call-path
 902   compare already-expanded?, 0  # false
 903   {
 904     break-if-!=
 905 #?     print-string 0, "expand\n"
 906     # if not already-expanded, insert
 907     insert-in-call-path expanded-words cursor-call-path
 908 #?     print-string 0, "expanded words now:\n"
 909 #?     dump-call-paths 0, expanded-words
 910     break $toggle-cursor-word:body
 911   }
 912   {
 913     break-if-=
 914     # otherwise delete
 915     delete-in-call-path expanded-words cursor-call-path
 916   }
 917 }
 918 }
 919 
 920 fn append-line _sandbox: (addr sandbox) {
 921   var sandbox/esi: (addr sandbox) <- copy _sandbox
 922   var line-ah/ecx: (addr handle line) <- get sandbox, data
 923   {
 924     var line/eax: (addr line) <- lookup *line-ah
 925     var next-line-ah/edx: (addr handle line) <- get line, next
 926     var next-line/eax: (addr line) <- lookup *next-line-ah
 927     compare next-line, 0
 928     break-if-=
 929     line-ah <- copy next-line-ah
 930     loop
 931   }
 932   var line/eax: (addr line) <- lookup *line-ah
 933   var final-line-ah/edx: (addr handle line) <- get line, next
 934   allocate final-line-ah
 935   var final-line/eax: (addr line) <- lookup *final-line-ah
 936   initialize-line final-line
 937   var final-prev/eax: (addr handle line) <- get final-line, prev
 938   copy-object line-ah, final-prev
 939   # clear cursor
 940   var final-line/eax: (addr line) <- lookup *final-line-ah
 941   var word-ah/ecx: (addr handle word) <- get final-line, data
 942   var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
 943   var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
 944   var dest/eax: (addr handle word) <- get cursor-call-path, word
 945   copy-object word-ah, dest
 946 }
 947 
 948 #############
 949 # Visualize
 950 #############
 951 
 952 fn evaluate-environment _env: (addr environment), stack: (addr value-stack) {
 953   var env/esi: (addr environment) <- copy _env
 954   # functions
 955   var functions/edx: (addr handle function) <- get env, functions
 956   # line
 957   var sandbox-ah/esi: (addr handle sandbox) <- get env, sandboxes
 958   var sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
 959   var line-ah/eax: (addr handle line) <- get sandbox, data
 960   var _line/eax: (addr line) <- lookup *line-ah
 961   var line/esi: (addr line) <- copy _line
 962   evaluate functions, 0, line, 0, stack
 963 }
 964 
 965 fn render _env: (addr environment) {
 966 #?   print-string 0, "== render\n"
 967   var env/esi: (addr environment) <- copy _env
 968   clear-canvas env
 969   # screen
 970   var screen-ah/eax: (addr handle screen) <- get env, screen
 971   var _screen/eax: (addr screen) <- lookup *screen-ah
 972   var screen/edi: (addr screen) <- copy _screen
 973   # repl-col
 974   var _repl-col/eax: (addr int) <- get env, code-separator-col
 975   var repl-col/ecx: int <- copy *_repl-col
 976   repl-col <- add 2  # repl-margin-left
 977   # functions
 978   var functions/edx: (addr handle function) <- get env, functions
 979   # sandbox
 980   var sandbox-ah/eax: (addr handle sandbox) <- get env, sandboxes
 981   var sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
 982   # bindings
 983   var bindings-storage: table
 984   var bindings/ebx: (addr table) <- address bindings-storage
 985   initialize-table bindings, 0x10
 986 #?   print-string 0, "render-sandbox {\n"
 987   render-sandbox screen, functions, bindings, sandbox, 3, repl-col
 988 #?   print-string 0, "render-sandbox }\n"
 989 }
 990 
 991 fn render-sandbox screen: (addr screen), functions: (addr handle function), bindings: (addr table), _sandbox: (addr sandbox), top-row: int, left-col: int {
 992   var sandbox/esi: (addr sandbox) <- copy _sandbox
 993   # line
 994   var curr-line-ah/eax: (addr handle line) <- get sandbox, data
 995   var _curr-line/eax: (addr line) <- lookup *curr-line-ah
 996   var curr-line/ecx: (addr line) <- copy _curr-line
 997   #
 998   var curr-row/edx: int <- copy top-row
 999   # cursor row, col
1000   var cursor-row: int
1001   var cursor-row-addr: (addr int)
1002   var tmp/eax: (addr int) <- address cursor-row
1003   copy-to cursor-row-addr, tmp
1004   var cursor-col: int
1005   var cursor-col-addr: (addr int)
1006   tmp <- address cursor-col
1007   copy-to cursor-col-addr, tmp
1008   # render all but final line without stack
1009 #?   print-string 0, "render all but final line\n"
1010   {
1011     var next-line-ah/eax: (addr handle line) <- get curr-line, next
1012     var next-line/eax: (addr line) <- lookup *next-line-ah
1013     compare next-line, 0
1014     break-if-=
1015     {
1016       var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
1017       var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
1018       var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
1019       var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
1020       # it's enough to pass in the first word of the path, because if the path isn't a singleton the word is guaranteed to be unique
1021       render-line-without-stack screen, curr-line, curr-row, left-col, cursor-word, cursor-row-addr, cursor-col-addr
1022     }
1023     curr-line <- copy next-line
1024     curr-row <- add 2
1025     loop
1026   }
1027   #
1028   render-final-line-with-stack screen, functions, bindings, sandbox, curr-row, left-col, cursor-row-addr, cursor-col-addr
1029   # at most one of the following dialogs will be rendered
1030   render-rename-dialog screen, sandbox, cursor-row, cursor-col
1031   render-define-dialog screen, sandbox, cursor-row, cursor-col
1032   move-cursor screen, cursor-row, cursor-col
1033 }
1034 
1035 fn render-final-line-with-stack screen: (addr screen), functions: (addr handle function), bindings: (addr table), _sandbox: (addr sandbox), top-row: int, left-col: int, cursor-row-addr: (addr int), cursor-col-addr: (addr int) {
1036   var sandbox/esi: (addr sandbox) <- copy _sandbox
1037   # expanded-words
1038   var expanded-words/edi: (addr handle call-path) <- get sandbox, expanded-words
1039   # cursor-word
1040   var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
1041   var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
1042   var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
1043   var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
1044   var cursor-word/ebx: (addr word) <- copy _cursor-word
1045 #?   print-string 0, "word at cursor: "
1046 #?   print-word 0, cursor-word
1047 #?   print-string 0, "\n"
1048   # cursor-call-path
1049   var cursor-call-path: (addr handle call-path-element)
1050   {
1051     var src/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
1052     copy-to cursor-call-path, src
1053   }
1054   # first line
1055   var first-line-ah/eax: (addr handle line) <- get sandbox, data
1056   var _first-line/eax: (addr line) <- lookup *first-line-ah
1057   var first-line/edx: (addr line) <- copy _first-line
1058   # final line
1059   var final-line-storage: (handle line)
1060   var final-line-ah/eax: (addr handle line) <- address final-line-storage
1061   final-line sandbox, final-line-ah
1062   var final-line/eax: (addr line) <- lookup *final-line-ah
1063   # curr-path
1064   var curr-path-storage: (handle call-path-element)
1065   var curr-path/ecx: (addr handle call-path-element) <- address curr-path-storage
1066   allocate curr-path  # leak
1067   initialize-path-from-line final-line, curr-path
1068   #
1069   var dummy/ecx: int <- render-line screen, functions, bindings, first-line, final-line, expanded-words, top-row, left-col, curr-path, cursor-word, cursor-call-path, cursor-row-addr, cursor-col-addr
1070 }
1071 
1072 fn final-line _sandbox: (addr sandbox), out: (addr handle line) {
1073   var sandbox/esi: (addr sandbox) <- copy _sandbox
1074   var curr-line-ah/ecx: (addr handle line) <- get sandbox, data
1075   {
1076     var curr-line/eax: (addr line) <- lookup *curr-line-ah
1077     var next-line-ah/edx: (addr handle line) <- get curr-line, next
1078     var next-line/eax: (addr line) <- lookup *next-line-ah
1079     compare next-line, 0
1080     break-if-=
1081     curr-line-ah <- copy next-line-ah
1082     loop
1083   }
1084   copy-object curr-line-ah, out
1085 }
1086 
1087 fn render-rename-dialog screen: (addr screen), _sandbox: (addr sandbox), cursor-row: int, cursor-col: int {
1088   var sandbox/edi: (addr sandbox) <- copy _sandbox
1089   var rename-word-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-cursor-word
1090   var rename-word-mode?/eax: (addr word) <- lookup *rename-word-mode-ah?
1091   compare rename-word-mode?, 0
1092   break-if-=
1093   # clear a space for the dialog
1094   var top-row/eax: int <- copy cursor-row
1095   top-row <- subtract 3
1096   var bottom-row/ecx: int <- copy cursor-row
1097   bottom-row <- add 3
1098   var left-col/edx: int <- copy cursor-col
1099   left-col <- subtract 0x10
1100   var right-col/ebx: int <- copy cursor-col
1101   right-col <- add 0x10
1102   clear-rect screen, top-row, left-col, bottom-row, right-col
1103   draw-box screen, top-row, left-col, bottom-row, right-col
1104   # render a little menu for the dialog
1105   var menu-row/ecx: int <- copy bottom-row
1106   menu-row <- decrement
1107   var menu-col/edx: int <- copy left-col
1108   menu-col <- add 2
1109   move-cursor screen, menu-row, menu-col
1110   start-reverse-video screen
1111   print-string screen, " esc "
1112   reset-formatting screen
1113   print-string screen, " cancel  "
1114   start-reverse-video screen
1115   print-string screen, " enter "
1116   reset-formatting screen
1117   print-string screen, " rename  "
1118   # draw the word, positioned appropriately around the cursor
1119   var start-col/ecx: int <- copy cursor-col
1120   var word-ah?/edx: (addr handle word) <- get sandbox, partial-name-for-cursor-word
1121   var word/eax: (addr word) <- lookup *word-ah?
1122   var cursor-index/eax: int <- cursor-index word
1123   start-col <- subtract cursor-index
1124   move-cursor screen, cursor-row, start-col
1125   var word/eax: (addr word) <- lookup *word-ah?
1126   print-word screen, word
1127 }
1128 
1129 fn render-define-dialog screen: (addr screen), _sandbox: (addr sandbox), cursor-row: int, cursor-col: int {
1130   var sandbox/edi: (addr sandbox) <- copy _sandbox
1131   var define-function-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-function
1132   var define-function-mode?/eax: (addr word) <- lookup *define-function-mode-ah?
1133   compare define-function-mode?, 0
1134   break-if-=
1135   # clear a space for the dialog
1136   var top-row/eax: int <- copy cursor-row
1137   top-row <- subtract 3
1138   var bottom-row/ecx: int <- copy cursor-row
1139   bottom-row <- add 3
1140   var left-col/edx: int <- copy cursor-col
1141   left-col <- subtract 0x10
1142   var right-col/ebx: int <- copy cursor-col
1143   right-col <- add 0x10
1144   clear-rect screen, top-row, left-col, bottom-row, right-col
1145   draw-box screen, top-row, left-col, bottom-row, right-col
1146   # render a little menu for the dialog
1147   var menu-row/ecx: int <- copy bottom-row
1148   menu-row <- decrement
1149   var menu-col/edx: int <- copy left-col
1150   menu-col <- add 2
1151   move-cursor screen, menu-row, menu-col
1152   start-reverse-video screen
1153   print-string screen, " esc "
1154   reset-formatting screen
1155   print-string screen, " cancel  "
1156   start-reverse-video screen
1157   print-string screen, " enter "
1158   reset-formatting screen
1159   print-string screen, " define  "
1160   # draw the word, positioned appropriately around the cursor
1161   var start-col/ecx: int <- copy cursor-col
1162   var word-ah?/edx: (addr handle word) <- get sandbox, partial-name-for-function
1163   var word/eax: (addr word) <- lookup *word-ah?
1164   var cursor-index/eax: int <- cursor-index word
1165   start-col <- subtract cursor-index
1166   move-cursor screen, cursor-row, start-col
1167   var word/eax: (addr word) <- lookup *word-ah?
1168   print-word screen, word
1169 }
1170 
1171 # Render just the words in 'line'.
1172 fn render-line-without-stack screen: (addr screen), _line: (addr line), curr-row: int, left-col: int, cursor-word: (addr word), cursor-row-addr: (addr int), cursor-col-addr: (addr int) {
1173   # curr-word
1174   var line/eax: (addr line) <- copy _line
1175   var first-word-ah/eax: (addr handle word) <- get line, data
1176   var _curr-word/eax: (addr word) <- lookup *first-word-ah
1177   var curr-word/esi: (addr word) <- copy _curr-word
1178   #
1179   # loop-carried dependency
1180   var curr-col/ecx: int <- copy left-col
1181   #
1182   {
1183     compare curr-word, 0
1184     break-if-=
1185 #?     print-string 0, "-- word in penultimate lines: "
1186 #?     {
1187 #?       var foo/eax: int <- copy curr-word
1188 #?       print-int32-hex 0, foo
1189 #?     }
1190 #?     print-string 0, "\n"
1191     var old-col/edx: int <- copy curr-col
1192     reset-formatting screen
1193     move-cursor screen, curr-row, curr-col
1194     print-word screen, curr-word
1195     {
1196       var max-width/eax: int <- word-length curr-word
1197       curr-col <- add max-width
1198       curr-col <- add 1  # margin-right
1199     }
1200     # cache cursor column if necessary
1201     {
1202       compare curr-word, cursor-word
1203       break-if-!=
1204 #?       print-string 0, "Cursor at "
1205 #?       print-int32-decimal 0, curr-row
1206 #?       print-string 0, ", "
1207 #?       print-int32-decimal 0, old-col
1208 #?       print-string 0, "\n"
1209 #?       print-string 0, "contents: "
1210 #?       print-word 0, cursor-word
1211 #?       print-string 0, "\n"
1212 #?       {
1213 #?         var foo/eax: int <- copy cursor-word
1214 #?         print-int32-hex 0, foo
1215 #?         print-string 0, "\n"
1216 #?       }
1217       var dest/ecx: (addr int) <- copy cursor-row-addr
1218       var src/eax: int <- copy curr-row
1219       copy-to *dest, src
1220       dest <- copy cursor-col-addr
1221       copy-to *dest, old-col
1222       var cursor-index-in-word/eax: int <- cursor-index curr-word
1223       add-to *dest, cursor-index-in-word
1224     }
1225     # loop update
1226     var next-word-ah/edx: (addr handle word) <- get curr-word, next
1227     var _curr-word/eax: (addr word) <- lookup *next-word-ah
1228     curr-word <- copy _curr-word
1229     loop
1230   }
1231 }
1232 
1233 fn call-depth-at-cursor _sandbox: (addr sandbox) -> _/eax: int {
1234   var sandbox/esi: (addr sandbox) <- copy _sandbox
1235   var cursor-call-path/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
1236   var result/eax: int <- call-path-element-length cursor-call-path
1237   result <- add 2  # input-row - 1
1238   return result
1239 }
1240 
1241 fn call-path-element-length _x: (addr handle call-path-element) -> _/eax: int {
1242   var curr-ah/ecx: (addr handle call-path-element) <- copy _x
1243   var result/edi: int <- copy 0
1244   {
1245     var curr/eax: (addr call-path-element) <- lookup *curr-ah
1246     compare curr, 0
1247     break-if-=
1248     curr-ah <- get curr, next
1249     result <- increment
1250     loop
1251   }
1252   return result
1253 }
1254 
1255 # Render the line of words in line, along with the state of the stack under each word.
1256 # Also render any expanded function calls using recursive calls.
1257 #
1258 # Along the way, compute the column the cursor should be positioned at (cursor-col-addr).
1259 fn render-line screen: (addr screen), functions: (addr handle function), bindings: (addr table), first-line: (addr line), _line: (addr line), expanded-words: (addr handle call-path), top-row: int, left-col: int, curr-path: (addr handle call-path-element), cursor-word: (addr word), cursor-call-path: (addr handle call-path-element), cursor-row-addr: (addr int), cursor-col-addr: (addr int) -> _/ecx: int {
1260 #?   print-string 0, "render-line\n"
1261 #?   dump-table bindings
1262   # curr-word
1263   var line/esi: (addr line) <- copy _line
1264   var first-word-ah/eax: (addr handle word) <- get line, data
1265   var curr-word/eax: (addr word) <- lookup *first-word-ah
1266   #
1267   # loop-carried dependency
1268   var curr-col/ecx: int <- copy left-col
1269   #
1270   {
1271     compare curr-word, 0
1272     break-if-=
1273 #?     print-string 0, "-- word "
1274 #?     print-word 0, curr-word
1275 #?     print-string 0, "\n"
1276     # if necessary, first render columns for subsidiary stack
1277     $render-line:subsidiary: {
1278       {
1279 #?         print-string 0, "check sub\n"
1280         var display-subsidiary-stack?/eax: boolean <- find-in-call-paths expanded-words, curr-path
1281         compare display-subsidiary-stack?, 0  # false
1282         break-if-= $render-line:subsidiary
1283       }
1284 #?       print-string 0, "render subsidiary stack\n"
1285       # does function exist?
1286       var callee/edi: (addr function) <- copy 0
1287       {
1288         var callee-h: (handle function)
1289         var callee-ah/ecx: (addr handle function) <- address callee-h
1290         callee functions, curr-word, callee-ah
1291         var _callee/eax: (addr function) <- lookup *callee-ah
1292         callee <- copy _callee
1293         compare callee, 0
1294         break-if-= $render-line:subsidiary
1295       }
1296       move-cursor screen, top-row, curr-col
1297       start-color screen, 8, 7
1298       print-word screen, curr-word
1299       {
1300         var word-len/eax: int <- word-length curr-word
1301         curr-col <- add word-len
1302         curr-col <- add 2
1303         increment top-row
1304       }
1305       # obtain stack at call site
1306       var stack-storage: value-stack
1307       var stack/edx: (addr value-stack) <- address stack-storage
1308       initialize-value-stack stack, 0x10
1309       {
1310         var prev-word-ah/eax: (addr handle word) <- get curr-word, prev
1311         var prev-word/eax: (addr word) <- lookup *prev-word-ah
1312         compare prev-word, 0
1313         break-if-=
1314         var bindings2-storage: table
1315         var bindings2/ebx: (addr table) <- address bindings2-storage
1316         deep-copy-table bindings, bindings2
1317         evaluate functions, bindings2, first-line, prev-word, stack
1318       }
1319       # construct new bindings
1320       var callee-bindings-storage: table
1321       var callee-bindings/esi: (addr table) <- address callee-bindings-storage
1322       initialize-table callee-bindings, 0x10
1323       bind-args callee, stack, callee-bindings
1324       # obtain body
1325       var callee-body-ah/eax: (addr handle line) <- get callee, body
1326       var callee-body/eax: (addr line) <- lookup *callee-body-ah
1327       var callee-body-first-word/edx: (addr handle word) <- get callee-body, data
1328       # - render subsidiary stack
1329       push-to-call-path-element curr-path, callee-body-first-word  # leak
1330 #?       print-string 0, "subsidiary {\n"
1331 #?       dump-table callee-bindings
1332 #?       syscall_exit
1333       curr-col <- render-line screen, functions, callee-bindings, callee-body, callee-body, expanded-words, top-row, curr-col, curr-path, cursor-word, cursor-call-path, cursor-row-addr, cursor-col-addr
1334 #?       print-string 0, "}\n"
1335       drop-from-call-path-element curr-path
1336       #
1337       move-cursor screen, top-row, curr-col
1338       print-code-point screen, 0x21d7  # ⇗
1339       #
1340       curr-col <- add 2
1341       decrement top-row
1342     }
1343     # render main column
1344     var old-col/edx: int <- copy curr-col
1345     var bindings2-storage: table
1346     var bindings2/ebx: (addr table) <- address bindings2-storage
1347 #?     print-string 0, "deep-copy {\n"
1348     deep-copy-table bindings, bindings2
1349 #?     print-string 0, "}\n"
1350 #?     print-string 0, "render column {\n"
1351     curr-col <- render-column screen, functions, bindings2, first-line, line, curr-word, top-row, curr-col
1352 #?     print-string 0, "}\n"
1353     # cache cursor column if necessary
1354     $render-line:cache-cursor-column: {
1355       {
1356         var found?/eax: boolean <- call-path-element-match? curr-path, cursor-call-path
1357         compare found?, 0  # false
1358         break-if-= $render-line:cache-cursor-column
1359       }
1360       var dest/edi: (addr int) <- copy cursor-row-addr
1361       {
1362         var src/eax: int <- copy top-row
1363         copy-to *dest, src
1364       }
1365       dest <- copy cursor-col-addr
1366       copy-to *dest, old-col
1367       var cursor-index-in-word/eax: int <- cursor-index curr-word
1368       add-to *dest, cursor-index-in-word
1369     }
1370     # loop update
1371 #?     print-string 0, "next word\n"
1372     var next-word-ah/edx: (addr handle word) <- get curr-word, next
1373     curr-word <- lookup *next-word-ah
1374 #?     {
1375 #?       var foo/eax: int <- copy curr-word
1376 #?       print-int32-hex 0, foo
1377 #?       print-string 0, "\n"
1378 #?     }
1379     increment-final-element curr-path
1380     loop
1381   }
1382   return curr-col
1383 }
1384 
1385 fn callee functions: (addr handle function), word: (addr word), out: (addr handle function) {
1386   var stream-storage: (stream byte 0x10)
1387   var stream/esi: (addr stream byte) <- address stream-storage
1388   emit-word word, stream
1389   find-function functions, stream, out
1390 }
1391 
1392 # Render:
1393 #   - starting at top-row, left-col: final-word
1394 #   - starting somewhere below at left-col: the stack result from interpreting first-world to final-word (inclusive)
1395 #
1396 # Return the farthest column written.
1397 fn render-column screen: (addr screen), functions: (addr handle function), bindings: (addr table), first-line: (addr line), line: (addr line), final-word: (addr word), top-row: int, left-col: int -> _/ecx: int {
1398 #?   print-string 0, "render-column\n"
1399 #?   dump-table bindings
1400   var max-width/esi: int <- copy 0
1401   {
1402     # compute stack
1403     var stack: value-stack
1404     var stack-addr/edi: (addr value-stack) <- address stack
1405     initialize-value-stack stack-addr, 0x10  # max-words
1406     # copy bindings
1407     var bindings2-storage: table
1408     var bindings2/ebx: (addr table) <- address bindings2-storage
1409 #?     print-string 0, "deep copy table {\n"
1410     deep-copy-table bindings, bindings2
1411 #?     print-string 0, "}\n"
1412     evaluate functions, bindings2, first-line, final-word, stack-addr
1413     # indent stack
1414     var indented-col/ebx: int <- copy left-col
1415     indented-col <- add 1  # margin-right
1416     # render stack
1417     var curr-row/edx: int <- copy top-row
1418     curr-row <- add 2  # stack-margin-top
1419     var _max-width/eax: int <- value-stack-max-width stack-addr
1420     max-width <- copy _max-width
1421     {
1422       var top-addr/ecx: (addr int) <- get stack-addr, top
1423       compare *top-addr, 0
1424       break-if-<=
1425       decrement *top-addr
1426       var data-ah/eax: (addr handle array value) <- get stack-addr, data
1427       var data/eax: (addr array value) <- lookup *data-ah
1428       var top/ecx: int <- copy *top-addr
1429       var dest-offset/ecx: (offset value) <- compute-offset data, top
1430       var val/eax: (addr value) <- index data, dest-offset
1431       render-value-at screen, curr-row, indented-col, val, max-width
1432       var height/eax: int <- value-height val
1433       curr-row <- add height
1434       loop
1435     }
1436   }
1437 
1438   max-width <- add 2  # spaces on either side of items on the stack
1439 
1440   # render word, initialize result
1441   reset-formatting screen
1442   move-cursor screen, top-row, left-col
1443   print-word screen, final-word
1444   {
1445     var size/eax: int <- word-length final-word
1446     compare size, max-width
1447     break-if-<=
1448     max-width <- copy size
1449   }
1450 
1451   # post-process right-col
1452   var right-col/ecx: int <- copy left-col
1453   right-col <- add max-width
1454   right-col <- add 1  # margin-right
1455 #?   print-int32-decimal 0, left-col
1456 #?   print-string 0, " => "
1457 #?   print-int32-decimal 0, right-col
1458 #?   print-string 0, "\n"
1459   return right-col
1460 }
1461 
1462 fn clear-canvas _env: (addr environment) {
1463   var env/esi: (addr environment) <- copy _env
1464   var screen-ah/edi: (addr handle screen) <- get env, screen
1465   var _screen/eax: (addr screen) <- lookup *screen-ah
1466   var screen/edi: (addr screen) <- copy _screen
1467   clear-screen screen
1468   var nrows/eax: (addr int) <- get env, nrows
1469   var _repl-col/ecx: (addr int) <- get env, code-separator-col
1470   var repl-col/ecx: int <- copy *_repl-col
1471   draw-vertical-line screen, 1, *nrows, repl-col
1472   # wordstar-style cheatsheet of shortcuts
1473   move-cursor screen, *nrows, 0
1474   start-reverse-video screen
1475   print-string screen, " ctrl-q "
1476   reset-formatting screen
1477   print-string screen, " quit "
1478   var menu-start/ebx: int <- copy repl-col
1479   menu-start <- subtract 0x48  # 72 = half the size of the menu
1480   move-cursor screen, *nrows, menu-start
1481   start-reverse-video screen
1482   print-string screen, " ctrl-a "
1483   reset-formatting screen
1484   print-string screen, " ⏮   "
1485   start-reverse-video screen
1486   print-string screen, " ctrl-b "
1487   reset-formatting screen
1488   print-string screen, " ◀ word  "
1489   start-reverse-video screen
1490   print-string screen, " ctrl-f "
1491   reset-formatting screen
1492   print-string screen, " word ▶  "
1493   start-reverse-video screen
1494   print-string screen, " ctrl-e "
1495   reset-formatting screen
1496   print-string screen, " ⏭         "
1497   start-reverse-video screen
1498   print-string screen, " ctrl-l "
1499   reset-formatting screen
1500   print-string screen, " new line "
1501   start-reverse-video screen
1502   print-string screen, " ctrl-u "
1503   reset-formatting screen
1504   print-string screen, " clear  "
1505   start-reverse-video screen
1506   print-string screen, " ctrl-n "
1507   reset-formatting screen
1508   print-string screen, " name value  "
1509   start-reverse-video screen
1510   print-string screen, " ctrl-d "
1511   reset-formatting screen
1512   print-string screen, " define function  "
1513   # primitives
1514   var dummy/eax: int <- render-primitives screen, *nrows, repl-col
1515   # currently defined functions
1516   render-functions screen, repl-col, env
1517 }
1518 
1519 # return value: top-most row written to
1520 fn render-primitives screen: (addr screen), bottom-margin-row: int, right-col: int -> _/eax: int {
1521   # render primitives from the bottom of the screen upward
1522   var row/ecx: int <- copy bottom-margin-row
1523   row <- subtract 1
1524   var col/edx: int <- copy 1
1525   move-cursor screen, row, col
1526   row, col <- render-primitive-group screen, row, col, right-col, "numbers: ", "+ - *  "
1527   row, col <- render-primitive-group screen, row, col, right-col, "arrays: ", "len  "
1528   row, col <- render-primitive-group screen, row, col, right-col, "files: ", "open read slurp lines  "
1529   row, col <- render-primitive-group screen, row, col, right-col, "misc: ", "dup swap  "  # hack: keep these at the right of the bottom row
1530   row, col <- render-primitive-group screen, row, col, right-col, "screens: ", "fake-screen print move up down left right  "
1531   # finally print heading up top
1532   row <- decrement
1533   move-cursor screen, row, 1
1534   start-bold screen
1535   print-string screen, "primitives:"
1536   reset-formatting screen
1537   return row
1538 }
1539 
1540 # start at row, col and print the given strings
1541 # move up one row if there isn't enough room before right-col
1542 # return row, col printed until
1543 fn render-primitive-group screen: (addr screen), _row: int, _col: int, right-col: int, _heading: (addr array byte), _contents: (addr array byte) -> _/ecx: int, _/edx: int {
1544   var row/ecx: int <- copy _row
1545   var col/edx: int <- copy _col
1546   # decrement row if necessary
1547   var new-col/ebx: int <- copy col
1548   var heading/esi: (addr array byte) <- copy _heading
1549   var len1/eax: int <- length heading
1550   new-col <- add len1
1551   var contents/edi: (addr array byte) <- copy _contents
1552   var len2/eax: int <- length contents
1553   new-col <- add len2
1554   var bound/eax: int <- copy right-col
1555   bound <- decrement
1556   {
1557     compare new-col, bound
1558     break-if-<=
1559     row <- decrement
1560     col <- copy 1
1561   }
1562   move-cursor screen, row, col
1563   start-color screen, 0xf6, 7
1564   print-string screen, heading
1565   reset-formatting screen
1566   print-string screen, contents
1567   return row, new-col
1568 }
1569 
1570 fn render-functions screen: (addr screen), right-col: int, _env: (addr environment) {
1571   var row/ecx: int <- copy 1
1572   var dummy-col/edx: int <- copy right-col
1573   var env/esi: (addr environment) <- copy _env
1574   var functions/esi: (addr handle function) <- get env, functions
1575   {
1576     var curr/eax: (addr function) <- lookup *functions
1577     compare curr, 0
1578     break-if-=
1579     row, dummy-col <- render-function-right-aligned screen, row, right-col, curr
1580     functions <- get curr, next
1581     row <- add 1  # inter-function-margin
1582     loop
1583   }
1584 }
1585 
1586 # print function starting at row, right-aligned before right-col
1587 # return row, col printed until
1588 fn render-function-right-aligned screen: (addr screen), row: int, right-col: int, f: (addr function) -> _/ecx: int, _/edx: int {
1589   var col/edx: int <- copy right-col
1590   col <- subtract 1  # function-right-margin
1591   var col2/ebx: int <- copy col
1592   var width/eax: int <- function-width f
1593   col <- subtract width
1594   var new-row/ecx: int <- copy row
1595   var height/eax: int <- function-height f
1596   new-row <- add height
1597   new-row <- decrement
1598   col <- subtract 1  # function-left-padding
1599   start-color screen, 0, 0xf7
1600   clear-rect screen, row, col, new-row, col2
1601   col <- add 1
1602 #?   var dummy/eax: grapheme <- read-key-from-real-keyboard
1603   render-function screen, row, col, f
1604   new-row <- add 1  # function-bottom-margin
1605   col <- subtract 1  # function-left-padding
1606   col <- subtract 1  # function-left-margin
1607   reset-formatting screen
1608   return new-row, col
1609 }
1610 
1611 # render function starting at row, col
1612 # only single-line functions supported for now
1613 fn render-function screen: (addr screen), row: int, col: int, _f: (addr function) {
1614   var f/esi: (addr function) <- copy _f
1615   var args/ecx: (addr handle word) <- get f, args
1616   move-cursor screen, row, col
1617   print-words-in-reverse screen, args
1618   var name-ah/eax: (addr handle array byte) <- get f, name
1619   var name/eax: (addr array byte) <- lookup *name-ah
1620   start-bold screen
1621   print-string screen, name
1622   reset-formatting screen
1623   start-color screen, 0, 0xf7
1624   increment row
1625   add-to col, 2
1626   move-cursor screen, row, col
1627   print-string screen, "≡ "
1628   var body-ah/eax: (addr handle line) <- get f, body
1629   var body/eax: (addr line) <- lookup *body-ah
1630   var body-words-ah/eax: (addr handle word) <- get body, data
1631   print-words screen, body-words-ah
1632 }
1633 
1634 fn real-grapheme? g: grapheme -> _/eax: boolean {
1635   # if g == newline return true
1636   compare g, 0xa
1637   {
1638     break-if-!=
1639     return 1  # true
1640   }
1641   # if g == tab return true
1642   compare g, 9
1643   {
1644     break-if-!=
1645     return 1  # true
1646   }
1647   # if g < 32 return false
1648   compare g, 0x20
1649   {
1650     break-if->=
1651     return 0  # false
1652   }
1653   # if g <= 255 return true
1654   compare g, 0xff
1655   {
1656     break-if->
1657     return 1  # true
1658   }
1659   # if (g&0xff == Esc) it's an escape sequence
1660   and-with g, 0xff
1661   compare g, 0x1b  # Esc
1662   {
1663     break-if-!=
1664     return 0  # false
1665   }
1666   # otherwise return true
1667   return 1  # true
1668 }