about summary refs log tree commit diff stats
path: root/apps/tile/environment.mu
blob: a4b7580fddd4f74faf82d649f88a4cd4397e6af5 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.highlight .hll { background-color: #ffffcc }
.highlight .c { color: #888888 } /* Comment */
.highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */
.highlight .k { color: #008800; font-weight: bold } /* Keyword */
.highlight .ch { color: #888888 } /* Comment.Hashbang */
.highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .cpf { color: #888888 } /* Comment.PreprocFile */
.highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.highlight .gr { color: #aa0000 } /* Generic.Error */
.highlight .gh { color: #333333 } /* Generic.Heading */
.highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */
.highlight .go { color: #888888 } /* Generic.Output */
.highlight .gp { color: #555555 } /* Generic.Prompt */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #666666 } /* Generic.Subheading */
.highlight .gt { color: #aa0000 } /* Generic.Traceback */
.highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */
.highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */
.highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */
.highlight .kp { color: #008800 } /* Keyword.Pseudo */
.highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */
.highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */
.highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */
.highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */
.highlight .na { color: #336699 } /* Name.Attribute */
.highlight .nb { color: #003388 } /* Name.Builtin */
.highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */
.highlight .no { color: #003366; font-weight: bold } /* Name.Constant */
.highlight .nd { color: #555555 } /* Name.Decorator */
.highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */
.highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */
.highlight .nl { color: #336699; font-style: italic } /* Name.Label */
.highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */
.highlight .py { color: #336699; font-weight: bold } /* Name.Property */
.highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */
.highlight .nv { color: #336699 } /* Name.Variable */
.highlight .ow { color: #008800 } /* Operator.Word */
.highlight .w { color: #bbbbbb } /* Text.Whitespace */
.highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */
.highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */
.highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */
.highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */
.highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */
.highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */
.highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */
.highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */
.highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */
.highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */
.highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */
.highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */
.highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */
.highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */
.highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */
.highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */
.highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */
.highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */
.highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */
.highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */
.highlight .vc { color: #336699 } /* Name.Variable.Class */
.highlight .vg { color: #dd7700 } /* Name.Variable.Global */
.highlight .vi { color: #3333bb } /* Name.Variable.Instance */
.highlight .vm { color: #336699 } /* Name.Variable.Magic */
.highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
# Draw pixels in response to keyboard events, starting from the top-left
# and in raster order.
#
# To run, first prepare a realistically sized disk image:
#   dd if=/dev/zero of=disk.img count=20160  # 512-byte sectors, so 10MB
# Load the program on the disk image:
#   cat baremetal/boot.hex baremetal/ex3.hex  |./bootstrap run apps/hex  > a.bin
#   dd if=a.bin of=disk.img conv=notrunc
# To run:
#   qemu-system-i386 disk.img
# Or:
#   bochs -f baremetal/boot.bochsrc  # boot.bochsrc loads disk.img

# main:  (address 0x9000)

# eax <- LFB
8b  # copy *rm32 to r32
  05  # 00/mod/indirect 000/r32/eax 101/rm32/use-disp32
  28 7f 00 00 # disp32 [label]

# var read index/ecx: byte = 0
31 c9  # ecx <- xor ecx;  11/direct 001/r32/ecx 001/rm32/ecx

# $loop:
  # CL = *read index
  8a  # copy m8 at r32 to r8
    0d  # 00/mod/indirect 001/r8/cl 101/rm32/use-disp32
    cc 7d 00 00  # disp32 [label]
  # CL = *(keyboard buffer + ecx)
  8a  # copy m8 at r32 to r8
    89  # 10/mod/*+disp32 001/r8/cl 001/rm32/ecx
    d0 7d 00 00  # disp32 [label]
  # if (CL == 0) loop (spin loop)
  80
    f9  # 11/mod/direct 111/subop/compare 001/rm8/CL
    00  # imm8
  74 ef  # loop -17 [label]
# offset 0x19:
  # otherwise increment read index
  fe  # increment byte
    05  # 00/mod/indirect 000/subop/increment 101/rm32/use-disp32
    cc 7d 00 00  # disp32 [label]
  # clear top nibble of index (keyboard buffer is circular)
  80  # and byte
    25  # 00/mod/indirect 100/subop/and 101/rm32/use-disp32
    cc 7d 00 00  # disp32 [label]
    0f  # imm8
  # print a pixel in fluorescent green
  c6  # copy imm8 to m8 at rm32
    00  # 00/mod/indirect 000/subop 000/rm32/eax
    31  # imm32
  40  # increment eax
  eb dc # loop -36 [label]

# $break:
e9 fb ff ff ff  # hang indefinitely

# vim:ft=subx
'#n278'>278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669
type environment {
  screen: (handle screen)
  functions: (handle function)
  sandboxes: (handle sandbox)
  nrows: int
  ncols: int
  code-separator-col: int
}

fn initialize-environment _env: (addr environment) {
  var env/esi: (addr environment) <- copy _env
  # initialize some predefined function definitions
  var functions/eax: (addr handle function) <- get env, functions
  create-primitive-functions functions
  # initialize first sandbox
  var sandbox-ah/eax: (addr handle sandbox) <- get env, sandboxes
  allocate sandbox-ah
  var sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
  initialize-sandbox sandbox
  # initialize screen
  var screen-ah/eax: (addr handle screen) <- get env, screen
  var _screen/eax: (addr screen) <- lookup *screen-ah
  var screen/edi: (addr screen) <- copy _screen
  var nrows/eax: int <- copy 0
  var ncols/ecx: int <- copy 0
  nrows, ncols <- screen-size screen
  var dest/edx: (addr int) <- get env, nrows
  copy-to *dest, nrows
  dest <- get env, ncols
  copy-to *dest, ncols
  var repl-col/ecx: int <- copy ncols
  repl-col <- shift-right 1
  dest <- get env, code-separator-col
  copy-to *dest, repl-col
}

fn draw-screen _env: (addr environment) {
  var env/esi: (addr environment) <- copy _env
  var screen-ah/eax: (addr handle screen) <- get env, screen
  var _screen/eax: (addr screen) <- lookup *screen-ah
  var screen/edi: (addr screen) <- copy _screen
  var dest/edx: (addr int) <- get env, code-separator-col
  var tmp/eax: int <- copy *dest
  clear-canvas env
  tmp <- add 2  # repl-margin-left
  move-cursor screen, 3, tmp  # input-row
}

fn initialize-environment-with-fake-screen _self: (addr environment), nrows: int, ncols: int {
  var self/esi: (addr environment) <- copy _self
  var screen-ah/eax: (addr handle screen) <- get self, screen
  allocate screen-ah
  var screen-addr/eax: (addr screen) <- lookup *screen-ah
  initialize-screen screen-addr, nrows, ncols
  initialize-environment self
}

#############
# Iterate
#############

fn process _self: (addr environment), key: grapheme {
$process:body: {
  var self/esi: (addr environment) <- copy _self
  var sandbox-ah/eax: (addr handle sandbox) <- get self, sandboxes
  var _sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
  var sandbox/edi: (addr sandbox) <- copy _sandbox
  var rename-word-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-cursor-word
  var rename-word-mode?/eax: (addr word) <- lookup *rename-word-mode-ah?
  compare rename-word-mode?, 0
  {
    break-if-=
#?     print-string 0, "processing sandbox rename\n"
    process-sandbox-rename sandbox, key
    break $process:body
  }
  var define-function-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-function
  var define-function-mode?/eax: (addr word) <- lookup *define-function-mode-ah?
  compare define-function-mode?, 0
  {
    break-if-=
#?     print-string 0, "processing function definition\n"
    var functions/ecx: (addr handle function) <- get self, functions
    process-sandbox-define sandbox, functions, key
    break $process:body
  }
#?   print-string 0, "processing sandbox\n"
  process-sandbox self, sandbox, key
}
}

fn process-sandbox _self: (addr environment), _sandbox: (addr sandbox), key: grapheme {
$process-sandbox:body: {
  var self/esi: (addr environment) <- copy _self
  var sandbox/edi: (addr sandbox) <- copy _sandbox
  var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
  var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
  var cursor-word-ah/ebx: (addr handle word) <- get cursor-call-path, word
  var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
  var cursor-word/ecx: (addr word) <- copy _cursor-word
  compare key, 0x445b1b  # left-arrow
  $process-sandbox:key-left-arrow: {
    break-if-!=
#?     print-string 0, "left-arrow\n"
    # if not at start, move left within current word
    var at-start?/eax: boolean <- cursor-at-start? cursor-word
    compare at-start?, 0  # false
    {
      break-if-!=
#?       print-string 0, "cursor left within word\n"
      cursor-left cursor-word
      break $process-sandbox:body
    }
    # if current word is expanded, move to the rightmost word in its body
    {
      var cursor-call-path/esi: (addr handle call-path-element) <- get sandbox, cursor-call-path
      var expanded-words/edx: (addr handle call-path) <- get sandbox, expanded-words
      var curr-word-is-expanded?/eax: boolean <- find-in-call-paths expanded-words, cursor-call-path
      compare curr-word-is-expanded?, 0  # false
      break-if-=
      # update cursor-call-path
#?       print-string 0, "curr word is expanded\n"
      var self/ecx: (addr environment) <- copy _self
      var functions/ecx: (addr handle function) <- get self, functions
      var body: (handle line)
      var body-ah/eax: (addr handle line) <- address body
      function-body functions, cursor-word-ah, body-ah
      var body-addr/eax: (addr line) <- lookup *body-ah
      var first-word-ah/edx: (addr handle word) <- get body-addr, data
      var final-word-h: (handle word)
      var final-word-ah/eax: (addr handle word) <- address final-word-h
      final-word first-word-ah, final-word-ah
      push-to-call-path-element cursor-call-path, final-word-ah
      # move cursor to end of word
      var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
      var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
      var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
      cursor-to-end cursor-word
      break $process-sandbox:body
    }
    # if at first word, look for a caller to jump to
    $process-sandbox:key-left-arrow-first-word: {
      var prev-word-ah/edx: (addr handle word) <- get cursor-word, prev
      var prev-word/eax: (addr word) <- lookup *prev-word-ah
      compare prev-word, 0
      break-if-!=
      $process-sandbox:key-left-arrow-first-word-and-caller: {
#?         print-string 0, "return\n"
        {
          var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
          var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
          var next-cursor-element-ah/edx: (addr handle call-path-element) <- get cursor-call-path, next
          var next-cursor-element/eax: (addr call-path-element) <- lookup *next-cursor-element-ah
          compare next-cursor-element, 0
          break-if-= $process-sandbox:key-left-arrow-first-word-and-caller
          copy-object next-cursor-element-ah, cursor-call-path-ah
        }
        var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
        var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
        var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
        var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
        cursor-word <- copy _cursor-word
      }
    }
    # then move to end of previous word
    var prev-word-ah/edx: (addr handle word) <- get cursor-word, prev
    var prev-word/eax: (addr word) <- lookup *prev-word-ah
    {
      compare prev-word, 0
      break-if-=
#?       print-string 0, "move to previous word\n"
      cursor-to-end prev-word
#?       {
#?         var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
#?         var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
#?         var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
#?         var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
#?         var cursor-word/ebx: (addr word) <- copy _cursor-word
#?         print-string 0, "word at cursor before: "
#?         print-word 0, cursor-word
#?         print-string 0, "\n"
#?       }
      var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      decrement-final-element cursor-call-path
#?       {
#?         var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
#?         var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
#?         var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
#?         var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
#?         var cursor-word/ebx: (addr word) <- copy _cursor-word
#?         print-string 0, "word at cursor after: "
#?         print-word 0, cursor-word
#?         print-string 0, "\n"
#?       }
    }
    break $process-sandbox:body
  }
  compare key, 0x435b1b  # right-arrow
  $process-sandbox:key-right-arrow: {
    break-if-!=
    # if not at end, move right within current word
    var at-end?/eax: boolean <- cursor-at-end? cursor-word
    compare at-end?, 0  # false
    {
      break-if-!=
#?       print-string 0, "a\n"
      cursor-right cursor-word
      break $process-sandbox:body
    }
    # if at final word, look for a caller to jump to
    {
      var next-word-ah/edx: (addr handle word) <- get cursor-word, next
      var next-word/eax: (addr word) <- lookup *next-word-ah
      compare next-word, 0
      break-if-!=
      var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
      var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
      var next-cursor-element-ah/ecx: (addr handle call-path-element) <- get cursor-call-path, next
      var next-cursor-element/eax: (addr call-path-element) <- lookup *next-cursor-element-ah
      compare next-cursor-element, 0
      break-if-=
      copy-object next-cursor-element-ah, cursor-call-path-ah
      break $process-sandbox:body
    }
    # otherwise, move to the next word
    var next-word-ah/edx: (addr handle word) <- get cursor-word, next
    var next-word/eax: (addr word) <- lookup *next-word-ah
    {
      compare next-word, 0
      break-if-=
#?       print-string 0, "b\n"
      cursor-to-start next-word
      # . . cursor-word now out of date
      var cursor-call-path/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
      increment-final-element cursor-call-path
      # Is the new cursor word expanded? If so, it's a function call. Add a
      # new level to the cursor-call-path for the call's body.
      $process-sandbox:key-right-arrow-next-word-is-call-expanded: {
#?         print-string 0, "c\n"
        {
          var expanded-words/eax: (addr handle call-path) <- get sandbox, expanded-words
          var curr-word-is-expanded?/eax: boolean <- find-in-call-paths expanded-words, cursor-call-path
          compare curr-word-is-expanded?, 0  # false
          break-if-= $process-sandbox:key-right-arrow-next-word-is-call-expanded
        }
        var callee-h: (handle function)
        var callee-ah/edx: (addr handle function) <- address callee-h
        var functions/ebx: (addr handle function) <- get self, functions
        callee functions, next-word, callee-ah
        var callee/eax: (addr function) <- lookup *callee-ah
        var callee-body-ah/eax: (addr handle line) <- get callee, body
        var callee-body/eax: (addr line) <- lookup *callee-body-ah
        var callee-body-first-word/edx: (addr handle word) <- get callee-body, data
        push-to-call-path-element cursor-call-path, callee-body-first-word
        # position cursor at left
        var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
        var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
        var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
        var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
        cursor-to-start cursor-word
#?         print-string 0, "d\n"
        break $process-sandbox:body
      }
    }
    break $process-sandbox:body
  }
  compare key, 0xa  # enter
  {
    break-if-!=
    # toggle display of subsidiary stack
    toggle-cursor-word sandbox
    break $process-sandbox:body
  }
  # word-based motions
  compare key, 2  # ctrl-b
  $process-sandbox:prev-word: {
    break-if-!=
    # jump to previous word at same level
    var prev-word-ah/edx: (addr handle word) <- get cursor-word, prev
    var prev-word/eax: (addr word) <- lookup *prev-word-ah
    {
      compare prev-word, 0
      break-if-=
      cursor-to-end prev-word
      var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      decrement-final-element cursor-call-path
      break $process-sandbox:body
    }
    # if previous word doesn't exist, try to bump up one level
    {
      var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
      var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
      var caller-cursor-element-ah/ecx: (addr handle call-path-element) <- get cursor-call-path, next
      var caller-cursor-element/eax: (addr call-path-element) <- lookup *caller-cursor-element-ah
      compare caller-cursor-element, 0
      break-if-=
      # check if previous word exists in caller
      var caller-word-ah/eax: (addr handle word) <- get caller-cursor-element, word
      var caller-word/eax: (addr word) <- lookup *caller-word-ah
      var word-before-caller-ah/eax: (addr handle word) <- get caller-word, prev
      var word-before-caller/eax: (addr word) <- lookup *word-before-caller-ah
      compare word-before-caller, 0
      break-if-=
      # if so jump to it
      drop-from-call-path-element cursor-call-path-ah
      decrement-final-element cursor-call-path-ah
      break $process-sandbox:body
    }
  }
  compare key, 6  # ctrl-f
  $process-sandbox:next-word: {
    break-if-!=
#?     print-string 0, "AA\n"
    # jump to previous word at same level
    var next-word-ah/edx: (addr handle word) <- get cursor-word, next
    var next-word/eax: (addr word) <- lookup *next-word-ah
    {
      compare next-word, 0
      break-if-=
#?       print-string 0, "BB\n"
      cursor-to-end next-word
      var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      increment-final-element cursor-call-path
      break $process-sandbox:body
    }
    # if next word doesn't exist, try to bump up one level
#?     print-string 0, "CC\n"
    var cursor-call-path-ah/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
    var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
    var caller-cursor-element-ah/ecx: (addr handle call-path-element) <- get cursor-call-path, next
    var caller-cursor-element/eax: (addr call-path-element) <- lookup *caller-cursor-element-ah
    compare caller-cursor-element, 0
    break-if-=
#?     print-string 0, "DD\n"
    copy-object caller-cursor-element-ah, cursor-call-path-ah
    break $process-sandbox:body
  }
  # line-based motions
  compare key, 1  # ctrl-a
  $process-sandbox:start-of-line: {
    break-if-!=
    # move cursor up past all calls and to start of line
    var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
    drop-nested-calls cursor-call-path-ah
    move-final-element-to-start-of-line cursor-call-path-ah
    # move cursor to start of initial word
    var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
    var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
    var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
    cursor-to-start cursor-word
    # this works as long as the first word isn't expanded
    # but we don't expect to see zero-arg functions first-up
    break $process-sandbox:body
  }
  compare key, 5  # ctrl-e
  $process-sandbox:end-of-line: {
    break-if-!=
    # move cursor to final word of sandbox
    var cursor-call-path-ah/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
    initialize-path-from-sandbox sandbox, cursor-call-path-ah
    var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
    var dest/eax: (addr handle word) <- get cursor-call-path, word
    final-word dest, dest
    # move cursor to end of final word
    var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
    cursor-to-end cursor-word
    # this works because expanded words lie to the right of their bodies
    # so the final word is always guaranteed to be at the top-level
    break $process-sandbox:body
  }
  compare key, 0x15  # ctrl-u
  $process-sandbox:clear-line: {
    break-if-!=
    # clear line in sandbox
    initialize-sandbox sandbox
    break $process-sandbox:body
  }
  # if cursor is within a call, disable editing hotkeys below
  var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
  var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
  var next-cursor-element-ah/eax: (addr handle call-path-element) <- get cursor-call-path, next
  var next-cursor-element/eax: (addr call-path-element) <- lookup *next-cursor-element-ah
  compare next-cursor-element, 0
  break-if-!= $process-sandbox:body
  # - remaining keys only work at the top row outside any function calls
  compare key, 0x7f  # del (backspace on Macs)
  $process-sandbox:backspace: {
    break-if-!=
    # if not at start of some word, delete grapheme before cursor within current word
    var at-start?/eax: boolean <- cursor-at-start? cursor-word
    compare at-start?, 0  # false
    {
      break-if-!=
      delete-before-cursor cursor-word
      break $process-sandbox:body
    }
    # otherwise delete current word and move to end of prev word
    var prev-word-ah/eax: (addr handle word) <- get cursor-word, prev
    var prev-word/eax: (addr word) <- lookup *prev-word-ah
    {
      compare prev-word, 0
      break-if-=
      cursor-to-end prev-word
      delete-next prev-word
      var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      decrement-final-element cursor-call-path
    }
    break $process-sandbox:body
  }
  compare key, 0x20  # space
  $process-sandbox:space: {
    break-if-!=
#?     print-string 0, "space\n"
    # if cursor is at start of word, insert word before
    {
      var at-start?/eax: boolean <- cursor-at-start? cursor-word
      compare at-start?, 0  # false
      break-if-=
      var prev-word-ah/eax: (addr handle word) <- get cursor-word, prev
      append-word prev-word-ah
      var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      decrement-final-element cursor-call-path
      break $process-sandbox:body
    }
    # if start of word is quote and grapheme before cursor is not, just insert it as usual
    # TODO: support string escaping
    {
      var first-grapheme/eax: grapheme <- first-grapheme cursor-word
      compare first-grapheme, 0x22  # double quote
      break-if-!=
      var final-grapheme/eax: grapheme <- grapheme-before-cursor cursor-word
      compare final-grapheme, 0x22  # double quote
      break-if-=
      break $process-sandbox:space
    }
    # if start of word is '[' and grapheme before cursor is not ']', just insert it as usual
    # TODO: support nested arrays
    {
      var first-grapheme/eax: grapheme <- first-grapheme cursor-word
      compare first-grapheme, 0x5b  # '['
      break-if-!=
      var final-grapheme/eax: grapheme <- grapheme-before-cursor cursor-word
      compare final-grapheme, 0x5d  # ']'
      break-if-=
      break $process-sandbox:space
    }
    # otherwise insert word after and move cursor to it for the next key
    # (but we'll continue to track the current cursor-word for the rest of this function)
    append-word cursor-word-ah
    var cursor-call-path/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
    increment-final-element cursor-call-path
    # if cursor is at end of word, that's all
    var at-end?/eax: boolean <- cursor-at-end? cursor-word
    compare at-end?, 0  # false
    break-if-!= $process-sandbox:body
    # otherwise we're in the middle of a word
    # move everything after cursor to the (just created) next word
    var next-word-ah/eax: (addr handle word) <- get cursor-word, next
    var _next-word/eax: (addr word) <- lookup *next-word-ah
    var next-word/ebx: (addr word) <- copy _next-word
    {
      var at-end?/eax: boolean <- cursor-at-end? cursor-word
      compare at-end?, 0  # false
      break-if-!=
      var g/eax: grapheme <- pop-after-cursor cursor-word
      add-grapheme-to-word next-word, g
      loop
    }
    cursor-to-start next-word
    break $process-sandbox:body
  }
  compare key, 0xe  # ctrl-n
  $process:rename-word: {
    break-if-!=
    # TODO: ensure current word is not a function
    # rename word at cursor
    var new-name-ah/eax: (addr handle word) <- get sandbox, partial-name-for-cursor-word
    allocate new-name-ah
    var new-name/eax: (addr word) <- lookup *new-name-ah
    initialize-word new-name
    break $process-sandbox:body
  }
  compare key, 4  # ctrl-d
  $process:define-function: {
    break-if-!=
    # define function out of line at cursor
    var new-name-ah/eax: (addr handle word) <- get sandbox, partial-name-for-function
    allocate new-name-ah
    var new-name/eax: (addr word) <- lookup *new-name-ah
    initialize-word new-name
    break $process-sandbox:body
  }
  # otherwise insert key within current word
  var g/edx: grapheme <- copy key
  var print?/eax: boolean <- real-grapheme? key
  $process-sandbox:real-grapheme: {
    compare print?, 0  # false
    break-if-=
    add-grapheme-to-word cursor-word, g
    break $process-sandbox:body
  }
  # silently ignore other hotkeys
}
}

# collect new name in partial-name-for-cursor-word, and then rename the word
# at cursor to it
# Precondition: cursor-call-path is a singleton (not within a call)
fn process-sandbox-rename _sandbox: (addr sandbox), key: grapheme {
$process-sandbox-rename:body: {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  var new-name-ah/edi: (addr handle word) <- get sandbox, partial-name-for-cursor-word
  # if 'esc' pressed, cancel rename
  compare key, 0x1b  # esc
  $process-sandbox-rename:cancel: {
    break-if-!=
    var empty: (handle word)
    copy-handle empty, new-name-ah
    break $process-sandbox-rename:body
  }
  # if 'enter' pressed, perform rename
  compare key, 0xa  # enter
  $process-sandbox-rename:commit: {
    break-if-!=
#?     print-string 0, "rename\n"
    # new line
    var new-line-h: (handle line)
    var new-line-ah/eax: (addr handle line) <- address new-line-h
    allocate new-line-ah
    var new-line/eax: (addr line) <- lookup *new-line-ah
    initialize-line new-line
    var new-line-word-ah/ecx: (addr handle word) <- get new-line, data
    {
      # move word at cursor to new line
      var cursor-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      var cursor/eax: (addr call-path-element) <- lookup *cursor-ah
      var word-at-cursor-ah/eax: (addr handle word) <- get cursor, word
#?       print-string 0, "cursor before at word "
#?       {
#?         var cursor-word/eax: (addr word) <- lookup *word-at-cursor-ah
#?         print-word 0, cursor-word
#?         print-string 0, "\n"
#?       }
      move-word-contents word-at-cursor-ah, new-line-word-ah
      # copy name to word at cursor
      copy-word-contents-before-cursor new-name-ah, word-at-cursor-ah
#?       print-string 0, "cursor after at word "
#?       {
#?         var cursor-word/eax: (addr word) <- lookup *word-at-cursor-ah
#?         print-word 0, cursor-word
#?         print-string 0, "\n"
#?         var foo/eax: int <- copy cursor-word
#?         print-int32-hex 0, foo
#?         print-string 0, "\n"
#?       }
#?       print-string 0, "new name word "
#?       {
#?         var new-name/eax: (addr word) <- lookup *new-name-ah
#?         print-word 0, new-name
#?         print-string 0, "\n"
#?         var foo/eax: int <- copy new-name
#?         print-int32-hex 0, foo
#?         print-string 0, "\n"
#?       }
    }
    # prepend '=' to name
    {
      var new-name/eax: (addr word) <- lookup *new-name-ah
      cursor-to-start new-name
      add-grapheme-to-word new-name, 0x3d  # '='
    }
    # append name to new line
    chain-words new-line-word-ah, new-name-ah
    # new-line->next = sandbox->data
    var new-line-next/ecx: (addr handle line) <- get new-line, next
    var sandbox-slot/edx: (addr handle line) <- get sandbox, data
    copy-object sandbox-slot, new-line-next
    # sandbox->data = new-line
    copy-handle new-line-h, sandbox-slot
    # clear partial-name-for-cursor-word
    var empty: (handle word)
    copy-handle empty, new-name-ah
#?     # XXX
#?     var cursor-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
#?     var cursor/eax: (addr call-path-element) <- lookup *cursor-ah
#?     var word-at-cursor-ah/eax: (addr handle word) <- get cursor, word
#?     print-string 0, "cursor after rename: "
#?     {
#?       var cursor-word/eax: (addr word) <- lookup *word-at-cursor-ah
#?       print-word 0, cursor-word
#?       print-string 0, " -- "
#?       var foo/eax: int <- copy cursor-word
#?       print-int32-hex 0, foo
#?       print-string 0, "\n"
#?     }
    break $process-sandbox-rename:body
  }
  #
  compare key, 0x7f  # del (backspace on Macs)
  $process-sandbox-rename:backspace: {
    break-if-!=
    # if not at start, delete grapheme before cursor
    var new-name/eax: (addr word) <- lookup *new-name-ah
    var at-start?/eax: boolean <- cursor-at-start? new-name
    compare at-start?, 0  # false
    {
      break-if-!=
      var new-name/eax: (addr word) <- lookup *new-name-ah
      delete-before-cursor new-name
    }
    break $process-sandbox-rename:body
  }
  # otherwise insert key within current word
  var print?/eax: boolean <- real-grapheme? key
  $process-sandbox-rename:real-grapheme: {
    compare print?, 0  # false
    break-if-=
    var new-name/eax: (addr word) <- lookup *new-name-ah
    add-grapheme-to-word new-name, key
    break $process-sandbox-rename:body
  }
  # silently ignore other hotkeys
}
}

# collect new name in partial-name-for-function, and then define the last line
# of the sandbox to be a new function with that name. Replace the last line
# with a call to the appropriate function.
# Precondition: cursor-call-path is a singleton (not within a call)
fn process-sandbox-define _sandbox: (addr sandbox), functions: (addr handle function), key: grapheme {
$process-sandbox-define:body: {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  var new-name-ah/edi: (addr handle word) <- get sandbox, partial-name-for-function
  # if 'esc' pressed, cancel define
  compare key, 0x1b  # esc
  $process-sandbox-define:cancel: {
    break-if-!=
    var empty: (handle word)
    copy-handle empty, new-name-ah
    break $process-sandbox-define:body
  }
  # if 'enter' pressed, perform define
  compare key, 0xa  # enter
  $process-sandbox-define:commit: {
    break-if-!=
#?     print-string 0, "define\n"
    # create new function
    var new-function: (handle function)
    var new-function-ah/ecx: (addr handle function) <- address new-function
    allocate new-function-ah
    var _new-function/eax: (addr function) <- lookup *new-function-ah
    var new-function/ebx: (addr function) <- copy _new-function
    var dest/edx: (addr handle function) <- get new-function, next
    copy-object functions, dest
    copy-object new-function-ah, functions
    # set function name to new-name
    var new-name/eax: (addr word) <- lookup *new-name-ah
    var dest/edx: (addr handle array byte) <- get new-function, name
    word-to-string new-name, dest
    # move final line to body
    var body-ah/eax: (addr handle line) <- get new-function, body
    allocate body-ah
    var body/eax: (addr line) <- lookup *body-ah
    var body-contents/ecx: (addr handle word) <- get body, data
    var final-line-storage: (handle line)
    var final-line-ah/eax: (addr handle line) <- address final-line-storage
    final-line sandbox, final-line-ah
    var final-line/eax: (addr line) <- lookup *final-line-ah
    var final-line-contents/eax: (addr handle word) <- get final-line, data
    copy-object final-line-contents, body-contents
    #
    copy-unbound-words-to-args functions
    #
    var empty-word: (handle word)
    copy-handle empty-word, final-line-contents
    construct-call functions, final-line-contents
    # clear partial-name-for-function
    var empty-word: (handle word)
    copy-handle empty-word, new-name-ah
    # update cursor
    var final-line/eax: (addr line) <- lookup final-line-storage
    var cursor-call-path-ah/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
    allocate cursor-call-path-ah  # leak
    initialize-path-from-line final-line, cursor-call-path-ah
    break $process-sandbox-define:body
  }
  #
  compare key, 0x7f  # del (backspace on Macs)
  $process-sandbox-define:backspace: {
    break-if-!=
    # if not at start, delete grapheme before cursor
    var new-name/eax: (addr word) <- lookup *new-name-ah
    var at-start?/eax: boolean <- cursor-at-start? new-name
    compare at-start?, 0  # false
    {
      break-if-!=
      var new-name/eax: (addr word) <- lookup *new-name-ah
      delete-before-cursor new-name
    }
    break $process-sandbox-define:body
  }
  # otherwise insert key within current word
  var print?/eax: boolean <- real-grapheme? key
  $process-sandbox-define:real-grapheme: {
    compare print?, 0  # false
    break-if-=
    var new-name/eax: (addr word) <- lookup *new-name-ah
    add-grapheme-to-word new-name, key
    break $process-sandbox-define:body
  }
  # silently ignore other hotkeys
}
}

# extract from the body of the first function in 'functions' all words that
# aren't defined in the rest of 'functions'. Prepend them in reverse order.
# Assumes function body is a single line for now.
fn copy-unbound-words-to-args _functions: (addr handle function) {
  # target
  var target-ah/eax: (addr handle function) <- copy _functions
  var _target/eax: (addr function) <- lookup *target-ah
  var target/esi: (addr function) <- copy _target
  var dest-ah/edi: (addr handle word) <- get target, args
  # next
  var functions-ah/edx: (addr handle function) <- get target, next
  # src
  var line-ah/eax: (addr handle line) <- get target, body
  var line/eax: (addr line) <- lookup *line-ah
  var curr-ah/eax: (addr handle word) <- get line, data
  var curr/eax: (addr word) <- lookup *curr-ah
  {
    compare curr, 0
    break-if-=
    $copy-unbound-words-to-args:loop-iter: {
      # is it a number?
      {
        var is-int?/eax: boolean <- word-is-decimal-integer? curr
        compare is-int?, 0  # false
        break-if-!= $copy-unbound-words-to-args:loop-iter
      }
      # is it a pre-existing function?
      var bound?/ebx: boolean <- bound-function? curr, functions-ah
      compare bound?, 0  # false
      break-if-!=
      # is it already bound as an arg?
      var dup?/ebx: boolean <- arg-exists? _functions, curr  # _functions = target-ah
      compare dup?, 0  # false
      break-if-!= $copy-unbound-words-to-args:loop-iter
      # push copy of curr before dest-ah
      var rest-h: (handle word)
      var rest-ah/ecx: (addr handle word) <- address rest-h
      copy-object dest-ah, rest-ah
      copy-word curr, dest-ah
      chain-words dest-ah, rest-ah
    }
    var next-ah/ecx: (addr handle word) <- get curr, next
    curr <- lookup *next-ah
    loop
  }
}

fn bound-function? w: (addr word), functions-ah: (addr handle function) -> _/ebx: boolean {
  var result/ebx: boolean <- copy 1  # true
  {
    # if w == "+" return true
    var subresult/eax: boolean <- word-equal? w, "+"
    compare subresult, 0  # false
    break-if-!=
    # if w == "-" return true
    subresult <- word-equal? w, "-"
    compare subresult, 0  # false
    break-if-!=
    # if w == "*" return true
    subresult <- word-equal? w, "*"
    compare subresult, 0  # false
    break-if-!=
    # if w == "len" return true
    subresult <- word-equal? w, "len"
    compare subresult, 0  # false
    break-if-!=
    # if w == "open" return true
    subresult <- word-equal? w, "open"
    compare subresult, 0  # false
    break-if-!=
    # if w == "read" return true
    subresult <- word-equal? w, "read"
    compare subresult, 0  # false
    break-if-!=
    # if w == "slurp" return true
    subresult <- word-equal? w, "slurp"
    compare subresult, 0  # false
    break-if-!=
    # if w == "lines" return true
    subresult <- word-equal? w, "lines"
    compare subresult, 0  # false
    break-if-!=
    # if w == "dup" return true
    subresult <- word-equal? w, "dup"
    compare subresult, 0  # false
    break-if-!=
    # if w == "swap" return true
    subresult <- word-equal? w, "swap"
    compare subresult, 0  # false
    break-if-!=
    # return w in functions
    var out-h: (handle function)
    var out/eax: (addr handle function) <- address out-h
    callee functions-ah, w, out
    var found?/eax: (addr function) <- lookup *out
    result <- copy found?
  }
  return result
}

fn arg-exists? _f-ah: (addr handle function), arg: (addr word) -> _/ebx: boolean {
  var f-ah/eax: (addr handle function) <- copy _f-ah
  var f/eax: (addr function) <- lookup *f-ah
  var args-ah/eax: (addr handle word) <- get f, args
  var result/ebx: boolean <- word-exists? args-ah, arg
  return result
}

# construct a call to `f` with copies of exactly its args
fn construct-call _f-ah: (addr handle function), _dest-ah: (addr handle word) {
  var f-ah/eax: (addr handle function) <- copy _f-ah
  var _f/eax: (addr function) <- lookup *f-ah
  var f/esi: (addr function) <- copy _f
  # append args in reverse
  var args-ah/eax: (addr handle word) <- get f, args
  var dest-ah/edi: (addr handle word) <- copy _dest-ah
  copy-words-in-reverse args-ah, dest-ah
  # append name
  var name-ah/eax: (addr handle array byte) <- get f, name
  var name/eax: (addr array byte) <- lookup *name-ah
  append-word-at-end-with dest-ah, name
}

fn word-index _words: (addr handle word), _n: int, out: (addr handle word) {
$word-index:body: {
  var n/ecx: int <- copy _n
  {
    compare n, 0
    break-if-!=
    copy-object _words, out
    break $word-index:body
  }
  var words-ah/eax: (addr handle word) <- copy _words
  var words/eax: (addr word) <- lookup *words-ah
  var next/eax: (addr handle word) <- get words, next
  n <- decrement
  word-index next, n, out
}
}

fn toggle-cursor-word _sandbox: (addr sandbox) {
$toggle-cursor-word:body: {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  var expanded-words/edi: (addr handle call-path) <- get sandbox, expanded-words
  var cursor-call-path/ecx: (addr handle call-path-element) <- get sandbox, cursor-call-path
#?   print-string 0, "cursor call path: "
#?   dump-call-path-element 0, cursor-call-path
#?   print-string 0, "expanded words:\n"
#?   dump-call-paths 0, expanded-words
  var already-expanded?/eax: boolean <- find-in-call-paths expanded-words, cursor-call-path
  compare already-expanded?, 0  # false
  {
    break-if-!=
#?     print-string 0, "expand\n"
    # if not already-expanded, insert
    insert-in-call-path expanded-words cursor-call-path
#?     print-string 0, "expanded words now:\n"
#?     dump-call-paths 0, expanded-words
    break $toggle-cursor-word:body
  }
  {
    break-if-=
    # otherwise delete
    delete-in-call-path expanded-words cursor-call-path
  }
}
}

#############
# Visualize
#############

fn evaluate-environment _env: (addr environment), stack: (addr value-stack) {
  var env/esi: (addr environment) <- copy _env
  # functions
  var functions/edx: (addr handle function) <- get env, functions
  # line
  var sandbox-ah/esi: (addr handle sandbox) <- get env, sandboxes
  var sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
  var line-ah/eax: (addr handle line) <- get sandbox, data
  var _line/eax: (addr line) <- lookup *line-ah
  var line/esi: (addr line) <- copy _line
  evaluate functions, 0, line, 0, stack
}

fn render _env: (addr environment) {
#?   print-string 0, "==\n"
  var env/esi: (addr environment) <- copy _env
  clear-canvas env
  # screen
  var screen-ah/eax: (addr handle screen) <- get env, screen
  var _screen/eax: (addr screen) <- lookup *screen-ah
  var screen/edi: (addr screen) <- copy _screen
  # repl-col
  var _repl-col/eax: (addr int) <- get env, code-separator-col
  var repl-col/ecx: int <- copy *_repl-col
  repl-col <- add 2  # repl-margin-left
  # functions
  var functions/edx: (addr handle function) <- get env, functions
  # sandbox
  var sandbox-ah/eax: (addr handle sandbox) <- get env, sandboxes
  var sandbox/eax: (addr sandbox) <- lookup *sandbox-ah
#?   {
#?     var line-ah/eax: (addr handle line) <- get sandbox, data
#?     var line/eax: (addr line) <- lookup *line-ah
#?     var first-word-ah/eax: (addr handle word) <- get line, data
#?     var curr-word/eax: (addr word) <- lookup *first-word-ah
#?     print-word 0, curr-word
#?     print-string 0, "\n"
#?   }
  # bindings
  var bindings-storage: table
  var bindings/ebx: (addr table) <- address bindings-storage
  initialize-table bindings, 0x10
  render-sandbox screen, functions, bindings, sandbox, 3, repl-col
}

fn render-sandbox screen: (addr screen), functions: (addr handle function), bindings: (addr table), _sandbox: (addr sandbox), top-row: int, left-col: int {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  # line
  var curr-line-ah/eax: (addr handle line) <- get sandbox, data
  var _curr-line/eax: (addr line) <- lookup *curr-line-ah
  var curr-line/ecx: (addr line) <- copy _curr-line
  #
  var curr-row/edx: int <- copy top-row
  # cursor row, col
  var cursor-row: int
  var cursor-row-addr: (addr int)
  var tmp/eax: (addr int) <- address cursor-row
  copy-to cursor-row-addr, tmp
  var cursor-col: int
  var cursor-col-addr: (addr int)
  tmp <- address cursor-col
  copy-to cursor-col-addr, tmp
  # render all but final line without stack
#?   print-string 0, "render all but final line\n"
  {
    var next-line-ah/eax: (addr handle line) <- get curr-line, next
    var next-line/eax: (addr line) <- lookup *next-line-ah
    compare next-line, 0
    break-if-=
    {
      var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
      var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
      var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
      var cursor-word/eax: (addr word) <- lookup *cursor-word-ah
#?       print-string 0, "cursor 2: "
#?       {
#?         print-word 0, cursor-word
#?         print-string 0, " -- "
#?         var foo/eax: int <- copy cursor-word
#?         print-int32-hex 0, foo
#?         print-string 0, "\n"
#?       }
      # 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
      render-line-without-stack screen, curr-line, curr-row, left-col, cursor-word, cursor-row-addr, cursor-col-addr
    }
    curr-line <- copy next-line
    curr-row <- add 2
    loop
  }
  #
#?   print-string 0, "render final line\n"
  render-final-line-with-stack screen, functions, bindings, sandbox, curr-row, left-col, cursor-row-addr, cursor-col-addr
  # at most one of the following dialogs will be rendered
  render-rename-dialog screen, sandbox, cursor-row, cursor-col
  render-define-dialog screen, sandbox, cursor-row, cursor-col
  move-cursor screen, cursor-row, cursor-col
}

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) {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  # expanded-words
  var expanded-words/edi: (addr handle call-path) <- get sandbox, expanded-words
  # cursor-word
  var cursor-call-path-ah/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
  var cursor-call-path/eax: (addr call-path-element) <- lookup *cursor-call-path-ah
  var cursor-word-ah/eax: (addr handle word) <- get cursor-call-path, word
  var _cursor-word/eax: (addr word) <- lookup *cursor-word-ah
  var cursor-word/ebx: (addr word) <- copy _cursor-word
#?   print-string 0, "word at cursor: "
#?   print-word 0, cursor-word
#?   print-string 0, "\n"
  # cursor-call-path
  var cursor-call-path: (addr handle call-path-element)
  {
    var src/eax: (addr handle call-path-element) <- get sandbox, cursor-call-path
    copy-to cursor-call-path, src
  }
  # first line
  var first-line-ah/eax: (addr handle line) <- get sandbox, data
  var _first-line/eax: (addr line) <- lookup *first-line-ah
  var first-line/edx: (addr line) <- copy _first-line
  # final line
  var final-line-storage: (handle line)
  var final-line-ah/eax: (addr handle line) <- address final-line-storage
  final-line sandbox, final-line-ah
  var final-line/eax: (addr line) <- lookup *final-line-ah
  # curr-path
  var curr-path-storage: (handle call-path-element)
  var curr-path/ecx: (addr handle call-path-element) <- address curr-path-storage
  allocate curr-path  # leak
  initialize-path-from-line final-line, curr-path
  #
  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
}

fn final-line _sandbox: (addr sandbox), out: (addr handle line) {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  var curr-line-ah/ecx: (addr handle line) <- get sandbox, data
  {
    var curr-line/eax: (addr line) <- lookup *curr-line-ah
    var next-line-ah/edx: (addr handle line) <- get curr-line, next
    var next-line/eax: (addr line) <- lookup *next-line-ah
    compare next-line, 0
    break-if-=
    curr-line-ah <- copy next-line-ah
    loop
  }
  copy-object curr-line-ah, out
}

fn render-rename-dialog screen: (addr screen), _sandbox: (addr sandbox), cursor-row: int, cursor-col: int {
  var sandbox/edi: (addr sandbox) <- copy _sandbox
  var rename-word-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-cursor-word
  var rename-word-mode?/eax: (addr word) <- lookup *rename-word-mode-ah?
  compare rename-word-mode?, 0
  break-if-=
  # clear a space for the dialog
  var top-row/eax: int <- copy cursor-row
  top-row <- subtract 3
  var bottom-row/ecx: int <- copy cursor-row
  bottom-row <- add 3
  var left-col/edx: int <- copy cursor-col
  left-col <- subtract 0x10
  var right-col/ebx: int <- copy cursor-col
  right-col <- add 0x10
  clear-rect screen, top-row, left-col, bottom-row, right-col
  draw-box screen, top-row, left-col, bottom-row, right-col
  # render a little menu for the dialog
  var menu-row/ecx: int <- copy bottom-row
  menu-row <- decrement
  var menu-col/edx: int <- copy left-col
  menu-col <- add 2
  move-cursor screen, menu-row, menu-col
  start-reverse-video screen
  print-string screen, " esc "
  reset-formatting screen
  print-string screen, " cancel  "
  start-reverse-video screen
  print-string screen, " enter "
  reset-formatting screen
  print-string screen, " rename  "
  # draw the word, positioned appropriately around the cursor
  var start-col/ecx: int <- copy cursor-col
  var word-ah?/edx: (addr handle word) <- get sandbox, partial-name-for-cursor-word
  var word/eax: (addr word) <- lookup *word-ah?
  var cursor-index/eax: int <- cursor-index word
  start-col <- subtract cursor-index
  move-cursor screen, cursor-row, start-col
  var word/eax: (addr word) <- lookup *word-ah?
  print-word screen, word
}

fn render-define-dialog screen: (addr screen), _sandbox: (addr sandbox), cursor-row: int, cursor-col: int {
  var sandbox/edi: (addr sandbox) <- copy _sandbox
  var define-function-mode-ah?/ecx: (addr handle word) <- get sandbox, partial-name-for-function
  var define-function-mode?/eax: (addr word) <- lookup *define-function-mode-ah?
  compare define-function-mode?, 0
  break-if-=
  # clear a space for the dialog
  var top-row/eax: int <- copy cursor-row
  top-row <- subtract 3
  var bottom-row/ecx: int <- copy cursor-row
  bottom-row <- add 3
  var left-col/edx: int <- copy cursor-col
  left-col <- subtract 0x10
  var right-col/ebx: int <- copy cursor-col
  right-col <- add 0x10
  clear-rect screen, top-row, left-col, bottom-row, right-col
  draw-box screen, top-row, left-col, bottom-row, right-col
  # render a little menu for the dialog
  var menu-row/ecx: int <- copy bottom-row
  menu-row <- decrement
  var menu-col/edx: int <- copy left-col
  menu-col <- add 2
  move-cursor screen, menu-row, menu-col
  start-reverse-video screen
  print-string screen, " esc "
  reset-formatting screen
  print-string screen, " cancel  "
  start-reverse-video screen
  print-string screen, " enter "
  reset-formatting screen
  print-string screen, " define  "
  # draw the word, positioned appropriately around the cursor
  var start-col/ecx: int <- copy cursor-col
  var word-ah?/edx: (addr handle word) <- get sandbox, partial-name-for-function
  var word/eax: (addr word) <- lookup *word-ah?
  var cursor-index/eax: int <- cursor-index word
  start-col <- subtract cursor-index
  move-cursor screen, cursor-row, start-col
  var word/eax: (addr word) <- lookup *word-ah?
  print-word screen, word
}

# Render just the words in 'line'.
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) {
  # curr-word
  var line/eax: (addr line) <- copy _line
  var first-word-ah/eax: (addr handle word) <- get line, data
  var _curr-word/eax: (addr word) <- lookup *first-word-ah
  var curr-word/esi: (addr word) <- copy _curr-word
  #
  # loop-carried dependency
  var curr-col/ecx: int <- copy left-col
  #
  {
    compare curr-word, 0
    break-if-=
#?     print-string 0, "-- word in penultimate lines: "
#?     {
#?       var foo/eax: int <- copy curr-word
#?       print-int32-hex 0, foo
#?     }
#?     print-string 0, "\n"
    var old-col/edx: int <- copy curr-col
    reset-formatting screen
    move-cursor screen, curr-row, curr-col
    print-word screen, curr-word
    {
      var max-width/eax: int <- word-length curr-word
      curr-col <- add max-width
      curr-col <- add 1  # margin-right
    }
    # cache cursor column if necessary
    {
      compare curr-word, cursor-word
      break-if-!=
#?       print-string 0, "Cursor at "
#?       print-int32-decimal 0, curr-row
#?       print-string 0, ", "
#?       print-int32-decimal 0, old-col
#?       print-string 0, "\n"
#?       print-string 0, "contents: "
#?       print-word 0, cursor-word
#?       print-string 0, "\n"
#?       {
#?         var foo/eax: int <- copy cursor-word
#?         print-int32-hex 0, foo
#?         print-string 0, "\n"
#?       }
      var dest/ecx: (addr int) <- copy cursor-row-addr
      var src/eax: int <- copy curr-row
      copy-to *dest, src
      dest <- copy cursor-col-addr
      copy-to *dest, old-col
      var cursor-index-in-word/eax: int <- cursor-index curr-word
      add-to *dest, cursor-index-in-word
    }
    # loop update
    var next-word-ah/edx: (addr handle word) <- get curr-word, next
    var _curr-word/eax: (addr word) <- lookup *next-word-ah
    curr-word <- copy _curr-word
    loop
  }
}

fn call-depth-at-cursor _sandbox: (addr sandbox) -> _/eax: int {
  var sandbox/esi: (addr sandbox) <- copy _sandbox
  var cursor-call-path/edi: (addr handle call-path-element) <- get sandbox, cursor-call-path
  var result/eax: int <- call-path-element-length cursor-call-path
  result <- add 2  # input-row - 1
  return result
}

fn call-path-element-length _x: (addr handle call-path-element) -> _/eax: int {
  var curr-ah/ecx: (addr handle call-path-element) <- copy _x
  var result/edi: int <- copy 0
  {
    var curr/eax: (addr call-path-element) <- lookup *curr-ah
    compare curr, 0
    break-if-=
    curr-ah <- get curr, next
    result <- increment
    loop
  }
  return result
}

# Render the line of words in line, along with the state of the stack under each word.
# Also render any expanded function calls using recursive calls.
#
# Along the way, compute the column the cursor should be positioned at (cursor-col-addr).
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 {
#?   print-string 0, "--\n"
  # curr-word
  var line/esi: (addr line) <- copy _line
  var first-word-ah/eax: (addr handle word) <- get line, data
  var curr-word/eax: (addr word) <- lookup *first-word-ah
  var debug-row: int
  copy-to debug-row, 0x20
  #
  # loop-carried dependency
  var curr-col/ecx: int <- copy left-col
  #
  {
    compare curr-word, 0
    break-if-=
#?     print-string 0, "-- word in final line: "
#?     {
#?       var foo/eax: int <- copy curr-word
#?       print-int32-hex 0, foo
#?     }
#?     print-string 0, "\n"
    # if necessary, first render columns for subsidiary stack
    $render-line:subsidiary: {
      {
#?         print-string 0, "check sub\n"
        var display-subsidiary-stack?/eax: boolean <- find-in-call-paths expanded-words, curr-path
        compare display-subsidiary-stack?, 0  # false
        break-if-= $render-line:subsidiary
      }
#?       print-string 0, "render subsidiary stack\n"
      # does function exist?
      var callee/edi: (addr function) <- copy 0
      {
        var callee-h: (handle function)
        var callee-ah/ecx: (addr handle function) <- address callee-h
        callee functions, curr-word, callee-ah
        var _callee/eax: (addr function) <- lookup *callee-ah
        callee <- copy _callee
        compare callee, 0
        break-if-= $render-line:subsidiary
      }
      move-cursor screen, top-row, curr-col
      start-color screen, 8, 7
      print-word screen, curr-word
      {
        var word-len/eax: int <- word-length curr-word
        curr-col <- add word-len
        curr-col <- add 2
        increment top-row
      }
      # obtain stack at call site
      var stack-storage: value-stack
      var stack/edx: (addr value-stack) <- address stack-storage
      initialize-value-stack stack, 0x10
      {
        var prev-word-ah/eax: (addr handle word) <- get curr-word, prev
        var prev-word/eax: (addr word) <- lookup *prev-word-ah
        compare prev-word, 0
        break-if-=
        evaluate functions, bindings, line, prev-word, stack
      }
      # construct new bindings
      var callee-bindings-storage: table
      var callee-bindings/esi: (addr table) <- address callee-bindings-storage
      initialize-table callee-bindings, 0x10
      bind-args callee, stack, callee-bindings
      # obtain body
      var callee-body-ah/eax: (addr handle line) <- get callee, body
      var callee-body/eax: (addr line) <- lookup *callee-body-ah
      var callee-body-first-word/edx: (addr handle word) <- get callee-body, data
      # - render subsidiary stack
      push-to-call-path-element curr-path, callee-body-first-word  # leak
      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
      drop-from-call-path-element curr-path
      #
      move-cursor screen, top-row, curr-col
      print-code-point screen, 0x21d7  #       #
      curr-col <- add 2
      decrement top-row
    }
    # render main column
    var old-col/edx: int <- copy curr-col
#?     move-cursor 0, debug-row, 1
#?     increment debug-row
#?     print-string 0, "rendering column from "
#?     print-int32-decimal 0, curr-col
#?     print-string 0, "\n"
    curr-col <- render-column screen, functions, bindings, first-line, line, curr-word, top-row, curr-col
    # cache cursor column if necessary
    $render-line:cache-cursor-column: {
#?       print-string 0, "cache cursor? "
#?       {
#?         var foo/eax: int <- copy curr-word
#?         print-int32-hex 0, foo
#?       }
#?       print-string 0, "\n"
      {
        var found?/eax: boolean <- call-path-element-match? curr-path, cursor-call-path
        compare found?, 0  # false
        break-if-= $render-line:cache-cursor-column
      }
#?       print-string 0, "cursor at "
#?       print-int32-decimal 0, top-row
#?       print-string 0, ", "
#?       print-int32-decimal 0, old-col
#?       print-string 0, "\n"
      var dest/edi: (addr int) <- copy cursor-row-addr
      {
        var src/eax: int <- copy top-row
        copy-to *dest, src
      }
      dest <- copy cursor-col-addr
      copy-to *dest, old-col
      var cursor-index-in-word/eax: int <- cursor-index curr-word
      add-to *dest, cursor-index-in-word
    }
    # loop update
#?     print-string 0, "next word\n"
    var next-word-ah/edx: (addr handle word) <- get curr-word, next
    curr-word <- lookup *next-word-ah
#?     {
#?       var foo/eax: int <- copy curr-word
#?       print-int32-hex 0, foo
#?       print-string 0, "\n"
#?     }
    increment-final-element curr-path
    loop
  }
  return curr-col
}

fn callee functions: (addr handle function), word: (addr word), out: (addr handle function) {
  var stream-storage: (stream byte 0x10)
  var stream/esi: (addr stream byte) <- address stream-storage
  emit-word word, stream
  find-function functions, stream, out
}

# Render:
#   - starting at top-row, left-col: final-word
#   - starting somewhere below at left-col: the stack result from interpreting first-world to final-word (inclusive)
#
# Return the farthest column written.
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 {
#?   print-string 0, "render-column\n"
  var max-width/esi: int <- copy 0
  {
    # indent stack
    var indented-col/ebx: int <- copy left-col
    indented-col <- add 1  # margin-right
    # compute stack
    var stack: value-stack
    var stack-addr/edi: (addr value-stack) <- address stack
    initialize-value-stack stack-addr, 0x10  # max-words
    evaluate functions, bindings, first-line, final-word, stack-addr
    # render stack
    var curr-row/edx: int <- copy top-row
    curr-row <- add 2  # stack-margin-top
    var _max-width/eax: int <- value-stack-max-width stack-addr
    max-width <- copy _max-width
    {
      var top-addr/ecx: (addr int) <- get stack-addr, top
      compare *top-addr, 0
      break-if-<=
      decrement *top-addr
      move-cursor screen, curr-row, indented-col
      {
        var data-ah/eax: (addr handle array value) <- get stack-addr, data
        var data/eax: (addr array value) <- lookup *data-ah
        var top/edx: int <- copy *top-addr
        var dest-offset/edx: (offset value) <- compute-offset data, top
        var val/eax: (addr value) <- index data, dest-offset
        render-value screen, val, max-width
      }
      curr-row <- increment
      loop
    }
  }

  max-width <- add 2  # spaces on either side of items on the stack

  # render word, initialize result
  reset-formatting screen
  move-cursor screen, top-row, left-col
  print-word screen, final-word
  {
    var size/eax: int <- word-length final-word
    compare size, max-width
    break-if-<=
    max-width <- copy size
  }

  # post-process right-col
  var right-col/ecx: int <- copy left-col
  right-col <- add max-width
  right-col <- add 1  # margin-right
#?   print-int32-decimal 0, left-col
#?   print-string 0, " => "
#?   print-int32-decimal 0, right-col
#?   print-string 0, "\n"
  return right-col
}

fn render-value screen: (addr screen), _val: (addr value), max-width: int {
$render-value:body: {
  var val/esi: (addr value) <- copy _val
  var val-type/ecx: (addr int) <- get val, type
  # per-type rendering logic goes here
  compare *val-type, 1  # string
  {
    break-if-!=
    var val-ah/eax: (addr handle array byte) <- get val, text-data
    var val-string/eax: (addr array byte) <- lookup *val-ah
    compare val-string, 0
    break-if-=
    var orig-len/ecx: int <- length val-string
    var truncated: (handle array byte)
    var truncated-ah/esi: (addr handle array byte) <- address truncated
    substring val-string, 0, 0xc, truncated-ah
    var truncated-string/eax: (addr array byte) <- lookup *truncated-ah
#?     {
#?       var foo/eax: int <- copy truncated-string
#?       print-int32-hex 0, foo
#?       print-string 0, "\n"
#?     }
    var len/edx: int <- length truncated-string
    start-color screen, 0xf2, 7
    print-code-point screen, 0x275d  # open-quote
    print-string screen, truncated-string
    compare len, orig-len
    {
      break-if-=
      print-code-point screen, 0x2026  # ellipses
    }
    print-code-point screen, 0x275e  # close-quote
    reset-formatting screen
    break $render-value:body
  }
  compare *val-type, 2  # array
  {
    break-if-!=
    var val-ah/eax: (addr handle array value) <- get val, array-data
    var val-array/eax: (addr array value) <- lookup *val-ah
    render-array screen, val-array
    break $render-value:body
  }
  compare *val-type, 3  # file
  {
    break-if-!=
    var val-ah/eax: (addr handle buffered-file) <- get val, file-data
    var val-file/eax: (addr buffered-file) <- lookup *val-ah
    start-color screen, 0, 7
    # TODO
    print-string screen, " FILE "
    break $render-value:body
  }
  # render ints by default for now
  var val-int/eax: (addr int) <- get val, int-data
  render-integer screen, *val-int, max-width
}
}

# synaesthesia
fn render-integer screen: (addr screen), val: int, max-width: int {
$render-integer:body: {
  # if max-width is 0, we're inside an array. No coloring.
  compare max-width, 0
  {
    break-if-!=
    print-int32-decimal screen, val
    break $render-integer:body
  }
  var bg/eax: int <- hash-color val
  var fg/ecx: int <- copy 7
  {
    compare bg, 2
    break-if-!=
    fg <- copy 0
  }
  {
    compare bg, 3
    break-if-!=
    fg <- copy 0
  }
  {
    compare bg, 6
    break-if-!=
    fg <- copy 0
  }
  start-color screen, fg, bg
  print-grapheme screen, 0x20  # space
  print-int32-decimal-right-justified screen, val, max-width
  print-grapheme screen, 0x20  # space
}
}

fn render-array screen: (addr screen), _a: (addr array value) {
  start-color screen, 0xf2, 7
  # don't surround in spaces
  print-grapheme screen, 0x5b  # '['
  var a/esi: (addr array value) <- copy _a
  var max/ecx: int <- length a
  var i/eax: int <- copy 0
  {
    compare i, max
    break-if->=
    {
      compare i, 0
      break-if-=
      print-string screen, " "
    }
    var off/ecx: (offset value) <- compute-offset a, i
    var x/ecx: (addr value) <- index a, off
    render-value screen, x, 0
    i <- increment
    loop
  }
  print-grapheme screen, 0x5d  # ']'
}

fn hash-color val: int -> _/eax: int {
  var result/eax: int <- try-modulo val, 7  # assumes that 7 is always the background color
  return result
}

fn clear-canvas _env: (addr environment) {
  var env/esi: (addr environment) <- copy _env
  var screen-ah/edi: (addr handle screen) <- get env, screen
  var _screen/eax: (addr screen) <- lookup *screen-ah
  var screen/edi: (addr screen) <- copy _screen
  clear-screen screen
  var nrows/eax: (addr int) <- get env, nrows
  var _repl-col/ecx: (addr int) <- get env, code-separator-col
  var repl-col/ecx: int <- copy *_repl-col
  draw-vertical-line screen, 1, *nrows, repl-col
  # wordstar-style cheatsheet of shortcuts
  move-cursor screen, *nrows, 0
  start-reverse-video screen
  print-string screen, " ctrl-q "
  reset-formatting screen
  print-string screen, " quit "
  var menu-start/ebx: int <- copy repl-col
  menu-start <- subtract 0x40  # 64 = half the size of the menu
  move-cursor screen, *nrows, menu-start
  start-reverse-video screen
  print-string screen, " ctrl-a "
  reset-formatting screen
  print-string screen, " ⏮   "
  start-reverse-video screen
  print-string screen, " ctrl-b "
  reset-formatting screen
  print-string screen, " ◀ word  "
  start-reverse-video screen
  print-string screen, " ctrl-f "
  reset-formatting screen
  print-string screen, " word ▶  "
  start-reverse-video screen
  print-string screen, " ctrl-e "
  reset-formatting screen
  print-string screen, " ⏭   "
  start-reverse-video screen
  print-string screen, " ctrl-u "
  reset-formatting screen
  print-string screen, " clear line  "
  start-reverse-video screen
  print-string screen, " ctrl-n "
  reset-formatting screen
  print-string screen, " name value  "
  start-reverse-video screen
  print-string screen, " ctrl-d "
  reset-formatting screen
  print-string screen, " define function  "
  # primitives
  var start-col/ecx: int <- copy repl-col
  start-col <- subtract 0x20
  move-cursor screen, 1, start-col
  print-string screen, "primitives:"
  start-col <- add 2
  move-cursor screen, 2, start-col
  print-string screen, "+ - * len"
  move-cursor screen, 3, start-col
  print-string screen, "open read slurp lines"
  move-cursor screen, 4, start-col
  print-string screen, "dup swap"
  # currently defined functions
  start-col <- subtract 2
  move-cursor screen, 6, start-col
  print-string screen, "functions:"
  start-col <- add 2
  var row/ebx: int <- copy 7
  var functions/esi: (addr handle function) <- get env, functions
  {
    var curr/eax: (addr function) <- lookup *functions
    compare curr, 0
    break-if-=
    row <- render-function screen, row, start-col, curr
    functions <- get curr, next
    row <- increment
    loop
  }
}

# only single-line functions supported for now
fn render-function screen: (addr screen), row: int, col: int, _f: (addr function) -> _/ebx: int {
  var f/esi: (addr function) <- copy _f
  var args/ecx: (addr handle word) <- get f, args
  move-cursor screen, row, col
  print-words-in-reverse screen, args
  var name-ah/eax: (addr handle array byte) <- get f, name
  var name/eax: (addr array byte) <- lookup *name-ah
  start-bold screen
  print-string screen, name
  reset-formatting screen
  increment row
  add-to col, 2
  move-cursor screen, row, col
  print-string screen, "= "
  var body-ah/eax: (addr handle line) <- get f, body
  var body/eax: (addr line) <- lookup *body-ah
  var body-words-ah/eax: (addr handle word) <- get body, data
  print-words screen, body-words-ah
  return row
}

fn real-grapheme? g: grapheme -> _/eax: boolean {
  # if g == newline return true
  compare g, 0xa
  {
    break-if-!=
    return 1  # true
  }
  # if g == tab return true
  compare g, 9
  {
    break-if-!=
    return 1  # true
  }
  # if g < 32 return false
  compare g, 0x20
  {
    break-if->=
    return 0  # false
  }
  # if g <= 255 return true
  compare g, 0xff
  {
    break-if->
    return 1  # true
  }
  # if (g&0xff == Esc) it's an escape sequence
  and-with g, 0xff
  compare g, 0x1b  # Esc
  {
    break-if-!=
    return 0  # false
  }
  # otherwise return true
  return 1  # true
}