about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorKartik K. Agaram <vc@akkartik.com>2021-10-23 23:38:14 -0700
committerKartik K. Agaram <vc@akkartik.com>2021-10-23 23:38:14 -0700
commitb4a3e804af6e78fe129f9a500c995d64fe70c4c0 (patch)
tree48ff67492ff9718ca71b441204bee79f0250cc82
parent145957b7ee74c9698f6735caf3505eba67b33d03 (diff)
downloadmu-b4a3e804af6e78fe129f9a500c995d64fe70c4c0.tar.gz
rewrite Mu reference
-rw-r--r--mu.md892
-rw-r--r--tutorial/index.md16
2 files changed, 390 insertions, 518 deletions
diff --git a/mu.md b/mu.md
index e8ec2dfa..1eeb9842 100644
--- a/mu.md
+++ b/mu.md
@@ -1,594 +1,559 @@
-# Mu Syntax
+# Mu reference
 
-Here are two valid statements in Mu:
+Mu programs are sequences of `fn` and `type` definitions.
+
+## Functions
+
+Define functions with the `fn` keyword. For example:
 
 ```
-increment x
-y <- increment
+  fn foo arg1: int, arg2: int -> result/eax: boolean
 ```
 
-Understanding when to use one vs the other is the critical idea in Mu. In
-short, the former increments a value in memory, while the latter increments a
-value in a register.
+Functions contain `{}` blocks, `var` declarations, primitive statements and
+calls to other functions. Primitive statements and function calls look
+similar:
 
-Most languages start from some syntax and do what it takes to implement it.
-Mu, however, is designed as a safe way to program in [a regular subset of
-32-bit x86 machine code](subx.md), _satisficing_ rather than optimizing for a
-clean syntax. To keep the mapping to machine code lightweight, Mu exclusively
-uses statements. Most statements map to a single instruction of machine code.
+```
+  out1, out2, out3, ... <- operation inout1, inout2, inout3, ...
+```
 
-Since the x86 instruction set restricts how many memory locations an instruction
-can use, Mu makes registers explicit as well. Variables must be explicitly
-mapped to specific registers; otherwise they live in memory. While you have to
-do your own register allocation, Mu will helpfully point out when you get it
-wrong.
+They can take any number of inouts and outputs, including 0. Statements
+with 0 outputs also drop the `<-`.
 
-Statements consist of 3 parts: the operation, optional _inouts_ and optional
-_outputs_. Outputs come before the operation name and `<-`.
+Inouts can be either variables in memory, variables in registers, or
+constants. Outputs are always variables in registers.
 
-Outputs are always registers; memory locations that need to be modified are
-passed in by reference in inouts.
+Inouts in memory can be either inputs or outputs (if they're addresses being
+written to). Hence the name.
 
-So Mu programmers need to make two new categories of decisions: whether to
-define variables in registers or memory, and whether to put variables to the
-left or right. There's always exactly one way to write any given operation. In
-return for this overhead you get a lightweight and future-proof stack. And Mu
-will provide good error messages to support you.
+Primitives can often write to arbitrary output registers. User-defined
+functions, however, require rigidly specified output registers.
 
