Soul of a tiny new machine. More thorough tests → More comprehensible and rewrite-friendly software → More resilient society.
Go to file
Kartik K. Agaram bb58e1797e 2214 2015-09-28 23:36:39 -07:00
arc 1310 2015-05-08 09:02:50 -07:00
edit 2206 - fix missing ingredients 2015-09-26 10:32:17 -07:00
enumerate 2155 - `mu` can now load all files in a directory 2015-09-05 11:46:34 -07:00
html 2187 2015-09-12 20:46:48 -07:00
sandbox 2206 - fix missing ingredients 2015-09-26 10:32:17 -07:00
tangle 1746 - load file and run a single test 2015-07-10 10:25:44 -07:00
termbox 2181 - detect shift-tab 2015-09-11 09:05:22 -07:00
000organization.cc 1342 2015-05-11 10:03:39 -07:00
001help.cc 2208 2015-09-26 11:07:50 -07:00
002test.cc 2095 2015-08-28 23:25:21 -07:00
003trace.cc 2202 - don't let editor die on syntax errors 2015-09-15 19:48:53 -07:00
003trace.test.cc 1844 - explicitly end each trace line 2015-07-25 00:02:20 -07:00
010vm.cc 2201 2015-09-15 00:38:00 -07:00
011load.cc 2193 2015-09-14 21:38:53 -07:00
012transform.cc 2154 - check types only after loading all layers 2015-09-05 11:23:20 -07:00
013literal_string.cc 2194 2015-09-14 21:39:07 -07:00
014literal_noninteger.cc 1876 2015-07-28 16:38:37 -07:00
020_check_type_by_instruction.cc 2214 2015-09-28 23:36:39 -07:00
021run.cc 2214 2015-09-28 23:36:39 -07:00
022arithmetic.cc 2214 2015-09-28 23:36:39 -07:00
023boolean.cc 2214 2015-09-28 23:36:39 -07:00
024jump.cc 2214 2015-09-28 23:36:39 -07:00
025compare.cc 2214 2015-09-28 23:36:39 -07:00
029tools.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
030container.cc 2154 - check types only after loading all layers 2015-09-05 11:23:20 -07:00
031address.cc 2213 - start moving type checks out of 'run' 2015-09-28 22:21:15 -07:00
032array.cc 2214 2015-09-28 23:36:39 -07:00
033exclusive_container.cc 2115 2015-08-30 22:13:22 -07:00
034call.cc 2095 2015-08-28 23:25:21 -07:00
035call_ingredient.cc 2205 - warn on missing ingredient 2015-09-18 17:21:28 -07:00
036call_reply.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
037recipe.cc 2137 2015-09-03 18:58:08 -07:00
038scheduler.cc 2202 - don't let editor die on syntax errors 2015-09-15 19:48:53 -07:00
039wait.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
040brace.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
041jump_target.cc 2138 - warn on jump to an ambiguous label 2015-09-04 00:22:36 -07:00
042name.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
043new.cc 2204 2015-09-16 10:09:16 -07:00
044space.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
045space_surround.cc 1868 - start using naked literals everywhere 2015-07-28 14:33:22 -07:00
046closure_name.cc 2095 2015-08-28 23:25:21 -07:00
047global.cc 1868 - start using naked literals everywhere 2015-07-28 14:33:22 -07:00
048_check_type_by_name.cc 2214 2015-09-28 23:36:39 -07:00
050scenario.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
051scenario_test.mu 1990 - extra ingredient for 'trace' depth 2015-08-13 16:53:32 -07:00
052tangle.cc 2149 2015-09-05 09:44:35 -07:00
053continuation.cc 2137 2015-09-03 18:58:08 -07:00
060string.mu 2205 - warn on missing ingredient 2015-09-18 17:21:28 -07:00
061channel.mu 2009 2015-08-15 05:33:02 -07:00
062array.mu 1883 - type-deducing in more .mu files 2015-07-29 14:37:57 -07:00
063list.mu 2095 2015-08-28 23:25:21 -07:00
064random.cc 1923 2015-08-02 22:18:19 -07:00
065duplex_list.mu 2122 - undo/redo should now be working 2015-09-01 13:38:01 -07:00
066stream.mu 1883 - type-deducing in more .mu files 2015-07-29 14:37:57 -07:00
070display.cc 2113 - stop updating entire screen on tb_present() 2015-08-29 23:51:18 -07:00
071print.mu 2113 - stop updating entire screen on tb_present() 2015-08-29 23:51:18 -07:00
072scenario_screen.cc 2199 - stop printing numbers in scientific notation 2015-09-14 23:37:12 -07:00
073scenario_screen_test.mu 1962 2015-08-09 12:26:31 -07:00
074console.mu 2041 2015-08-20 15:14:59 -07:00
075scenario_console.cc 2149 2015-09-05 09:44:35 -07:00
076scenario_console_test.mu 1616 - merging keyboard and events into 'console' 2015-06-21 23:27:54 -07:00
080trace_browser.cc 2095 2015-08-28 23:25:21 -07:00
081run_interactive.cc 2202 - don't let editor die on syntax errors 2015-09-15 19:48:53 -07:00
082persist.cc 2196 2015-09-14 21:44:15 -07:00
999spaces.cc 1842 - get layers building again after 2 weeks 2015-07-24 21:41:26 -07:00
Readme.md 2176 2015-09-06 16:52:48 -07:00
build_and_test_until 2197 2015-09-14 23:10:31 -07:00
callcc.mu 1880 - switch .mu files to new type-deducing idiom 2015-07-29 01:23:22 -07:00
cannot_write_tests_for 2192 2015-09-14 13:53:53 -07:00
channel.mu 1880 - switch .mu files to new type-deducing idiom 2015-07-29 01:23:22 -07:00
chessboard.mu 2095 2015-08-28 23:25:21 -07:00
console.mu 2132 - support for ctrl + arrow keys 2015-09-02 19:18:37 -07:00
counters.mu 1880 - switch .mu files to new type-deducing idiom 2015-07-29 01:23:22 -07:00
display.mu 2095 2015-08-28 23:25:21 -07:00
factorial.mu 1880 - switch .mu files to new type-deducing idiom 2015-07-29 01:23:22 -07:00
fork.mu 1868 - start using naked literals everywhere 2015-07-28 14:33:22 -07:00
global.mu 1868 - start using naked literals everywhere 2015-07-28 14:33:22 -07:00
index.html 2177 2015-09-07 10:37:27 -07:00
makefile 2067 2015-08-24 14:19:52 -07:00
mu 1706 - automatically recompile mu when necessary 2015-07-04 20:53:26 -07:00
mu.vim 2174 2015-09-06 16:32:41 -07:00
relayout 1276 - make C++ version the default 2015-05-05 21:17:24 -07:00
screen.mu 1868 - start using naked literals everywhere 2015-07-28 14:33:22 -07:00
tangle.mu 1880 - switch .mu files to new type-deducing idiom 2015-07-29 01:23:22 -07:00
test_all_layers 2158 2015-09-05 12:39:21 -07:00
update_github_page 1377 - handle github errors 2015-05-14 16:05:24 -07:00
update_html 2177 2015-09-07 10:37:27 -07:00
vimrc.vim 1555 - mu.vim: distinguish control-flow from constants 2015-06-12 22:32:28 -07:00
x.mu 1868 - start using naked literals everywhere 2015-07-28 14:33:22 -07:00

