about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--103grapheme.subx8
-rw-r--r--400.mu6
-rw-r--r--500text-screen.mu4
-rw-r--r--501draw-text.mu2
-rw-r--r--502test.mu6
-rw-r--r--504test-screen.mu14
-rw-r--r--ex7.mu2
-rw-r--r--linux/401test.mu4
-rw-r--r--linux/404stream.mu16
-rw-r--r--linux/412print-float-decimal.mu2
-rw-r--r--linux/README.md4
-rw-r--r--linux/vocabulary.md368
-rw-r--r--rpn.mu2
-rw-r--r--vocabulary.md335
14 files changed, 578 insertions, 195 deletions
diff --git a/103grapheme.subx b/103grapheme.subx
index d26a0b58..65c5bb2e 100644
--- a/103grapheme.subx
+++ b/103grapheme.subx
@@ -125,13 +125,13 @@ $set-cursor-position-on-real-screen:end:
     5d/pop-to-ebp
     c3/return
 
-# Draw cursor at current location. But this is rickety:
+# Not a real `show-cursor` primitive:
 #   - does not clear previous location cursor was shown at.
 #   - does not preserve what was at the cursor. Caller is responsible for
 #     tracking what was on the screen at this position before and passing it
 #     in again.
 #   - does not stop showing the cursor at this location when the cursor moves
-show-cursor-on-real-screen:  # g: grapheme
+draw-cursor-on-real-screen:  # g: grapheme
     # . prologue
     55/push-ebp
     89/<- %ebp 4/r32/esp
@@ -141,7 +141,7 @@ show-cursor-on-real-screen:  # g: grapheme
     #
     (cursor-position-on-real-screen)  # => eax, ecx
     (draw-grapheme-on-real-screen *(ebp+8) %eax %ecx 0 7)
-$show-cursor-on-real-screen:end:
+$draw-cursor-on-real-screen:end:
     # . restore registers
     59/pop-to-ecx
     58/pop-to-eax
@@ -156,7 +156,7 @@ $show-cursor-on-real-screen:end:
 # 'draw*cursor*') print to by default.
 #
 # We don't bother displaying the cursor when drawing. It only becomes visible
-# on show-cursor, which is quite rickety (see above)
+# on draw-cursor, which is quite rickety (see above)
 #
 # It's up to applications to manage cursor display:
 #   - clean up where it used to be
diff --git a/400.mu b/400.mu
index 44d94213..c60f17ca 100644
--- a/400.mu
+++ b/400.mu
@@ -3,7 +3,7 @@ sig pixel-on-real-screen x: int, y: int, color: int
 sig draw-grapheme-on-real-screen g: grapheme, x: int, y: int, color: int, background-color: int
 sig cursor-position-on-real-screen -> _/eax: int, _/ecx: int
 sig set-cursor-position-on-real-screen x: int, y: int
-sig show-cursor-on-real-screen g: grapheme
+sig draw-cursor-on-real-screen g: grapheme
 
 # keyboard
 sig read-key kbd: (addr keyboard) -> _/eax: byte
@@ -26,9 +26,9 @@ sig check-next-stream-line-equal f: (addr stream byte), s: (addr array byte), ms
 sig write f: (addr stream byte), s: (addr array byte)
 sig write-stream f: (addr stream byte), s: (addr stream byte)
 sig read-byte s: (addr stream byte) -> _/eax: byte
-sig append-byte f: (addr stream byte), n: int
+sig append-byte f: (addr stream byte), n: int  # really just a byte, but I want to pass in literal numbers
 #sig to-hex-char in/eax: int -> out/eax: int
-sig append-byte-hex f: (addr stream byte), n: int
+sig append-byte-hex f: (addr stream byte), n: int  # really just a byte, but I want to pass in literal numbers
 sig write-int32-hex f: (addr stream byte), n: int
 sig write-int32-hex-bits f: (addr stream byte), n: int, bits: int
 sig hex-int? in: (addr slice) -> _/eax: boolean
diff --git a/500text-screen.mu b/500text-screen.mu
index a764aa06..0fd7ae9d 100644
--- a/500text-screen.mu
+++ b/500text-screen.mu
@@ -177,11 +177,11 @@ fn set-cursor-position screen: (addr screen), x: int, y: int {
   copy-to *dest, src
 }
 
-fn show-cursor screen: (addr screen), g: grapheme {
+fn draw-cursor screen: (addr screen), g: grapheme {
   {
     compare screen, 0
     break-if-!=
-    show-cursor-on-real-screen g
+    draw-cursor-on-real-screen g
     return
   }
   # fake screen
diff --git a/501draw-text.mu b/501draw-text.mu
index 9b207361..ce87634e 100644
--- a/501draw-text.mu
+++ b/501draw-text.mu
@@ -62,7 +62,7 @@ fn move-cursor-down screen: (addr screen) {
   set-cursor-position screen, cursor-x, cursor-y
 }
 
-fn move-cursor-to-start-of-next-line screen: (addr screen) {
+fn move-cursor-to-left-margin-of-next-line screen: (addr screen) {
   var dummy/eax: int <- copy 0
   var _height/ecx: int <- copy 0
   dummy, _height <- screen-size screen
diff --git a/502test.mu b/502test.mu
index 00d55d34..058e594f 100644
--- a/502test.mu
+++ b/502test.mu
@@ -8,7 +8,7 @@ fn check-ints-equal _a: int, b: int, msg: (addr array byte) {
     return
   }
   draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, msg, 3/fg/cyan, 0/bg
-  move-cursor-to-start-of-next-line 0/screen
+  move-cursor-to-left-margin-of-next-line 0/screen
   count-test-failure
 }
 
@@ -25,7 +25,7 @@ fn check _a: boolean, msg: (addr array byte) {
     return
   }
   draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, msg, 3/fg/cyan, 0/bg
-  move-cursor-to-start-of-next-line 0/screen
+  move-cursor-to-left-margin-of-next-line 0/screen
   count-test-failure
 }
 
@@ -38,6 +38,6 @@ fn check-not _a: boolean, msg: (addr array byte) {
     return
   }
   draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, msg, 3/fg/cyan, 0/bg
-  move-cursor-to-start-of-next-line 0/screen
+  move-cursor-to-left-margin-of-next-line 0/screen
   count-test-failure
 }
diff --git a/504test-screen.mu b/504test-screen.mu
index a409443b..48550f09 100644
--- a/504test-screen.mu
+++ b/504test-screen.mu
@@ -52,7 +52,7 @@ fn check-screen-row-from screen-on-stack: (addr screen), x: int, y: int, expecte
       draw-grapheme-at-cursor 0/screen, g, 3/cyan, 0/bg
       move-cursor-rightward-and-downward 0/screen, 0/xmin, 0x80/xmax=screen-width
       draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "'", 3/fg/cyan, 0/bg
-      move-cursor-to-start-of-next-line 0/screen
+      move-cursor-to-left-margin-of-next-line 0/screen
     }
     idx <- increment
     increment x
