mu/Readme.md

409 lines
14 KiB
Markdown
Raw Normal View History

2014-11-29 15:52:08 +00:00
**Mu: making programs easier to understand in the large**
Imagine a world where you can:
2014-11-26 15:04:04 +00:00
1. think of a tiny improvement to a program you use, clone its sources,
orient yourself on its organization and make your tiny improvement, all in a
single afternoon.
2014-11-26 15:04:04 +00:00
2. Record your program as it runs, and easily convert arbitrary logs of runs
into reproducible automatic tests.
2014-11-26 15:04:04 +00:00
3. Answer arbitrary what-if questions about a codebase by trying out changes
and seeing what tests fail, confident that *every* scenario previous authors
have considered has been encoded as a test.
2014-11-30 00:22:11 +00:00
4. Run first simple and successively more complex versions to stage your
learning.
I think all these abilities might be strongly correlated; not only are they
achievable with a few common concepts, but you can't easily attack one of them
2014-11-25 09:28:17 +00:00
without also chasing after the others. The core mechanism enabling them all is
recording manual tests right after the first time you perform them:
2014-11-26 15:04:04 +00:00
* keyboard input
* printing to screen
2014-12-13 02:02:30 +00:00
* website layout
2014-11-26 15:04:04 +00:00
* disk filling up
* performance metrics
* race conditions
* fault tolerance
* ...
I hope to attain this world by creating a comprehensive library of fakes and
hooks for the entire software stack, at all layers of abstraction (programming
language, OS, standard libraries, application libraries).
To reduce my workload and get to a proof-of-concept quickly, this is a very
*alien* software stack. I've stolen ideas from lots of previous systems, but
it's not like anything you're used to. The 'OS' will lack virtual memory, user
accounts, any unprivileged mode, address space isolation, and many other
features.
To avoid building a compiler I'm going to do all my programming in (virtual
machine) assembly. To keep assembly from getting too painful I'm going to
pervasively use one trick: load-time directives to let me order code however I
want, and to write boilerplate once and insert it in multiple places. If
you're familiar with literate programming or aspect-oriented programming,
these directives may seem vaguely familiar. If you're not, think of them as a
richer interface for function inlining.
Trading off notational convenience for tests may seem regressive, but I
suspect high-level languages aren't particularly helpful in understanding
large codebases. No matter how good a notation is, it can only let you see a
tiny fraction of a large program at a time. Logs, on the other hand, can let
you zoom out and take in an entire *run* at a glance, making them a superior
unit of comprehension. If I'm right, it makes sense to prioritize the right
*tactile* interface for working with and getting feedback on large programs
before we invest in the *visual* tools for making them concise.
2015-03-09 23:00:05 +00:00
([More details.](http://akkartik.name/about))
**Taking Mu for a spin**
2015-06-10 17:22:02 +00:00
Mu is currently implemented in C++ and requires a unix-like environment. It's
been tested on ubuntu 14.04 with recent versions of gcc and clang, but should
work with earlier versions going quite far back. It has no other dependencies
that aren't taken for granted in unix. In spite of needing C++ it uses no
advanced features and is designed to eventually bootstrap using an assembler
written directly in machine code. Currently you build it like so:
2014-11-26 15:04:04 +00:00
```shell
$ cd mu
2015-07-05 17:35:26 +00:00
$ ./mu
2014-11-25 17:17:18 +00:00
```
2015-07-05 17:35:26 +00:00
Running mu will always recompile it if necessary. In this case it has no other
work to do, so it'll show you a hopefully helpful message after it's done
compiling.
As a sneak peek, here's how you compute factorial in Mu:
2015-07-14 03:44:19 +00:00
![code example](html/factorial.png)
Mu functions or 'recipes' are lists of instructions, one to a line. Each
instruction operates on some *ingredients* and returns some *results*.
2014-11-25 17:17:18 +00:00
```
[results] <- instruction [ingredients]
2014-11-25 17:17:18 +00:00
```
2015-05-11 19:00:50 +00:00
Result and ingredient *reagents* have to be variables. But you can have any
number of them. In particular you can have any number of results. For example,
you can perform integer division as follows:
2014-11-26 16:30:26 +00:00
```
quotient:number, remainder:number <- divide-with-remainder 11:literal, 3:literal
2014-11-26 16:30:26 +00:00
```
Each reagent provides its name as well as its type separated by a colon. Types
2014-12-14 21:21:32 +00:00
can be multiple words, like:
```nim
x:array:number:3 # x is an array of 3 numbers
y:list:number # y is a list of numbers
2014-12-14 21:21:32 +00:00
```
Recipes load their ingredients from their caller using the *next-ingredient*
instruction, and return results using *reply*.
2015-01-03 02:57:49 +00:00
Try out the factorial program now:
```shell
$ ./mu factorial.mu
result: 120 # factorial of 5
```
You can also run its unit tests:
```shell
$ ./mu test factorial.mu
```
Here's what one of the tests inside `factorial.mu` looks like:
2015-07-14 03:46:27 +00:00
![test example](html/factorial-test.png)
Every test conceptually spins up a really lightweight virtual machine, so you
can do things like check the value of specific locations in memory. You can
also print to screen and check that the screen contains what you expect at the
end of a test. For example, `chessboard.mu` checks the initial position of a
game of chess (delimiting the edges of the screen with periods):
2015-07-14 03:46:27 +00:00
![screen test](html/chessboard-test.png)
Similarly you can fake the keyboard to pretend someone typed something:
```
assume-keyboard [a2-a4]
```
As we add a file system, graphics, audio, network support and so on, we'll
augment scenarios with corresponding abilities to use them inside tests.
2015-01-03 02:57:49 +00:00
---
The name of a reagent is for humans, but what the computer needs to access it is
2015-01-03 02:57:49 +00:00
its address. Mu maps names to addresses for you like in other languages, but
2015-01-03 03:04:15 +00:00
in a more transparent, lightweight, hackable manner. This instruction:
2015-01-03 02:57:49 +00:00
```nim
z:number <- add x:number, y:number
2015-01-03 02:57:49 +00:00
```
might turn into this:
```nim
3:number <- add 1:number, 2:number
2015-01-03 02:57:49 +00:00
```
You shouldn't rely on the specific address Mu chooses for a variable, but it
2015-01-03 02:57:49 +00:00
will be unique (other variables won't clobber it) and consistent (all mentions
of the name will map to the same address inside a function).
Things get more complicated when your functions call other functions. Mu
2015-03-20 03:50:00 +00:00
doesn't preserve uniqueness of addresses across functions, so you need to
organize your names into spaces. At the start of each function (like
`factorial` above), set its *default space*:
2015-01-03 02:57:49 +00:00
```nim
2015-07-14 03:44:19 +00:00
new-default-space
2015-01-03 02:57:49 +00:00
```
Without this line, all variables in the function will be *global*, something
you rarely want. (Luckily, this is also the sort of mistake that will be
easily caught by tests. Later we'll automatically generate this boilerplate.)
*With* this line, all addresses in your function will by default refer to one
of the 30 slots inside this local space. (If you need more, mu will complain.)
2015-01-03 02:57:49 +00:00
Spaces can do more than just implement local variables. You can string them
together, pass them around, return them from functions, share them between
parallel routines, and much else. However, any function receiving a space has
to know the names and types of variables in it, so any instruction should
always receive spaces created by the same function, no matter how many times
it's run. (If you're familiar with lexical scope, this constraint is
identical to it.)
To string two spaces together, write one into slot 0 of the other. This
instruction chains a space received from its caller:
```nim
0:address:array:location <- next-ingredient
2015-01-03 02:57:49 +00:00
```
Once you've chained spaces together, you can access variables in them by
adding a 'space' property:
2015-01-03 02:57:49 +00:00
```nim
3:number/space:1
2015-01-03 02:57:49 +00:00
```
This reagent is the integer in slot 3 of the space chained in slot 0 of the
2015-01-03 02:57:49 +00:00
default space. We usually call it slot 3 in the 'next space'. `/space:2` would
be the next space of the next space, and so on.
See `counters.mu` for an example of managing multiple accumulators at once
without allowing them to clobber each other. This is a classic example of the
sorts of things closures and objects are useful for in other languages. Spaces
in Mu provide the same functionality.
2015-01-03 02:57:49 +00:00
---
You can append arbitrary properties to reagents besides types and spaces. Just
2015-01-03 02:57:49 +00:00
separate them with slashes.
2014-12-14 21:21:32 +00:00
```nim
x:array:number:3/uninitialized
2014-12-14 21:21:32 +00:00
y:string/tainted:yes
z:list:number/assign-once:true/assigned:false
2014-12-14 21:21:32 +00:00
```
Most properties are meaningless to Mu, and it'll silently skip them when
2015-01-03 02:57:49 +00:00
running, but they are fodder for *meta-programs* to check or modify your
programs, a task other languages typically hide from their programmers. For
example, where other programmers are restricted to the checks their type
2014-12-14 21:21:32 +00:00
system permits and forces them to use, you'll learn to create new checks that
make sense for your specific program. If it makes sense to perform different
checks in different parts of your program, you'll be able to do that.
To summarize: Mu instructions have multiple ingredient and result reagents.
Values can have multiple rows separated by slashes, and rows can have multiple
columns separated by colons. The address of a reagent is always in the very
2015-01-08 03:34:23 +00:00
first column of the first row of its 'table'. You can visualize the last
example above as:
```
z : list : integer /
assign-once : true /
assigned : false
```
---
2015-01-03 02:57:49 +00:00
An alternative way to define factorial is by inserting *labels* and later
2014-11-26 16:30:26 +00:00
inserting code at them.
```nim
recipe factorial [
2015-07-14 03:44:19 +00:00
new-default-space
n:number <- next-ingredient
2014-11-26 16:30:26 +00:00
{
2015-05-06 08:02:13 +00:00
+base-case:
2014-11-26 16:30:26 +00:00
}
2015-05-06 08:02:13 +00:00
+recursive-case:
2014-11-26 16:30:26 +00:00
]
2015-05-06 08:02:13 +00:00
after +base-case [
# if n=0 return 1
zero?:boolean <- equal n:number, 0:literal
2014-12-14 21:21:32 +00:00
break-unless zero?:boolean
reply 1:literal
2014-11-26 16:30:26 +00:00
]
2015-05-06 08:02:13 +00:00
after +recursive-case [
# return n * factorial(n-1)
x:number <- subtract n:number, 1:literal
subresult:number <- factorial x:number
result:number <- multiply subresult:number, n:number
reply result:number
2014-11-26 16:30:26 +00:00
]
```
(You'll find this version in `tangle.mu`.)
2015-05-11 19:00:50 +00:00
Any instruction without ingredients or products that starts with a
non-alphanumeric character is a label. By convention we use '+' to indicate
label names.
This is a good time to point out that `{` and `}` are also just labels in Mu
2015-02-02 20:19:47 +00:00
syntax, and that `break` and `loop` get rewritten as jumps to just after the
enclosing `}` and `{` respectively. This gives us a simple sort of structured
programming without adding complexity to the parser -- Mu functions remain
2015-02-02 20:19:47 +00:00
just flat lists of instructions.
2014-11-26 16:30:26 +00:00
---
2014-11-01 23:34:33 +00:00
Another example, this time with concurrency.
2015-05-11 16:59:29 +00:00
```
recipe main [
start-running thread2:recipe
{
$print 34:literal
loop
}
]
recipe thread2 [
{
$print 35:literal
loop
}
]
```
2014-11-26 15:04:04 +00:00
```shell
$ ./mu fork.mu
2014-11-25 17:17:18 +00:00
```
2014-11-01 23:34:33 +00:00
Notice that it repeatedly prints either '34' or '35' at random. Hit ctrl-c to
stop.
2014-11-26 16:48:06 +00:00
Yet another example forks two 'routines' that communicate over a channel:
2014-11-25 09:25:20 +00:00
2014-11-26 15:04:04 +00:00
```shell
$ ./mu channel.mu
2014-11-25 09:25:20 +00:00
produce: 0
produce: 1
produce: 2
produce: 3
consume: 0
consume: 1
consume: 2
produce: 4
consume: 3
consume: 4
# The exact order above might shift over time, but you'll never see a number
# consumed before it's produced.
2014-11-25 17:17:18 +00:00
```
2014-11-25 09:25:20 +00:00
Channels are the unit of synchronization in Mu. Blocking on channels are the
2014-11-25 09:25:20 +00:00
only way tasks can sleep waiting for results. The plan is to do all I/O over
channels that wait for data to return.
Routines are expected to communicate purely by message passing, though nothing
stops them from sharing memory since all routines share a common address
space. However, idiomatic Mu will make it hard to accidentally read or clobber
2014-11-25 09:25:20 +00:00
random memory locations. Bounds checking is baked deeply into the semantics,
and pointer arithmetic will be mostly forbidden (except inside the memory
allocator and a few other places).
---
2015-05-16 06:01:37 +00:00
If you're still reading, here are some more things to check out:
a) Look at the [chessboard program](http://akkartik.github.io/mu/html/chessboard.mu.html)
for a more complex example where I write tests showing blocking reads from the
keyboard and what gets printed to the screen -- things we don't typically
associate with automated tests.
b) Try skimming the [colorized source code](http://akkartik.github.io/mu). I'd
like it to eventually be possible to get a pretty good sense for how things
work just by skimming the files in order, skimming the top of each file and
ignoring details lower down. I'd love to hear feedback about how successful my
efforts are.
c) Try running the tests:
2014-11-26 15:04:04 +00:00
```shell
$ ./mu test
2014-11-25 17:17:18 +00:00
```
2014-11-26 16:30:26 +00:00
You might also want to peek in the `.traces` directory, which automatically
includes logs for each test showing you just how it ran on my machine. If Mu
eventually gets complex enough that you have trouble running examples, these
logs might help figure out if my system is somehow different from yours or if
I've just been insufficiently diligent and my documentation is out of date.
The immediate goal of Mu is to build up towards an environment for parsing and
visualizing these traces in a hierarchical manner, and to easily turn traces
into reproducible tests by flagging inputs entering the log and outputs
leaving it. The former will have to be faked in, and the latter will want to
be asserted on, to turn a trace into a test.
2014-11-29 15:52:08 +00:00
**Credits**
Mu builds on many ideas that have come before, especially:
2014-11-26 15:04:04 +00:00
- [Peter Naur](http://alistair.cockburn.us/ASD+book+extract%3A+%22Naur,+Ehn,+Musashi%22)
for articulating the paramount problem of programming: communicating a
codebase to others;
2014-11-26 15:04:04 +00:00
- [Christopher Alexander](http://www.amazon.com/Notes-Synthesis-Form-Harvard-Paperbacks/dp/0674627512)
and [Richard Gabriel](http://dreamsongs.net/Files/PatternsOfSoftware.pdf) for
the intellectual tools for reasoning about the higher order design of a
codebase;
- Unix and C for showing us how to co-evolve language and OS, and for teaching
the (much maligned, misunderstood and underestimated) value of concise
*implementation* in addition to a clean interface;
2014-11-26 15:04:04 +00:00
- Donald Knuth's [literate programming](http://www.literateprogramming.com/knuthweb.pdf)
for liberating "code for humans to read" from the tyranny of compiler order;
2014-11-26 15:04:04 +00:00
- [David Parnas](http://www.cs.umd.edu/class/spring2003/cmsc838p/Design/criteria.pdf)
and others for highlighting the value of separating concerns and stepwise
refinement;
2014-11-26 15:04:04 +00:00
- [Lisp](http://www.paulgraham.com/rootsoflisp.html) for showing the power of
2015-06-24 22:46:30 +00:00
dynamic languages, late binding and providing the right primitives *a la
carte*, especially lisp macros;
- The folklore of debugging by print and the trace facility in many lisp
systems;
- Automated tests for showing the value of developing programs inside an
elaborate harness;
2014-11-26 15:04:04 +00:00
- [Python doctest](http://docs.python.org/2/library/doctest.html) for
exemplifying interactive documentation that doubles as tests;
2014-11-26 15:04:04 +00:00
- [ReStructuredText](https://en.wikipedia.org/wiki/ReStructuredText)
and [its antecedents](https://en.wikipedia.org/wiki/Setext) for showing that
markup can be clean;
- BDD for challenging us all to write tests at a higher level;
- JavaScript and CSS for demonstrating the power of a DOM for complex
structured documents.