Readme.md

Mu: making programs easier to understand in the large

Imagine a world where you can:

  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.

  2. Record your program as it runs, and easily convert arbitrary logs of runs into reproducible automatic tests.

  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.

  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 without also chasing after the others. The core mechanism enabling them all is recording manual tests right after the first time you perform them:

  • keyboard input
  • printing to screen
  • website layout
  • 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.

(More details.)

Taking Mu for a spin

Mu is currently implemented in C++ and requires a unix-like environment. It's been tested on ubuntu 14.04 on x86, x86_64 and ARMv7 with recent versions of gcc and clang. Since it uses no recent language features and has no exotic dependencies, it should work with most reasonable versions, compilers or processors.

Running Mu will always recompile it if necessary:

$ cd mu
$ ./mu

As a sneak peek, here's how you compute factorial in Mu:

code example

Mu functions or 'recipes' are lists of instructions, one to a line. Each instruction operates on some ingredients and returns some products.

[products] <- instruction [ingredients]

Result and ingredient reagents have to be variables. But you can have any number of them. In particular you can have any number of products. For example, you can perform integer division as follows:

quotient:number, remainder:number <- divide-with-remainder 11, 3

Each reagent can provide a name as well as its type separated by a colon. You only have to specify the type the first time you mention a name, but you can be more explicit if you choose. Types can be multiple words, like:

x:array:number:3  # x is an array of 3 numbers
y:list:number  # y is a list of numbers

Recipes load their ingredients from their caller using the next-ingredient instruction, and return products using reply.

Try out the factorial program now:

$ ./mu factorial.mu
result: 120  # factorial of 5

You can also run its unit tests:

$ ./mu test factorial.mu

Here's what one of the tests inside factorial.mu looks like:

test example

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):

screen test

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.