@@ -120,7 +120,7 @@ fn check-screen-row-in-color-from screen-on-stack: (addr screen), fg: int, y: in
         draw-grapheme-at-cursor 0/screen, g, 3/cyan, 0/bg
         move-cursor-rightward-and-downward 0/screen, 0/xmin, 0x80/xmax=screen-width
         draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "'", 3/fg/cyan, 0/bg
-        move-cursor-to-start-of-next-line 0/screen
+        move-cursor-to-left-margin-of-next-line 0/screen
       }
       $check-screen-row-in-color-from:compare-colors: {
         var color/eax: int <- screen-color-at-idx screen, idx
@@ -144,7 +144,7 @@ fn check-screen-row-in-color-from screen-on-stack: (addr screen), fg: int, y: in
         draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, fg, 3/fg/cyan, 0/bg
         draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, " but observed color ", 3/fg/cyan, 0/bg
         draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, color, 3/fg/cyan, 0/bg
-        move-cursor-to-start-of-next-line 0/screen
+        move-cursor-to-left-margin-of-next-line 0/screen
       }
     }
     idx <- increment
@@ -211,7 +211,7 @@ fn check-screen-row-in-background-color-from screen-on-stack: (addr screen), bg:
         draw-grapheme-at-cursor 0/screen, g, 3/cyan, 0/bg
         move-cursor-rightward-and-downward 0/screen, 0/xmin, 0x80/xmax=screen-width
         draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, "'", 3/fg/cyan, 0/bg
-        move-cursor-to-start-of-next-line 0/screen
+        move-cursor-to-left-margin-of-next-line 0/screen
         break $check-screen-row-in-background-color-from:compare-graphemes
       }
       $check-screen-row-in-background-color-from:compare-background-colors: {
@@ -236,7 +236,7 @@ fn check-screen-row-in-background-color-from screen-on-stack: (addr screen), bg:
         draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, bg, 3/fg/cyan, 0/bg
         draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, " but observed background-color ", 3/fg/cyan, 0/bg
         draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, background-color, 3/fg/cyan, 0/bg
-        move-cursor-to-start-of-next-line 0/screen
+        move-cursor-to-left-margin-of-next-line 0/screen
       }
     }
     idx <- increment
@@ -281,7 +281,7 @@ fn check-background-color-in-screen-row-from screen-on-stack: (addr screen), bg:
         draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, y, 3/fg/cyan, 0/bg
         draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, ") to not be in background-color ", 3/fg/cyan, 0/bg
         draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, bg, 3/fg/cyan, 0/bg
-        move-cursor-to-start-of-next-line 0/screen
+        move-cursor-to-left-margin-of-next-line 0/screen
         break $check-background-color-in-screen-row-from:compare-cells
       }
       # otherwise assert that background IS bg
@@ -297,7 +297,7 @@ fn check-background-color-in-screen-row-from screen-on-stack: (addr screen), bg:
       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, bg, 3/fg/cyan, 0/bg
       draw-text-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, " but observed background-color ", 3/fg/cyan, 0/bg
       draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, background-color, 3/fg/cyan, 0/bg
-      move-cursor-to-start-of-next-line 0/screen
+      move-cursor-to-left-margin-of-next-line 0/screen
     }
     idx <- increment
     increment x