-Further down, this page enumerates all available primitives in Mu, and [a
-separate page](http://akkartik.github.io/mu/html/mu_instructions.html)
-describes how each primitive is translated to machine code. There is also a
-useful list of pre-defined functions (implemented in unsafe machine code) in [400.mu](http://akkartik.github.io/mu/html/400.mu.html)
-and [vocabulary.md](vocabulary.md).
+## Variables, registers and memory
 
-## Functions and calls
+Declare local variables in a function using the `var` keyword.
 
-Zooming out from single statements, here's a complete sample program in Mu
-that runs in Linux:
+You can declare local variables in either registers or memory (the stack). So
+a `var` statement has two forms:
+  - `var x/eax: int <- copy 0`
+  - `var x: int`
 
-<img alt='ex2.mu' src='html/ex2.mu.png' width='400px'>
+Variables in registers must be initialized. Variables on the stack are
+implicitly zeroed out.
 
-Mu programs are lists of functions. Each function has the following form:
+Variables can be in six 32-bit _general-purpose_ registers of the x86 processor.
+  - eax
+  - ebx
+  - ecx
+  - edx
+  - esi ('s' often a mnemonic for 'source')
+  - edi ('d' often a mnemonic for 'destination')
 
-```
-fn _name_ _inout_ ... -> _output_ ... {
-  _statement_
-  _statement_
-  ...
-}
-```
+You can store several types in these registers:
+  - int
+  - boolean
+  - (addr T) (address into memory)
+  - byte (uses only 8 bits)
+  - code-point (Unicode)
+  - grapheme (code-point encoded in UTF-8)
 
-Each function has a header line, and some number of statements, each on a
-separate line. Headers describe inouts and outputs. Inouts can't be registers,
-and outputs _must_ be registers (specified using metadata after a `/`).
-Outputs can't take names.
+There's one 32-bit type you _cannot_ store in these registers:
+  - float
 
-The above program also demonstrates a function call (to the function `do-add`).
-Function calls look the same as primitive statements: they can return (multiple)
-outputs in registers, and modify inouts passed in by reference. In addition,
-there's one more constraint: output registers must match the function header.
-For example:
+It instead uses eight separate 32-bit registers: xmm0, xmm1, ..., xmm7
 
-```
-fn f -> _/eax: int {
-  ...
-}
-fn g {
-  a/eax <- f  # ok
-  a/ebx <- f  # wrong; `a` must be in register `eax`
-}
-```
+Types that require more than 32 bits (4 bytes) cannot be stored in registers:
+  - (array T)
+  - (handle T)
+  - (stream T)
+  - slice
+  - any compound types you define using the `type` keyword
 
-You can exit a function at any time with the `return` instruction. Give it the
-right number of arguments, and it'll assign them respectively to the function's
-outputs before jumping back to the caller.
+`T` here can be any type, including combinations of types. For example:
+  - (array int) -- an array of ints
+  - (addr int) -- an address to an int
+  - (handle int) -- a handle to an int
+  - (addr handle int) -- an address to a handle to int
+  - (addr array handle int) -- an address to an array of handles to ints
+  - ...and so on.
 
-Mu encloses multi-word types in parentheses, and types can get quite expressive.
-For example, you read `main`'s inout type as "an address to an array of
-addresses to arrays of bytes." Since addresses to arrays of bytes are almost
-always strings in Mu, you'll quickly learn to mentally shorten this type to
-"an address to an array of strings".
+Other miscellaneous restrictions:
+  - `byte` variables must be either in registers or on the heap, never local
+    variables on the stack.
+  - `addr` variables can never "escape" a function either by being returned or
+    by being written to a memory location. When you need that sort of thing,
+    use a `handle` instead.
 
-Mu currently has no way to name magic constants. Instead, document integer
-literals using metadata after a `/`. For example:
+## Primitive statement types
 
-```
-var x/eax: int <- copy 3/margin-left
-```
+These usually operate on variables with 32-bit types, with some restrictions
+noted below. Most instructions with multiple args require types to match.
 
-Here we use metadata in two ways: to specify a register for the variable `x`
-(checked), and to give a name to the constant `3` (unchecked; purely for
-documentation).
+Notation in this section:
+  - `var/reg` indicates a variable in a register
+  - `var/xreg` indicates a variable in a floating-point register
+  - `var` without a `reg` indicates either a variable on the stack or
+    dereferencing a variable in a (non-floating-point) register: `*var/reg`
+  - `n` indicates a literal integer. There are no floating-point literals.
 
-Variables can't currently accept unchecked metadata for documentation.
-(Perhaps this should change.)
+### Moving values around
 
-The function `main` is special. It's where Mu programs start executing. It has
-a different signature depending on whether a Mu program requires Linux or can
-run without an OS. On Linux, the signature looks like this:
+These instructions work with variables of any 32-bit type except `addr` and
+`float`.
 
 ```
-fn main args: (addr array addr array byte) -> _/ebx: int
+  var/reg <- copy var2/reg2
+  copy-to var1, var2/reg
+  var/reg <- copy var2
+  var/reg <- copy n
+  copy-to var, n
 ```
 
-It takes an array of strings and returns a status code to Linux in register
-`ebx`.
-
-Without an OS, the signature looks like this:
+Byte variables have their own instructions:
 
 ```
-fn main screen: (addr screen), keyboard: (addr keyboard), data-disk: (addr disk)
+  var/reg <- copy-byte var2/reg2
+  var/reg <- copy-byte *var2/reg2     # var2 must have type (addr byte)
+  copy-byte-to *var1/reg1, var2/reg2  # var1 must have type (addr byte)
 ```
 
-A screen and keyboard are explicitly passed in. The goal is for all hardware
-dependencies to always be explicit. However there are currently gaps:
-  * The mouse is accessed implicitly
-  * The screen argument only supports text-mode graphics. Pixel graphics rely
-    on implicit access to the screen.
-  * The Mu computer has two disks, and the disk containing Mu code is not
-    accessible.
-
-## Blocks
+Floating point instructions can be copied as well, but only to floating-point
+registers `xmm_`.
 
-Blocks are useful for grouping related statements. They're delimited by `{`
-and `}`, each alone on a line.
+```
+  var/xreg <- copy var2/xreg2
+  copy-to var1, var2/xreg
+  var/xreg <- copy var2
+  var/xreg <- copy *var2/reg2         # var2 must have type (addr byte) and live in a general-purpose register
+```
 
-Blocks can nest:
+There's no way to copy a literal to a floating-point register. However,
+there's a few ways to convert non-float values in general-purpose registers.
 
 ```
-{
-  _statements_
-  {
-    _more statements_
-  }
-}
+  var/xreg <- convert var2/reg2
+  var/xreg <- convert var2
+  var/xreg <- convert *var2/reg2
 ```
 
-Blocks can be named (with the name ending in a `:` on the same line as the
-`{`):
+Correspondingly, there are ways to convert floats into integers.
 
 ```
-$name: {
-  _statements_
-}
+  var/reg <- convert var2/xreg2
+  var/reg <- convert var2
+  var/reg <- convert *var2/reg2
+
+  var/reg <- truncate var2/xreg2
+  var/reg <- truncate var2
+  var/reg <- truncate *var2/reg2
 ```
 
-Further down we'll see primitive statements for skipping or repeating blocks.
-Besides control flow, the other use for blocks is...
+### Comparing values
+
+Work with variables of any 32-bit type. `addr` variables can only be compared
+to 0.
 
-## Local variables
+  compare var1, var2/reg
+  compare var1/reg, var2
+  compare var/eax, n
+  compare var, n
 
-Functions can define new variables at any time with the keyword `var`. There
-are two variants of the `var` statement, for defining variables in registers
-or memory.
+Floating-point numbers cannot be compared to literals, and the register must
+come first.
+
+  compare var1/xreg1, var2/xreg2
+  compare var1/xreg1, var2
+
+### Branches
+
+Immediately after a `compare` instruction you can branch on its result. For
+example:
 
 ```
-var name: type
-var name/reg: type <- ...
+  break-if-=
 ```
 
-Variables on the stack are never initialized. (They're always implicitly
-zeroed out.) Variables in registers are always initialized.
+This instruction will jump to after the enclosing `{}` block if the previous
+`compare` detected equality. Here's the list of conditional and unconditional
+`break` instructions:
 
-Register variables can go in 6 integer registers (`eax`, `ebx`, `ecx`, `edx`,
-`esi`, `edi`) or 8 floating-point registers (`xmm0`, `xmm1`, `xmm2`, `xmm3`,
-`xmm4`, `xmm5`, `xmm6`, `xmm7`).
-
-Defining a variable in a register either clobbers the previous variable (if it
-was defined in the same block) or shadows it temporarily (if it was defined in
-an outer block).
+```
+  break
+  break-if-=
+  break-if-!=
+  break-if-<
+  break-if->
+  break-if-<=
+  break-if->=
+```
 
-Variables exist from their definition until the end of their containing block.
-Register variables may also die earlier if their register is clobbered by a
-new variable.
+Similarly, you can jump back to the start of the enclosing `{}` block with
+`loop`. Here's the list of `loop` instructions.
 
-Variables on the stack can be of many types (but not `byte`). Integer registers
-can only contain 32-bit values: `int`, `byte`, `boolean`, `(addr ...)`. Floating-point
-registers can only contain values of type `float`.
+```
+  loop
+  loop-if-=
+  loop-if-!=
+  loop-if-<
+  loop-if->
+  loop-if-<=
+  loop-if->=
+```
 
-## Integer primitives
+Additionally, there are special variants for comparing `addr` and `float`
+values, which results in the following comprehensive list of jumps:
 
-Here is the list of arithmetic primitive operations supported by Mu. The name
-`n` indicates a literal integer rather than a variable, and `var/reg` indicates
-a variable in a register, though that's not always valid Mu syntax.
+```
+  break
+  break-if-=
+  break-if-!=
+  break-if-<    break-if-addr<    break-if-float<
+  break-if->    break-if-addr>    break-if-float>
+  break-if-<=   break-if-addr<=   break-if-float<=
+  break-if->=   break-if-addr>=   break-if-float>=
 
+  loop
+  loop-if-=
+  loop-if-!=
+  loop-if-<     loop-if-addr<     loop-if-float<
+  loop-if->     loop-if-addr>     loop-if-float>
+  loop-if-<=    loop-if-addr<=    loop-if-float<=
+  loop-if->=    loop-if-addr>=    loop-if-float>=
 ```
-var/reg <- increment
-increment var
-var/reg <- decrement
-decrement var
 
-var1/reg1 <- add var2/reg2
-var/reg <- add var2
-add-to var1, var2/reg
-var/reg <- add n
-add-to var, n
+One final property all these jump instructions share: they can take an
+optional block name to jump to. For example:
+
+```
+  a: {
+    ...
+    break a     #----------|
+    ...               #    |
+  }                   # <--|
 
-var1/reg1 <- subtract var2/reg2
-var/reg <- subtract var2
-subtract-from var1, var2/reg
-var/reg <- subtract n
-subtract-from var, n
 
-var1/reg1 <- negate
-negate var
+  a: {                # <--|
+    ...               #    |
+    b: {              #    |
+      ...             #    |
+      loop a    #----------|
+      ...
+    }
+    ...
+  }
+```
 
-var/reg <- copy var2/reg2
-copy-to var1, var2/reg
-var/reg <- copy var2
-var/reg <- copy n
-copy-to var, n
+However, there's no way to jump to a block that doesn't contain the `loop` or
+`break` instruction.
 
-compare var1, var2/reg
-compare var1/reg, var2
-compare var/eax, n
-compare var, n
+### Integer arithmetic
 
-var/reg <- shift-left n
-var/reg <- shift-right n
-var/reg <- shift-right-signed n
-shift-left var, n
-shift-right var, n
-shift-right-signed var, n
+These instructions require variables of non-`addr`, non-float types.
 
-var/reg <- multiply var2
+Add:
+```
+  var1/reg1 <- add var2/reg2
+  var/reg <- add var2
+  add-to var1, var2/reg                 # var1 += var2
+  var/reg <- add n
+  add-to var, n
 ```
 
-Bitwise operations:
+Subtract:
+```
+  var1/reg1 <- subtract var2/reg2
+  var/reg <- subtract var2
+  subtract-from var1, var2/reg          # var1 -= var2
+  var/reg <- subtract n
+  subtract-from var, n
 ```
-var1/reg1 <- and var2/reg2
-var/reg <- and var2
-and-with var1, var2/reg
-var/reg <- and n
-and-with var, n
 
-var1/reg1 <- or var2/reg2
-var/reg <- or var2
-or-with var1, var2/reg
-var/reg <- or n
-or-with var, n
+Add one:
+```
+  var/reg <- increment
+  increment var
+```
 
-var1/reg1 <- not
-not var
+Subtract one:
+```
+  var/reg <- decrement
+  decrement var
+```
 
-var1/reg1 <- xor var2/reg2
-var/reg <- xor var2
-xor-with var1, var2/reg
-var/reg <- xor n
-xor-with var, n
+Multiply:
+```
+  var/reg <- multiply var2
 ```
 
-Any statement above that takes a variable in memory can be replaced with a
-dereference (`*`) of an address variable (of type `(addr ...)`) in a register.
-You can't dereference variables in memory. You have to load them into a
-register first.
+The result of a multiply must be a register.
 
-Excluding dereferences, the above statements must operate on non-address
-values with primitive types: `int`, `boolean` or `byte`. (Booleans are really
-just `int`s, and Mu assumes any value but `0` is true.) You can copy addresses
-to int variables, but not the other way around.
+Negate:
+```
+  var1/reg1 <- negate
+  negate var
+```
 
-## Floating-point primitives
+### Floating-point arithmetic
 
-These instructions may use the floating-point registers `xmm0` ... `xmm7`
-(denoted by `/xreg2` or `/xrm32`). They also use integer values on occasion
-(`/rm32` and `/r32`).
+Operations on `float` variables include a few we've seen before and some new
+ones. Notice here that we mostly use floating-point registers `xmm_`, but
+still use the general-purpose registers when dereferencing variables of type
+`(addr float)`.
 
 ```
-var/xreg <- add var2/xreg2
-var/xreg <- add var2
-var/xreg <- add *var2/reg2
+  var/xreg <- add var2/xreg2
+  var/xreg <- add var2
+  var/xreg <- add *var2/reg2
 
-var/xreg <- subtract var2/xreg2
-var/xreg <- subtract var2
-var/xreg <- subtract *var2/reg2
+  var/xreg <- subtract var2/xreg2
+  var/xreg <- subtract var2
+  var/xreg <- subtract *var2/reg2
 
-var/xreg <- multiply var2/xreg2
-var/xreg <- multiply var2
-var/xreg <- multiply *var2/reg2
+  var/xreg <- multiply var2/xreg2
+  var/xreg <- multiply var2
+  var/xreg <- multiply *var2/reg2
 
-var/xreg <- divide var2/xreg2
-var/xreg <- divide var2
-var/xreg <- divide *var2/reg2
+  var/xreg <- divide var2/xreg2
+  var/xreg <- divide var2
+  var/xreg <- divide *var2/reg2
 
-var/xreg <- reciprocal var2/xreg2
-var/xreg <- reciprocal var2
-var/xreg <- reciprocal *var2/reg2
+  var/xreg <- reciprocal var2/xreg2
+  var/xreg <- reciprocal var2
+  var/xreg <- reciprocal *var2/reg2
 
-var/xreg <- square-root var2/xreg2
-var/xreg <- square-root var2
-var/xreg <- square-root *var2/reg2
+  var/xreg <- square-root var2/xreg2
+  var/xreg <- square-root var2
+  var/xreg <- square-root *var2/reg2
 
-var/xreg <- inverse-square-root var2/xreg2
-var/xreg <- inverse-square-root var2
-var/xreg <- inverse-square-root *var2/reg2
+  var/xreg <- inverse-square-root var2/xreg2
+  var/xreg <- inverse-square-root var2
+  var/xreg <- inverse-square-root *var2/reg2
 
-var/xreg <- min var2/xreg2
-var/xreg <- min var2
-var/xreg <- min *var2/reg2
+  var/xreg <- min var2/xreg2
+  var/xreg <- min var2
+  var/xreg <- min *var2/reg2
 
-var/xreg <- max var2/xreg2
-var/xreg <- max var2
-var/xreg <- max *var2/reg2
+  var/xreg <- max var2/xreg2
+  var/xreg <- max var2
+  var/xreg <- max *var2/reg2
 ```
 
-Remember, when these instructions use indirect mode, they still use an integer
-register. Floating-point registers can't hold addresses.
-
 Two instructions in the above list are approximate. According to the Intel
 manual, `reciprocal` and `inverse-square-root` [go off the rails around the
-fourth decimal place](x86_approx.md). If you need more precision, use `divide`
-separately.
+fourth decimal place](linux/x86_approx.md). If you need more precision, use
+`divide` separately.
 
-Most instructions operate exclusively on integer or floating-point operands.
-The only exceptions are the instructions for converting between integers and
-floating-point numbers.
-
-```
-var/xreg <- convert var2/reg2
-var/xreg <- convert var2
-var/xreg <- convert *var2/reg2
+### Bitwise boolean operations
 
-var/reg <- convert var2/xreg2
-var/reg <- convert var2
-var/reg <- convert *var2/reg2
+These require variables of non-`addr`, non-float types.
 
-var/reg <- truncate var2/xreg2
-var/reg <- truncate var2
-var/reg <- truncate *var2/reg2
+And:
+```
+  var1/reg1 <- and var2/reg2
+  var/reg <- and var2
+  and-with var1, var2/reg
+  var/reg <- and n
+  and-with var, n
 ```
 
-There are no instructions accepting floating-point literals. To obtain integer
-literals in floating-point registers, copy them to general-purpose registers
-and then convert them to floating-point.
-
-The floating-point instructions above always write to registers. The only
-instructions that can write floats to memory are `copy` instructions.
-
+Or:
 ```
-var/xreg <- copy var2/xreg2
-copy-to var1, var2/xreg
-var/xreg <- copy var2
-var/xreg <- copy *var2/reg2
+  var1/reg1 <- or var2/reg2
+  var/reg <- or var2
+  or-with var1, var2/reg
+  var/reg <- or n
+  or-with var, n
 ```
 
-Finally, there are floating-point comparisons. They must always put a register
-on the left-hand side:
-
+Not:
 ```
-compare var1/xreg1, var2/xreg2
-compare var1/xreg1, var2
+  var1/reg1 <- not
+  not var
 ```
 
-## Operating on individual bytes
+Xor:
+```
+  var1/reg1 <- xor var2/reg2
+  var/reg <- xor var2
+  xor-with var1, var2/reg
+  var/reg <- xor n
+  xor-with var, n
+```
 
-A special case is variables of type `byte`. Mu is a 32-bit platform so for the
-most part only supports types that are multiples of 32 bits. However, we do
-want to support strings in ASCII and UTF-8, which will be arrays of 8-bit
-bytes.
+### Shifts
 
-Since most x86 instructions implicitly load 32 bits at a time from memory,
-variables of type 'byte' are only allowed in registers, not on the stack. Here
-are the possible statements for reading bytes to/from memory:
+Shifts require variables of non-`addr`, non-float types.
 
 ```
-var/reg <- copy-byte var2/reg2      # var: byte
-var/reg <- copy-byte *var2/reg2     # var: byte
-copy-byte-to *var1/reg1, var2/reg2  # var1: (addr byte)
+  var/reg <- shift-left n
+  var/reg <- shift-right n
+  var/reg <- shift-right-signed n
+  shift-left var, n
+  shift-right var, n
+  shift-right-signed var, n
 ```
 
-In addition, variables of type 'byte' are restricted to (the lowest bytes of)
-just 4 registers: `eax`, `ecx`, `edx` and `ebx`. As always, this is due to
-constraints of the x86 instruction set.
+Shifting bits left always inserts zeros on the right.
+Shifting bits right inserts zeros on the left by default.
+A _signed_ shift right duplicates the leftmost bit, thereby preserving the
+sign of an integer.
+
+## More complex instructions on more complex types
 
-## Primitive jumps
+These instructions work with any type `T`. As before we use `/reg` here to
+indicate when a variable must live in a register. We also include type
+constraints after a `:`.
 
-There are two kinds of jumps, both with many variations: `break` and `loop`.
-`break` instructions jump to the end of the containing block. `loop` instructions
-jump to the beginning of the containing block.
+### Addresses and handles
 
-All jumps can take an optional label starting with '$':
+You can compute the address of any variable in memory (never in registers):
 
 ```
-loop $foo
+  var/reg: (addr T) <- address var2: T
 ```
 
-This instruction jumps to the beginning of the block called $foo. The corresponding
-`break` jumps to the end of the block. Either jump statement must lie somewhere
-inside such a block. Jumps are only legal to containing blocks. (Use named
-blocks with restraint; jumps to places far away can get confusing.)
+As mentioned up top, `addr` variables can never escape the function where
+they're computed. You can't store them on the heap, or in compound types.
 
-There are two unconditional jumps:
+To manage long-lived addresses, _allocate_ them on the heap.
 
 ```
-loop
-loop label
-break
-break label
+  allocate var: (addr handle T)       # var can be in either register or memory
 ```
 
-The remaining jump instructions are all conditional. Conditional jumps rely on
-the result of the most recently executed `compare` instruction. (To keep
-programs easy to read, keep `compare` instructions close to the jump that uses
-them.)
+Handles can be copied and stored without restriction. However, they're too
+large to fit in a register. You also can't access their payload directly, you
+have to first convert them into a short-lived `addr` using _lookup_.
 
 ```
-break-if-=
-break-if-= label
-break-if-!=
-break-if-!= label
+  var y/eax: (addr T) <- lookup x: (handle T)
 ```
 
-Inequalities are similar, but have additional variants for addresses and floats.
+The output of `lookup` is always returned in register `eax`. Many other
+function calls do the same thing. In practice, this means `eax` ends up being
+a temporary location used to store lots of variables in quick succession.
 
-```
-break-if-<
-break-if-< label
-break-if->
-break-if-> label
-break-if-<=
-break-if-<= label
-break-if->=
-break-if->= label
-
-break-if-addr<
-break-if-addr< label
-break-if-addr>
-break-if-addr> label
-break-if-addr<=
-break-if-addr<= label
-break-if-addr>=
-break-if-addr>= label
+Since handles are large compound types, there's a special helper for comparing
+them:
 
-break-if-float<
-break-if-float< label
-break-if-float>
-break-if-float> label
-break-if-float<=
-break-if-float<= label
-break-if-float>=
-break-if-float>= label
 ```
-
-Similarly, conditional loops:
-
+  var/eax: boolean <- handle-equal? var1: (handle T), var2: (handle T)
 ```
-loop-if-=
-loop-if-= label
-loop-if-!=
-loop-if-!= label
-
-loop-if-<
-loop-if-< label
-loop-if->
-loop-if-> label
-loop-if-<=
-loop-if-<= label
-loop-if->=
-loop-if->= label
 
-loop-if-addr<
-loop-if-addr< label
-loop-if-addr>
-loop-if-addr> label
-loop-if-addr<=
-loop-if-addr<= label
-loop-if-addr>=
-loop-if-addr>= label
+### Arrays
 
-loop-if-float<
-loop-if-float< label
-loop-if-float>
-loop-if-float> label
-loop-if-float<=
-loop-if-float<= label
-loop-if-float>=
-loop-if-float>= label
+Arrays are declared in two ways:
+  1. On the stack with a literal size:
 ```
-
-## Addresses
-
-Passing objects by reference requires the `address` operation, which returns
-an object of type `addr`.
-
+  var x: (array int 3)
 ```
-var/reg: (addr T) <- address var2: T
+  2. On the heap with a potentially variable size. For example:
 ```
+  var x: (handle array int)
+  var x-ah/eax: (addr handle array int) <- address x
+  populate x-ah, 8
+```
+  The `8` here can also be an int in a register or memory.
 
-Here `var2` can't live in a register.
-
-## Array operations
-
-Here's an example definition of a fixed-length array:
+You can compute the length of an array, though you'll need an `addr` to do so:
 
 ```
-var x: (array int 3)
+  var/reg: int <- length arr/reg: (addr array T)
 ```
 
-The length (here `3`) must be an integer literal. We'll show how to create
-dynamically-sized arrays further down.
-
-Arrays can be large; to avoid copying them around on every function call
-you'll usually want to manage `addr`s to them. Here's an example computing the
-address of an array.
+To read from or write to an array, use `index` to first obtain an address to
+read from or modify:
 
 ```
-var n/eax: (addr array int) <- address x
+  var/reg: (addr T) <- index arr/reg: (addr array T), n
+  var/reg: (addr T) <- index arr: (array T len), n
 ```
 
-Addresses to arrays don't include the array length in their type. However, you
-can obtain the length of an array like this:
+Like our notation of `n`, `len` here is required to be a literal.
+
+The index requested can also be a variable in a register, with one caveat:
 
 ```
-var/reg: int <- length arr/reg: (addr array T)
+  var/reg: (addr T) <- index arr/reg: (addr array T), idx/reg: int
+  var/reg: (addr T) <- index arr: (array T len), idx/reg: int
 ```
 
-To operate on elements of an array, use the `index` statement:
+The caveat: the size of T must be 1, 2, 4 or 8 bytes. For other sizes of T
+you'll need to split up the work, performing a `compute-offset` before the
+`index`.
 
 ```
-var/reg: (addr T) <- index arr/reg: (addr array T), n
-var/reg: (addr T) <- index arr: (array T len), n
+  var/reg: (offset T) <- compute-offset arr: (addr array T), idx/reg: int     # arr can be in reg or mem
+  var/reg: (offset T) <- compute-offset arr: (addr array T), idx: int         # arr can be in reg or mem
 ```
 
-The index can also be a variable in a register, with a caveat:
+The result of a `compute-offset` statement can be passed to `index`:
 
 ```
-var/reg: (addr T) <- index arr/reg: (addr array T), idx/reg: int
-var/reg: (addr T) <- index arr: (array T len), idx/reg: int
+  var/reg: (addr T) <- index arr/reg: (addr array T), idx/reg: (offset T)
 ```
 
-The caveat: the size of T must be 1, 2, 4 or 8 bytes. The x86 instruction set
-has complex addressing modes that can index into an array in a single instruction
-in these situations.
+### Stream operations
 
-For other sizes of T you'll need to split up the work, performing a `compute-offset`
-before the `index`.
+A common use for arrays is as buffers. Save a few items to a scratch space and
+then process them. This pattern is so common (we use it in files) that there's
+special support for it with a built-in type: `stream`.
+
+Streams are like arrays in many ways. You can initialize them with a length on
+the stack:
 
 ```
-var/reg: (offset T) <- compute-offset arr: (addr array T), idx/reg: int     # arr can be in reg or mem
-var/reg: (offset T) <- compute-offset arr: (addr array T), idx: int         # arr can be in reg or mem
+  var x: (stream int 3)
 ```
 
-The `compute-offset` statement returns a value of type `(offset T)` after
-performing any necessary bounds checking. Now the offset can be passed to
-`index` as usual:
-
+You can also populate them on the heap:
 ```
-var/reg: (addr T) <- index arr/reg: (addr array T), idx/reg: (offset T)
+  var x: (handle stream int)
+  var x-ah/eax: (addr handle stream int) <- address x
+  populate-stream x-ah, 8
 ```
 
-## Stream operations
+However, streams don't provide random access with an `index` instruction.
+Instead, you write to them sequentially, and read back what you wrote.
 
-A common use for arrays is as buffers. Save a few items to a scratch space and
-then process them. This pattern is so common (we use it in files) that there's
-special support for it with a built-in type: `stream`.
+```
+  read-from-stream s: (addr stream T), out: (addr T)
+  write-to-stream s: (addr stream T), in: (addr T)
+```
 
-Streams are like arrays in many ways. You can initialize them with a length:
+Streams of bytes are particularly common for managing Unicode text, and there
+are a few functions to help with them:
 
 ```
-var x: (stream int 3)
+  write s: (addr stream byte), u: (addr array byte)  # write u to s, abort if full
+  overflow?/eax: boolean <- try-write s: (addr stream byte), u: (addr array byte)
+  write-stream dest: (addr stream byte), src: (addr stream byte)
+  # bytes
+  append-byte s: (addr stream byte), var: int  # write lower byte of var
+  var/eax: byte <- read-byte s: (addr stream byte)
+  # 32-bit graphemes encoded in UTF-8
+  write-grapheme out: (addr stream byte), g: grapheme
+  g/eax: grapheme <- read-grapheme in: (addr stream byte)
 ```
 
-However, streams don't provide random access with an `index` instruction.
-Instead, you write to them sequentially, and read back what you wrote.
+You can check if a stream is empty or full:
 
 ```
-read-from-stream s: (addr stream T), out: (addr T)
-write-to-stream s: (addr stream T), in: (addr T)
-var/eax: boolean <- stream-empty? s: (addr stream)
-var/eax: boolean <- stream-full? s: (addr stream)
+  var/eax: boolean <- stream-empty? s: (addr stream)
+  var/eax: boolean <- stream-full? s: (addr stream)
 ```
 
 You can clear streams:
 
 ```
-clear-stream f: (addr stream _)
+  clear-stream f: (addr stream T)
 ```
 
-You can also rewind them to reread what's been written:
+You can also rewind them to reread their contents:
 
 ```
-rewind-stream f: (addr stream _)
+  rewind-stream f: (addr stream T)
 ```
 
 ## Compound types
@@ -610,7 +575,7 @@ which is described below). They also can't currently include `array`, `stream`
 or `byte` types. Since arrays and streams carry their size with them, supporting
 them in compound types complicates variable initialization. Instead of
 defining them inline in a type definition, define a `handle` to them. Bytes