The name of a reagent is for humans, but what the computer needs to access it is its address. Mu maps names to addresses for you like in other languages, but in a more transparent, lightweight, hackable manner. This instruction:

z:number <- add x:number, y:number

might turn into this:

3:number <- add 1:number, 2:number

You shouldn't rely on the specific address Mu chooses for a variable, but it will be unique (other variables won't clobber it) and consistent (all mentions of the name will map to the same address inside a recipe).

Things get more complicated when your recipes call other recipes. Mu doesn't preserve uniqueness of addresses across recipes, so you need to organize your names into spaces. At the start of each recipe (like factorial above), set its default space:

local-scope

or

new-default-space

or

default-space:address:array:location <- new location:type, 30/capacity

Without one of these lines, all variables in the recipe will be global, something you rarely want. (Luckily, this is also the sort of mistake that will be easily caught by tests.) With this line, all addresses in your recipe will by default refer to one of the (30, in the final case) slots inside this local space. (If you choose the last, most explicit option and need more than 30 slots, mu will complain asking you to increase capacity.)

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:

0:address:array:location <- next-ingredient

Once you've chained spaces together, you can access variables in them by adding a 'space' property:

3:number/space:1

This reagent is the number in slot 3 of the space chained in slot 0 of the 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.


You can append arbitrary properties to reagents besides types and spaces. Just separate them with slashes.

x:array:number:3/uninitialized
y:string/tainted:yes
z:list:number/assign-once:true/assigned:false

Most properties are meaningless to Mu, and it'll silently skip them when 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 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.

You can imagine each reagent as a table, rows separated by slashes, columns within a row separated by colons. So the last example above would become something like this:

z           : list   : integer  /
assign-once : true              /
assigned    : false

An alternative way to define factorial is by inserting labels and later inserting code at them.

recipe factorial [
  local-scope
  n:number <- next-ingredient
  {
    +base-case:
  }
  +recursive-case:
]

after +base-case [
  # if n=0 return 1
  zero?:boolean <- equal n, 0
  break-unless zero?
  reply 1
]

after +recursive-case [
  # return n * factorial(n-1)
  x:number <- subtract n, 1
  subresult:number <- factorial x
  result:number <- multiply subresult, n
  reply result
]

(You'll find this version in tangle.mu.)

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 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 just flat lists of instructions.


Another example, this time with concurrency.

recipe main [
  start-running thread2:recipe
  {
    $print 34
    loop
  }
]

recipe thread2 [
  {
    $print 35
    loop
  }
]
$ ./mu fork.mu

Notice that it repeatedly prints either '34' or '35' at random. Hit ctrl-c to stop.

Yet another example forks two 'routines' that communicate over a channel:

$ ./mu channel.mu
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.

Channels are the unit of synchronization in Mu. Blocking on channels are the 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 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).


If you're still reading, here are some more things to check out:

a) Look at the chessboard program 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. 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. Tell me how successful my efforts are.

c) Try running the tests:

$ ./mu test

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.

d) Try out the programming environment:

$ ./mu test edit  # takes about 30s; shouldn't show any failures
$ ./mu edit

Screenshot:

programming environment

You write recipes on the left and try them out in sandboxes on the right. Hit F4 to rerun all sandboxes with the latest version of the code. More details: http://akkartik.name/post/mu. Beware, it won't save your edits by default. But if you create a sub-directory called lesson/ under mu/ it will. If you turn that directory into a git repo with git init, it will also back up each version you try out.

Once you have a sandbox you can click on its result to mark it as expected:

expected result

Later if the result changes it'll be flagged in red to draw your attention to it. Thus, manually tested sandboxes become reproducible automated tests.

unexpected result

Another feature: Clicking on the code in a sandbox expands its trace for you to browse. To add to the trace, use stash. For example:

stash [first ingredient is ], x

Invaluable for understanding complex control flow without cluttering up the screen.

The next major milestone on Mu's roadmap is support for recording and faking console input to a sandbox, so that you can type in an input once and have it replay everytime you hit F4. Once this support is in place it will be easy to generalize to more interfaces, like requesting urls over a network or reading files on a disk.

Credits

Mu builds on many ideas that have come before, especially:

  • Peter Naur for articulating the paramount problem of programming: communicating a codebase to others;
  • Christopher Alexander and Richard Gabriel 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;
  • Donald Knuth's literate programming for liberating "code for humans to read" from the tyranny of compiler order;
  • David Parnas and others for highlighting the value of separating concerns and stepwise refinement;
  • Lisp for showing the power of 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;
  • Python doctest for exemplifying interactive documentation that doubles as tests;
  • ReStructuredText and its antecedents 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.