diff --git a/ex7.mu b/ex7.mu
index 788ff121..63ac5e51 100644
--- a/ex7.mu
+++ b/ex7.mu
@@ -14,7 +14,7 @@ fn main {
   var space/eax: grapheme <- copy 0x20
   set-cursor-position 0/screen, 0, 0
   {
-    show-cursor 0/screen, space
+    draw-cursor 0/screen, space
     var key/eax: byte <- read-key 0/keyboard
     {
       compare key, 0x68/h
diff --git a/linux/401test.mu b/linux/401test.mu
index 65b765df..03acabf1 100644
--- a/linux/401test.mu
+++ b/linux/401test.mu
@@ -1,11 +1,11 @@
 # Some helpers for Mu tests.
 
-fn check-true val: boolean, msg: (addr array byte) {
+fn check val: boolean, msg: (addr array byte) {
   var tmp/eax: int <- copy val
   check-ints-equal tmp, 1, msg
 }
 
-fn check-false val: boolean, msg: (addr array byte) {
+fn check-not val: boolean, msg: (addr array byte) {
   var tmp/eax: int <- copy val
   check-ints-equal tmp, 0, msg
 }
diff --git a/linux/404stream.mu b/linux/404stream.mu
index 19bbb6e5..cf852426 100644
--- a/linux/404stream.mu
+++ b/linux/404stream.mu
@@ -6,18 +6,18 @@ fn test-stream {
   var s: (stream int 4)
   var s2/ecx: (addr stream int) <- address s
   var tmp/eax: boolean <- stream-empty? s2
-  check-true tmp, "F - test-stream/empty?/0"
+  check tmp, "F - test-stream/empty?/0"
   tmp <- stream-full? s2
-  check-false tmp, "F - test-stream/full?/0"
+  check-not tmp, "F - test-stream/full?/0"
   # step 2: write to stream
   var x: int
   copy-to x, 0x34
   var x2/edx: (addr int) <- address x
   write-to-stream s2, x2
   tmp <- stream-empty? s2
-  check-false tmp, "F - test-stream/empty?/1"
+  check-not tmp, "F - test-stream/empty?/1"
   tmp <- stream-full? s2
-  check-false tmp, "F - test-stream/full?/1"
+  check-not tmp, "F - test-stream/full?/1"
   # step 3: modify the value written (should make no difference)
   copy-to x, 0
   # step 4: read back
@@ -25,9 +25,9 @@ fn test-stream {
   var y2/ebx: (addr int) <- address y
   read-from-stream s2, y2
   tmp <- stream-empty? s2
-  check-true tmp, "F - test-stream/empty?/2"
+  check tmp, "F - test-stream/empty?/2"
   tmp <- stream-full? s2
-  check-false tmp, "F - test-stream/full?/2"
+  check-not tmp, "F - test-stream/full?/2"
   # we read back what was written
   check-ints-equal y, 0x34, "F - test-stream"
 }
@@ -37,12 +37,12 @@ fn test-stream-full {
   var s: (stream int 1)
   var s2/ecx: (addr stream int) <- address s
   var tmp/eax: boolean <- stream-full? s2
-  check-false tmp, "F - test-stream-full?/pre"
+  check-not tmp, "F - test-stream-full?/pre"
   var x: int
   var x2/edx: (addr int) <- address x
   write-to-stream s2, x2
   tmp <- stream-full? s2
-  check-true tmp, "F - test-stream-full?"
+  check tmp, "F - test-stream-full?"
 }
 
 fn test-fake-input-buffered-file {
diff --git a/linux/412print-float-decimal.mu b/linux/412print-float-decimal.mu
index a9c7fd0e..c852c932 100644
--- a/linux/412print-float-decimal.mu
+++ b/linux/412print-float-decimal.mu
@@ -620,7 +620,7 @@ fn check-buffer-contains _buf: (addr array byte), _contents: (addr array byte),
   var buf/esi: (addr array byte) <- copy _buf
   var contents/edi: (addr array byte) <- copy _contents
   var a/eax: boolean <- string-starts-with? buf, contents
-  check-true a, msg
+  check a, msg
   var len/ecx: int <- length contents
   var len2/eax: int <- length buf
   compare len, len2
diff --git a/linux/README.md b/linux/README.md
index ca003d28..a1fe2820 100644
--- a/linux/README.md
+++ b/linux/README.md
@@ -8,6 +8,10 @@ kernel. To run programs under this directory, you must first `cd` into it.
   Hello world!
   ```
 
+See the [shared vocabulary](vocabulary.md) of data types and functions shared
+by Mu programs running on Linux. Mu programs can transparently call low-level
+functions written in SubX.
+
 Some programs to try out:
 
 * `tile`: [An experimental live-updating postfix shell environment](https://mastodon.social/@akkartik/105108305362341204)
diff --git a/linux/vocabulary.md b/linux/vocabulary.md
new file mode 100644
index 00000000..81be5238
--- /dev/null
+++ b/linux/vocabulary.md
@@ -0,0 +1,368 @@
+## Reference documentation on available primitives
+
+### Data Structures
+
+- Handles: addresses to objects allocated on the heap. They're augmented with
+  book-keeping to guarantee memory-safety, and so cannot be stored in registers.
+  See [mu.md](mu.md) for details, but in brief:
+    - You need `addr` values to access data they point to.
+    - You can't store `addr` values in other types. They're temporary.
+    - You can store `handle` values in other types.
+    - To convert `handle` to `addr`, use `lookup`.
+    - Reclaiming memory (currently unimplemented) invalidates all `addr`
+      values.
+
+- Kernel strings: null-terminated regions of memory. Unsafe and to be avoided,
+  but needed for interacting with the kernel.
+
+- Arrays: size-prefixed regions of memory containing multiple elements of a
+  single type. Contents are preceded by 4 bytes (32 bits) containing the
+  `size` of the array in bytes.
+
+- Slices: a pair of 32-bit addresses denoting a [half-open](https://en.wikipedia.org/wiki/Interval_(mathematics))
+  \[`start`, `end`) interval to live memory with a consistent lifetime.
+
+  Invariant: `start` <= `end`
+
+- Streams: strings prefixed by 32-bit `write` and `read` indexes that the next
+  write or read goes to, respectively.
+
+  - offset 0: write index
+  - offset 4: read index
+  - offset 8: size of array (in bytes)
+  - offset 12: start of array data
+
+  Invariant: 0 <= `read` <= `write` <= `size`
+
+- File descriptors (fd): Low-level 32-bit integers that the kernel uses to
+  track files opened by the program.
+
+- File: 32-bit value containing either a fd or an address to a stream (fake
+  file).
+
+- Buffered files (buffered-file): Contain a file descriptor and a stream for
+  buffering reads/writes. Each `buffered-file` must exclusively perform either
+  reads or writes.
+
+- Graphemes: 32-bit fragments of utf-8 that encode a single Unicode code-point.
+- Code-points: 32-bit integers representing a Unicode character.
+
+### 'system calls'
+
+As I said at the top, a primary design goal of SubX (and Mu more broadly) is
+to explore ways to turn arbitrary manual tests into reproducible automated
+tests. SubX aims for this goal by baking testable interfaces deep into the
+stack, at the OS syscall level. The idea is that every syscall that interacts
+with hardware (and so the environment) should be *dependency injected* so that
+it's possible to insert fake hardware in tests.
+
+But those are big goals. Here are the syscalls I have so far:
+
+- `write`: takes two arguments, a file `f` and an address to array `s`.
+
+  Comparing this interface with the Unix `write()` syscall shows two benefits:
+
+  1. SubX can handle 'fake' file descriptors in tests.
+
+  1. `write()` accepts buffer and its size in separate arguments, which
+     requires callers to manage the two separately and so can be error-prone.
+     SubX's wrapper keeps the two together to increase the chances that we
+     never accidentally go out of array bounds.
+
+- `read`: takes two arguments, a file `f` and an address to stream `s`. Reads
+  as much data from `f` as can fit in (the free space of) `s`.
+
+  Like with `write()`, this wrapper around the Unix `read()` syscall adds the
+  ability to handle 'fake' file descriptors in tests, and reduces the chances
+  of clobbering outside array bounds.
+
+  One bit of weirdness here: in tests we do a redundant copy from one stream
+  to another. See [the comments before the implementation](http://akkartik.github.io/mu/html/060read.subx.html)
+  for a discussion of alternative interfaces.
+
+- `stop`: takes two arguments:
+  - `ed` is an address to an _exit descriptor_. Exit descriptors allow us to
+    `exit()` the program in production, but return to the test harness within
+    tests. That allows tests to make assertions about when `exit()` is called.
+  - `value` is the status code to `exit()` with.
+
+  For more details on exit descriptors and how to create one, see [the
+  comments before the implementation](http://akkartik.github.io/mu/html/059stop.subx.html).
+
+- `new-segment`
+
+  Allocates a whole new segment of memory for the program, discontiguous with
+  both existing code and data (heap) segments. Just a more opinionated form of
+  [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html).
+
+- `allocate`: takes two arguments, an address to allocation-descriptor `ad`
+  and an integer `n`
+
+  Allocates a contiguous range of memory that is guaranteed to be exclusively
+  available to the caller. Returns the starting address to the range in `eax`.
+
+  An allocation descriptor tracks allocated vs available addresses in some
+  contiguous range of memory. The int specifies the number of bytes to allocate.
+
+  Explicitly passing in an allocation descriptor allows for nested memory
+  management, where a sub-system gets a chunk of memory and further parcels it
+  out to individual allocations. Particularly helpful for (surprise) tests.
+
+- `time`: returns the time in seconds since the epoch.
+
+- `ntime`: returns the number of nanoseconds since some arbitrary point.
+  Saturates at 32 bits. Useful for fine-grained measurements over relatively
+  short durations.
+
+- `sleep`: sleep for some number of whole seconds and some fraction of a
+  second expressed in nanoseconds. Not having decimal literals can be awkward
+  here.
+
+- ... _(to be continued)_
+
+I will continue to import syscalls over time from [the old Mu VM in the parent
+directory](https://github.com/akkartik/mu), which has experimented with
+interfaces for the screen, keyboard, mouse, disk and network.
+
+### Functions
+
+The most useful functions from 400.mu and later .mu files. Look for definitions
+(using `ctags`) to see type signatures.
+
+_(Compound arguments are usually passed in by reference. Where the results are
+compound objects that don't fit in a register, the caller usually passes in
+allocated memory for it.)_
+
+#### assertions for tests
+
+- `check`: fails current test if given boolean is false (`= 0`).
+- `check-not`: fails current test if given boolean isn't false (`!= 0`).
+- `check-ints-equal`: fails current test if given ints aren't equal
+- `check-array-equal`: only arrays of ints, passes in a literal array in a
+  whitespace-separated string.
+- `check-stream-equal`: fails current test if stream doesn't match string
+- `check-next-stream-line-equal`: fails current test if next line of stream
+  until newline doesn't match string
+
+Every Mu computer has a global trace that programs can write to, and that
+tests can make assertions on.
+
+- `clear-trace-stream`
+- `check-trace-contains`
+- `check-trace-scans-to`: like `check-trace-contains` but with an implicit,
+  stateful start index
+
+#### error handling
+
+- `error`: takes three arguments, an exit-descriptor, a file and a string (message)
+
+  Prints out the message to the file and then exits using the provided
+  exit-descriptor.
+
+- `error-byte`: like `error` but takes an extra byte value that it prints out
+  at the end of the message.
+
+#### numbers
+
+- `abs`
+- `repeated-shift-left`, since x86 only supports bit-shifts by constant values
+- `repeated-shift-right`
+- `shift-left-bytes`: shift left by `n*8` bits
+- `integer-divide`
+
+Floating point constructors, since x86 doesn't support immediate floats and Mu
+doesn't yet parse floating-point literals:
+
+- `rational`: int, int -> float
+- `fill-in-rational`: int, int, (addr float)
+- `fill-in-sqrt`: int, (addr float)
+
+#### arrays and strings
+
+- `populate`: allocates space for `n` objects of the appropriate type.
+- `copy-array`: allocates enough space and writes out a copy of an array of
+  some type.
+- `slice-to-string`: allocates space for an array of bytes and copies the
+  slice into it.
+
+- `array-equal?`
+- `substring`: string, start, length -> string
+- `split-string`: string, delimiter -> array of strings
+
+- `copy-array-object`
+
+#### predicates
+
+- `kernel-string-equal?`: compares a kernel string with a string
+- `string-equal?`: compares two strings
+- `stream-data-equal?`: compares a stream with a string
+- `next-stream-line-equal?`: compares with string the next line in a stream, from
+  `read` index to newline
+
+- `slice-empty?`: checks if the `start` and `end` of a slice are equal
+- `slice-equal?`: compares a slice with a string
+- `slice-starts-with?`: compares the start of a slice with a string
+- `slice-ends-with?`: compares the end of a slice with a string
+
+#### writing to disk
+
+- `write`: string -> file
+  - Can also be used to cat a string into a stream.
+- `write-stream`: stream -> file
+  - Can also be used to cat one stream into another.
+- `write-stream-data`: stream -> file
+  - Like `write-stream` but ignores read index.
+- `write-slice`: slice -> stream
+- `append-byte`: int -> stream
+- `append-byte-hex`: int -> stream
+  - textual representation in hex, no '0x' prefix
+
+- `write-int`: int -> stream
+  - write number to stream
+- `write-int32-hex`: int -> stream
+  - textual representation in hex, including '0x' prefix
+- `write-int32-hex-buffered`: int -> buffered-file
+- `write-int32-decimal`
+- `write-int32-decimal-buffered`
+- `write-buffered`: string -> buffered-file
+- `write-slice-buffered`: slice -> buffered-file
+- `flush`: buffered-file
+- `write-byte-buffered`: int -> buffered-file
+- `write-byte-buffered`: int -> buffered-file
+  - textual representation in hex, no '0x' prefix
+- `print-int32-buffered`: int -> buffered-file
+  - textual representation in hex, including '0x' prefix
+
+- `write-grapheme`: grapheme -> stream
+- `to-grapheme`: code-point -> grapheme
+
+- `write-float-decimal-approximate`: float, precision: int -> stream
+
+- `new-buffered-file`
+- `populate-buffered-file-containing`: string -> buffered-file
+
+Unless otherwise states, writes to a stream will abort the entire program if
+there isn't enough room in the destination stream.
+
+#### reading from disk
+
+- `read`: file -> stream
+  - Can also be used to cat one stream into another.
+  - Will silently stop reading when destination runs out of space.
+- `read-byte-buffered`: buffered-file -> byte
+- `read-line-buffered`: buffered-file -> stream
+  - Will abort the entire program if there isn't enough room.
+
+- `read-grapheme`: stream -> grapheme
+- `read-grapheme-buffered`: buffered-file -> grapheme
+
+- `read-lines`: buffered-file -> array of strings
+
+#### non-IO operations on streams
+
+- `populate-stream`: allocates space in a stream for `n` objects of the
+  appropriate type.
+  - Will abort the entire program if `n*b` requires more than 32 bits.
+- `clear-stream`: resets everything in the stream to `0` (except its `size`).
+- `rewind-stream`: resets the read index of the stream to `0` without modifying
+  its contents.
+
+#### reading/writing hex representations of integers
+
+- `is-hex-int?`: slice -> boolean
+- `parse-hex-int`: string -> int
+- `parse-hex-int-from-slice`: slice -> int
+- `is-hex-digit?`: byte -> boolean
+
+- `parse-array-of-ints`
+- `parse-array-of-decimal-ints`
+
+#### printing to screen
+
+All screen primitives require a screen object, which can be either the real
+screen on the computer or a fake screen for tests. Mu supports a subset of
+Unix terminal properties supported by almost all modern terminal emulators.
+
+- `enable-screen-type-mode` (default)
+- `enable-screen-grid-mode`
+
+- `clear-screen`
+- `screen-size`
+
+- `move-cursor`
+- `hide-cursor`
+- `show-cursor`
+
+- `print-string`: string -> screen
+- `print-stream`
+- `print-grapheme`
+- `print-code-point`
+- `print-int32-hex`
+- `print-int32-decimal`
+- `print-int32-decimal-right-justified`
+- `print-array-of-ints-in-decimal`
+
+- `print-float-hex`
+- `print-float-decimal-approximate`: up to some precision
+
+Printing to screen is stateful, and preserves formatting unless explicitly
+manipulated.
+
+- `reset-formatting`
+- `start-color`: adjusts foreground and background
+- `start-bold`
+- `start-underline`
+- `start-reverse-video`
+- `start-blinking`
+
+Assertions for tests:
+
+- `screen-grapheme-at`
+- `screen-color-at`
+- `screen-background-color-at`
+- `screen-bold-at?`
+- `screen-underline-at?`
+- `screen-reverse-at?`
+- `screen-blink-at?`
+
+- `check-screen-row`
+- `check-screen-row-from`
+- `check-screen-row-in-color`
+- `check-screen-row-in-color-from`
+- `check-screen-row-in-background-color`
+- `check-screen-row-in-background-color-from`
+- `check-screen-row-in-bold`
+- `check-screen-row-in-bold-from`
+- `check-screen-row-in-underline`
+- `check-screen-row-in-underline-from`
+- `check-screen-row-in-reverse`
+- `check-screen-row-in-reverse-from`
+- `check-screen-row-in-blinking`
+- `check-screen-row-in-blinking-from`
+
+#### keyboard
+
+- `enable-keyboard-type-mode`: process keystrokes on `enter` (default mode)
+- `read-line-from-real-keyboard`
+
+- `enable-keyboard-immediate-mode`: process keystrokes as they're typed
+- `read-key-from-real-keyboard`
+
+#### tokenization
+
+from a stream:
+- `next-token`: stream, delimiter byte -> slice
+- `skip-chars-matching`: stream, delimiter byte
+- `skip-chars-not-matching`: stream, delimiter byte
+
+from a slice:
+- `next-token-from-slice`: start, end, delimiter byte -> slice
+  - Given a slice and a delimiter byte, returns a new slice inside the input
+    that ends at the delimiter byte.
+
+- `skip-chars-matching-in-slice`: curr, end, delimiter byte -> new-curr (in `eax`)
+- `skip-chars-not-matching-in-slice`:  curr, end, delimiter byte -> new-curr (in `eax`)
+
+#### file system
+
+- `open`: filename, write? -> buffered-file
diff --git a/rpn.mu b/rpn.mu
index 341f942d..f7a56bd1 100644
--- a/rpn.mu
+++ b/rpn.mu
@@ -28,7 +28,7 @@ fn main {
     # read line from keyboard
     clear-stream in
     {
-      show-cursor 0/screen, space
+      draw-cursor 0/screen, space
       var key/eax: byte <- read-key 0/keyboard
       compare key, 0xa/newline
       break-if-=
diff --git a/vocabulary.md b/vocabulary.md
index ce3eab23..9471d132 100644
--- a/vocabulary.md
+++ b/vocabulary.md
@@ -2,8 +2,15 @@
 
 ### Data Structures
 
-- Kernel strings: null-terminated regions of memory. Unsafe and to be avoided,
-  but needed for interacting with the kernel.
+- Handles: addresses to objects allocated on the heap. They're augmented with
+  book-keeping to guarantee memory-safety, and so cannot be stored in registers.
+  See [mu.md](mu.md) for details, but in brief:
+    - You need `addr` values to access data they point to.
+    - You can't store `addr` values in other types. They're temporary.
+    - You can store `handle` values in other types.
+    - To convert `handle` to `addr`, use `lookup`.
+    - Reclaiming memory (currently unimplemented) invalidates all `addr`
+      values.
 
 - Arrays: size-prefixed regions of memory containing multiple elements of a
   single type. Contents are preceded by 4 bytes (32 bits) containing the
@@ -24,185 +31,189 @@
 
   Invariant: 0 <= `read` <= `write` <= `size`
 
-- File descriptors (fd): Low-level 32-bit integers that the kernel uses to
-  track files opened by the program.
+  Writes to a stream abort if it's full. Reads to a stream abort if it's
+  empty.
 
-- File: 32-bit value containing either a fd or an address to a stream (fake
-  file).
+- Graphemes: 32-bit fragments of utf-8 that encode a single Unicode code-point.
+- Code-points: 32-bit integers representing a Unicode character.
 
-- Buffered files (buffered-file): Contain a file descriptor and a stream for
-  buffering reads/writes. Each `buffered-file` must exclusively perform either
-  reads or writes.
+### Functions
 
-### 'system calls'
+The most useful functions from 400.mu and later .mu files. Look for definitions
+(using `ctags`) to see type signatures.
 
-As I said at the top, a primary design goal of SubX (and Mu more broadly) is
-to explore ways to turn arbitrary manual tests into reproducible automated
-tests. SubX aims for this goal by baking testable interfaces deep into the
-stack, at the OS syscall level. The idea is that every syscall that interacts
-with hardware (and so the environment) should be *dependency injected* so that
-it's possible to insert fake hardware in tests.
+- `abort`: print a message in red on the bottom left of the screen and halt
 
-But those are big goals. Here are the syscalls I have so far:
-
-- `write`: takes two arguments, a file `f` and an address to array `s`.
-
-  Comparing this interface with the Unix `write()` syscall shows two benefits:
-
-  1. SubX can handle 'fake' file descriptors in tests.
-
-  1. `write()` accepts buffer and its size in separate arguments, which
-     requires callers to manage the two separately and so can be error-prone.
-     SubX's wrapper keeps the two together to increase the chances that we
-     never accidentally go out of array bounds.
-
-- `read`: takes two arguments, a file `f` and an address to stream `s`. Reads
-  as much data from `f` as can fit in (the free space of) `s`.
-
-  Like with `write()`, this wrapper around the Unix `read()` syscall adds the
-  ability to handle 'fake' file descriptors in tests, and reduces the chances
-  of clobbering outside array bounds.
-
-  One bit of weirdness here: in tests we do a redundant copy from one stream
-  to another. See [the comments before the implementation](http://akkartik.github.io/mu/html/060read.subx.html)
-  for a discussion of alternative interfaces.
-
-- `stop`: takes two arguments:
-  - `ed` is an address to an _exit descriptor_. Exit descriptors allow us to
-    `exit()` the program in production, but return to the test harness within
-    tests. That allows tests to make assertions about when `exit()` is called.
-  - `value` is the status code to `exit()` with.
-
-  For more details on exit descriptors and how to create one, see [the
-  comments before the implementation](http://akkartik.github.io/mu/html/059stop.subx.html).
-
-- `new-segment`
-
-  Allocates a whole new segment of memory for the program, discontiguous with
-  both existing code and data (heap) segments. Just a more opinionated form of
-  [`mmap`](http://man7.org/linux/man-pages/man2/mmap.2.html).
+#### assertions for tests
 
-- `allocate`: takes two arguments, an address to allocation-descriptor `ad`
-  and an integer `n`
+- `check`: fails current test if given boolean is false (`= 0`).
+- `check-not`: fails current test if given boolean isn't false (`!= 0`).
+- `check-ints-equal`: fails current test if given ints aren't equal.
+- `check-strings-equal`: fails current test if given strings have different bytes.
+- `check-stream-equal`: fails current test if stream's data doesn't match
+  string in its entirety. Ignores the stream's read index.
+- `check-array-equal`: fails if an array's elements don't match what's written
+  in a whitespace-separated string.
+- `check-next-stream-line-equal`: fails current test if next line of stream
+  until newline doesn't match string.
 
-  Allocates a contiguous range of memory that is guaranteed to be exclusively
-  available to the caller. Returns the starting address to the range in `eax`.
+#### predicates
 
-  An allocation descriptor tracks allocated vs available addresses in some
-  contiguous range of memory. The int specifies the number of bytes to allocate.
+- `handle-equal?`: checks if two handles point at the identical address. Does
+  not compare payloads at their respective addresses.
 
-  Explicitly passing in an allocation descriptor allows for nested memory
-  management, where a sub-system gets a chunk of memory and further parcels it
-  out to individual allocations. Particularly helpful for (surprise) tests.
+- `array-equal?`: checks if two arrays (of ints only for now) have identical
+  elements.
 
-- ... _(to be continued)_
+- `string-equal?`: compares two strings.
+- `stream-data-equal?`: compares a stream with a string.
+- `next-stream-line-equal?`: compares with string the next line in a stream, from
+  `read` index to newline.
 
-I will continue to import syscalls over time from [the old Mu VM in the parent
-directory](https://github.com/akkartik/mu), which has experimented with
-interfaces for the screen, keyboard, mouse, disk and network.
+- `slice-empty?`: checks if the `start` and `end` of a slice are equal.
+- `slice-equal?`: compares a slice with a string.
+- `slice-starts-with?`: compares the start of a slice with a string.
 
-### primitives built atop system calls
+- `stream-full?`: checks if a write to a stream would abort.
+- `stream-empty?`: checks if a read from a stream would abort.
 
-_(Compound arguments are usually passed in by reference. Where the results are
-compound objects that don't fit in a register, the caller usually passes in
-allocated memory for it.)_
+#### arrays
 
-#### assertions for tests
-- `check-ints-equal`: fails current test if given ints aren't equal
-- `check-stream-equal`: fails current test if stream doesn't match string
-- `check-next-stream-line-equal`: fails current test if next line of stream
-  until newline doesn't match string
+- `populate`: allocates space for `n` objects of the appropriate type.
+- `copy-array`: allocates enough space and writes out a copy of an array of
+  some type.
+- `slice-to-string`: allocates space for an array of bytes and copies the
+  slice into it.
 
-#### error handling
-- `error`: takes three arguments, an exit-descriptor, a file and a string (message)
+#### streams
 
-  Prints out the message to the file and then exits using the provided
-  exit-descriptor.
+- `populate-stream`: allocates space in a stream for `n` objects of the
+  appropriate type.
+- `write-to-stream`: writes arbitrary objects to a stream of the appropriate
+  type.
+- `read-from-stream`: reads arbitrary objects from a stream of the appropriate
+  type.
+- `stream-to-array`: allocates just enough space and writes out a stream's
+  data between its read index (inclusive) and write index (exclusive).
 
-- `error-byte`: like `error` but takes an extra byte value that it prints out
-  at the end of the message.
-
-#### predicates
-- `kernel-string-equal?`: compares a kernel string with a string
-- `string-equal?`: compares two strings
-- `stream-data-equal?`: compares a stream with a string
-- `next-stream-line-equal?`: compares with string the next line in a stream, from
-  `read` index to newline
-
-- `slice-empty?`: checks if the `start` and `end` of a slice are equal
-- `slice-equal?`: compares a slice with a string
-- `slice-starts-with?`: compares the start of a slice with a string
-- `slice-ends-with?`: compares the end of a slice with a string
-
-#### writing to disk
-- `write`: string -> file
-  - Can also be used to cat a string into a stream.
-  - Will abort the entire program if destination is a stream and doesn't have
-    enough room.
-- `write-stream`: stream -> file
-  - Can also be used to cat one stream into another.
-  - Will abort the entire program if destination is a stream and doesn't have
-    enough room.
-- `write-slice`: slice -> stream
-  - Will abort the entire program if there isn't enough room in the
-    destination stream.
-- `append-byte`: int -> stream
-  - Will abort the entire program if there isn't enough room in the
-    destination stream.
-- `append-byte-hex`: int -> stream
-  - textual representation in hex, no '0x' prefix
-  - Will abort the entire program if there isn't enough room in the
-    destination stream.
-- `print-int32`: int -> stream
-  - textual representation in hex, including '0x' prefix
-  - Will abort the entire program if there isn't enough room in the
-    destination stream.
-- `write-buffered`: string -> buffered-file
-- `write-slice-buffered`: slice -> buffered-file
-- `flush`: buffered-file
-- `write-byte-buffered`: int -> buffered-file
-- `print-byte-buffered`: int -> buffered-file
-  - textual representation in hex, no '0x' prefix
-- `print-int32-buffered`: int -> buffered-file
-  - textual representation in hex, including '0x' prefix
-
-#### reading from disk
-- `read`: file -> stream
-  - Can also be used to cat one stream into another.
-  - Will silently stop reading when destination runs out of space.
-- `read-byte-buffered`: buffered-file -> byte
-- `read-line-buffered`: buffered-file -> stream
-  - Will abort the entire program if there isn't enough room.
-
-#### non-IO operations on streams
-- `new-stream`: allocates space for a stream of `n` elements, each occupying
-  `b` bytes.
-  - Will abort the entire program if `n*b` requires more than 32 bits.
 - `clear-stream`: resets everything in the stream to `0` (except its `size`).
 - `rewind-stream`: resets the read index of the stream to `0` without modifying
   its contents.
 
+- `write`: writes a string into a stream of bytes. Doesn't support streams of
+  other types.
+- `write-stream`: concatenates one stream into another.
+- `write-slice`: writes a slice into a stream of bytes.
+- `append-byte`: writes a single byte into a stream of bytes.
+- `append-byte-hex`: writes textual representation of lowest byte in hex to
+  a stream of bytes. Does not write a '0x' prefix.
+- `read-byte`: reads a single byte from a stream of bytes.
+
 #### reading/writing hex representations of integers
-- `is-hex-int?`: takes a slice argument, returns boolean result in `eax`
-- `parse-hex-int`: takes a slice argument, returns int result in `eax`
-- `is-hex-digit?`: takes a 32-bit word containing a single byte, returns
-  boolean result in `eax`.
-- `from-hex-char`: takes a hexadecimal digit character in `eax`, returns its
-  numeric value in `eax`
-- `to-hex-char`: takes a single-digit numeric value in `eax`, returns its
-  corresponding hexadecimal character in `eax`
-
-#### tokenization
-
-from a stream:
-- `next-token`: stream, delimiter byte -> slice
-- `skip-chars-matching`: stream, delimiter byte
-- `skip-chars-not-matching`: stream, delimiter byte
-
-from a slice:
-- `next-token-from-slice`: start, end, delimiter byte -> slice
-  - Given a slice and a delimiter byte, returns a new slice inside the input
-    that ends at the delimiter byte.
-
-- `skip-chars-matching-in-slice`: curr, end, delimiter byte -> new-curr (in `eax`)
-- `skip-chars-not-matching-in-slice`:  curr, end, delimiter byte -> new-curr (in `eax`)
+
+- `write-int32-hex`
+- `hex-int?`: checks if a slice contains an int in hex. Supports '0x' prefix.
+- `parse-hex-int`: reads int in hex from string
+- `parse-hex-int-from-slice`: reads int in hex from slice
+- `parse-array-of-ints`: reads in multiple ints in hex, separated by whitespace.
+- `hex-digit?`: checks if byte is in [0, 9] or [a, f] (lowercase only)
+
+- `write-int32-decimal`
+- `parse-decimal-int`
+- `parse-decimal-int-from-slice`
+- `parse-decimal-int-from-stream`
+- `parse-array-of-decimal-ints`
+- `decimal-digit?`: checks if byte is in [0, 9]
+
+#### printing to screen
+
+All screen primitives require a screen object, which can be either the real
+screen on the computer or a fake screen for tests.
+
+The real screen on the Mu computer can currently display only ASCII characters,
+though it's easy to import more of the font. There is only one font. All
+graphemes are 8 pixels wide and 16 pixels tall. These constraints only apply
+to the real screen.
+
+- `draw-grapheme`: draws a single grapheme at a given coordinate, with given
+  foreground and background colors.
+- `render-grapheme`: like `draw-grapheme` and can also handle newlines
+  assuming text is printed left-to-right, top-to-bottom.
+- `draw-code-point`
+- `clear-screen`
+
+- `draw-text-rightward`: draws a single line of text, stopping when it reaches
+  either the provided bound or the right screen margin.
+- `draw-stream-rightward`
+- `draw-text-rightward-over-full-screen`: does not provide a bound.
+- `draw-text-wrapping-right-then-down`: draws multiple lines of text on screen
+  with simplistic word-wrap (no hyphenation) within (x, y) bounds.
+- `draw-stream-wrapping-right-then-down`
+- `draw-text-wrapping-right-then-down-over-full-screen`
+- `draw-int32-hex-wrapping-right-then-down`
+- `draw-int32-hex-wrapping-right-then-down-over-full-screen`
+- `draw-int32-decimal-wrapping-right-then-down`
+- `draw-int32-decimal-wrapping-right-then-down-over-full-screen`
+
+Similar primitives for writing text top-to-bottom, left-to-right.
+
+- `draw-text-downward`
+- `draw-stream-downward`
+- `draw-text-wrapping-down-then-right`
+- `draw-stream-wrapping-down-then-right`
+- `draw-text-wrapping-down-then-right-over-full-screen`
+- `draw-int32-hex-wrapping-down-then-right`
+- `draw-int32-hex-wrapping-down-then-right-over-full-screen`
+- `draw-int32-decimal-wrapping-down-then-right`
+- `draw-int32-decimal-wrapping-down-then-right-over-full-screen`
+
+Screens remember the current cursor position.
+
+- `cursor-position`
+- `set-cursor-position`
+- `draw-grapheme-at-cursor`
+- `draw-code-point-at-cursor`
+- `draw-cursor`: highlights the current position of the cursor. Programs must
+  pass in the grapheme to draw at the cursor position, and are responsible for
+  clearing the highlight when the cursor moves.
+- `move-cursor-left`, `move-cursor-right`, `move-cursor-up`, `move-cursor-down`.
+  These primitives always silently fail if the desired movement would go out
+  of screen bounds.
+- `move-cursor-to-left-margin-of-next-line`
+- `move-cursor-rightward-and-downward`: move cursor one grapheme to the right
+
+- `draw-text-rightward-from-cursor`
+- `draw-text-wrapping-right-then-down-from-cursor`
+- `draw-text-wrapping-right-then-down-from-cursor-over-full-screen`
+- `draw-int32-hex-wrapping-right-then-down-from-cursor`
+- `draw-int32-hex-wrapping-right-then-down-from-cursor-over-full-screen`
+- `draw-int32-decimal-wrapping-right-then-down-from-cursor`
+- `draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen`
+
+- `draw-text-wrapping-down-then-right-from-cursor`
+- `draw-text-wrapping-down-then-right-from-cursor-over-full-screen`
+
+Assertions for tests:
+
+- `check-screen-row`: compare a screen from the left margin of a given row
+  index with a string. The row index counts downward from 0 at the top of the
+  screen. String can be smaller or larger than a single row, and defines the
+  region of interest. Strings longer than a row wrap around to the left margin
+  of the next screen row. Currently assumes text is printed left-to-right on
+  the screen.
+- `check-screen-row-from`: compare a fragment of a screen (left to write, top
+  to bottom) starting from a given (x, y) coordinate with an expected string.
+  Currently assumes text is printed left-to-right and top-to-bottom on the
+  screen.
+- `check-screen-row-in-color`: like `check-screen-row` but:
+  - also compares foreground color
+  - ignores screen locations where the expected string contains spaces
+- `check-screen-row-in-color-from`
+- `check-screen-row-in-background-color`
+- `check-screen-row-in-background-color-from`
+- `check-background-color-in-screen-row`: unlike previous functions, this
+  doesn't check screen contents, only background color. Ignores background
+  color where expected string contains spaces, and compares background color
+  where expected string does not contain spaces. Never compares the character
+  at any screen location.
+- `check-background-color-in-screen-row-from`