-shouldn't be used for anything but utf-8 strings.
+shouldn't be used for anything but UTF-8 strings.
 
 To access within a compound type, use the `get` instruction. There are two
 forms. You need either a variable of the type itself (say `T`) in memory, or a
@@ -630,108 +595,15 @@ var a/eax: (addr int) <- get p, x
 var a/eax: (addr int) <- get p, y
 ```
 
-You can clear arbitrary types using the `clear-object` function:
+You can clear compound types using the `clear-object` function:
 
 ```
 clear-object var: (addr T)
 ```
 
-Don't clear arrays or streams using `clear-object`; doing so will irreversibly
-make their length 0 as well.
-
-You can shallow-copy arbitrary types using the `copy-object` function:
+You can shallow-copy compound types using the `copy-object` function:
 
 ```
 copy-object src: (addr T), dest: (addr T)
 ```
 
-## Handles for safe access to the heap
-
-We've seen the `addr` type, but it's intended to be short-lived. `addr` values
-should never escape from functions. Function outputs can't be `addr`s,
-function inouts can't include `addr` in their payload type. Finally, you can't
-save `addr` values inside compound `type`s. To do that you need a "fat
-pointer" called a `handle` that is safe to keep around for extended periods
-and ensures it's used safely without corrupting the heap and causing security
-issues or hard-to-debug misbehavior.
-
-To actually _use_ a `handle`, we have to turn it into an `addr` first using
-the `lookup` statement.
-
-```
-var y/reg: (addr T) <- lookup x: (handle T)
-```
-
-Now operate on `y` as usual, safe in the knowledge that you can later recover
-any writes to its payload from `x`.
-
-It's illegal to continue to use an `addr` after a function that reclaims heap
-memory. You have to repeat the lookup from the `handle`. (Luckily Mu doesn't
-implement reclamation yet.)
-
-Having two kinds of addresses takes some getting used to. Do we pass in
-variables by value, by `addr` or by `handle`? In inputs or outputs? Here are 3
-rules of thumb:
-
-  * Functions that need to look at the payload should accept an `(addr ...)`
-    where possible.
-  * Functions that need to treat a handle as a value, without looking at its
-    payload, should accept a `(handle ...)`. Helpers that save handles into
-    data structures are a common example.
-  * Functions that need to allocate memory should accept an `(addr handle ...)`.
-
-Try to avoid mixing these use cases.
-
-If you have a variable `src` of type `(handle ...)`, you can save it inside a
-compound type like this (provided the types match):
-
-```
-var dest/reg: (addr handle T_f) <- get var: (addr T), f
-copy-handle src, dest
-```
-
-Or this:
-
-```
-var dest/reg: (addr handle T) <- index arr: (addr array handle T), n
-copy-handle src, dest
-```
-
-To create handles to non-array types, use `allocate`:
-
-```
-var x: (addr handle T)
-... initialize x ...
-allocate x
-```
-
-To create handles to array types (of potentially dynamic size), use `populate`:
-
-```
-var x: (addr handle array T)
-... initialize x ...
-populate x, 3  # array of 3 T's
-```
-
-## Seams
-
-I said at the start that most instructions map 1:1 to x86 machine code. To
-enforce type- and memory-safety, I was forced to carve out a few exceptions:
-
-* the `index` instruction on arrays, for bounds-checking
-* the `length` instruction on arrays, for translating the array size in bytes
-  into the number of elements.
-* the `lookup` instruction on handles, for validating fat-pointer metadata
-* `var` instructions, to initialize memory
-* byte copies, to initialize memory
-
-If you're curious, [the compiler summary page](http://akkartik.github.io/mu/html/mu_instructions.html)
-has the complete nitty-gritty on how each instruction is implemented. Including
-the above exceptions.
-
-## Conclusion
-
-Anything not allowed here is forbidden, even if the compiler doesn't currently
-detect and complain about it. Please [contact me](mailto:ak@akkartik.com) or
-[report issues](https://github.com/akkartik/mu/issues) when you encounter a
-missing or misleading error message.
diff --git a/tutorial/index.md b/tutorial/index.md
index e23e0542..8d295369 100644
--- a/tutorial/index.md
+++ b/tutorial/index.md
@@ -121,9 +121,10 @@ quotes.
 
 ## Task 4: your first Mu statement
 
-Mu is a statement-oriented language. Read the first section of the [Mu syntax
-description](https://github.com/akkartik/mu/blob/main/mu.md) (until the first
-sub-heading, "functions and calls") to learn a little bit about it.
+Mu is a statement-oriented language. Most statements translate into a single
+instruction to the x86 processor. Read the first two sections of the [Mu
+syntax description](https://github.com/akkartik/mu/blob/main/mu.md) (about
+functions and variables) to learn a little bit about it.
 
 Here's a skeleton of a Mu function that's missing a single statement.
 
@@ -178,10 +179,9 @@ convert 42 to hexadecimal, or [this page on your web browser](http://akkartik.gi
 ## Task 5: variables in registers, variables in memory
 
 We'll now practice managing one variable in a register (like last time) and
-a second one in memory. To prepare for this, reread the first section of the
-[Mu syntax description](https://github.com/akkartik/mu/blob/main/mu.md), and
-then its section on [local variables](https://github.com/akkartik/mu/blob/main/mu.md#local-variables).
-The section on [integer primitives](https://github.com/akkartik/mu/blob/main/mu.md#integer-primitives)
+a second one in memory. To prepare for this, reread the first two sections of
+the [Mu syntax description](https://github.com/akkartik/mu/blob/main/mu.md).
+The section on [integer arithmetic](https://github.com/akkartik/mu/blob/main/mu.md#integer-arithmetic)
 also provides a useful cheatsheet of the different forms of instructions you
 will need.
 
@@ -203,7 +203,7 @@ command as often as you like:
 ./translate tutorial/task5.mu  &&  qemu-system-i386 code.img
 ```
 
-The section on [integer primitives](https://github.com/akkartik/mu/blob/main/mu.md#integer-primitives)
+The section on [integer arithmetic](https://github.com/akkartik/mu/blob/main/mu.md#integer-arithmetic)
 shows that Mu consistently follows a few rules:
 * Instructions that write to a register always have an output before the `<-`.
 * Instructions that use an argument in memory always have it as the first