Soul of a tiny new machine. More thorough tests → More comprehensible and rewrite-friendly software → More resilient society.
Go to file
Kartik K. Agaram d0442e5e49 3166 2016-08-12 09:08:15 -07:00
arc 3166 2016-08-12 09:08:15 -07:00
edit 3111 2016-07-14 05:42:03 -07:00
enumerate 2665 2016-02-17 00:39:52 -08:00
html 3158 2016-07-27 22:51:11 -07:00
sandbox 3101 - purge .traces/ dir from repo history 2016-07-05 00:53:12 -07:00
tangle 3101 - purge .traces/ dir from repo history 2016-07-05 00:53:12 -07:00
termbox 2572 2016-01-19 17:54:24 -08:00
.travis.yml 3128 2016-07-22 14:37:56 -07:00
000organization.cc 3027 2016-06-02 10:40:06 -07:00
001help.cc 3137 2016-07-22 23:36:19 -07:00
002test.cc 3165 2016-08-10 09:12:11 -07:00
003trace.cc 3101 - purge .traces/ dir from repo history 2016-07-05 00:53:12 -07:00
003trace.test.cc 2547 2015-12-24 15:36:12 -08:00
010vm.cc 3101 - purge .traces/ dir from repo history 2016-07-05 00:53:12 -07:00
011load.cc 3120 2016-07-21 19:22:03 -07:00
012transform.cc 2882 - warn if programmer overuses transform_all() 2016-04-28 17:36:30 -07:00
013update_operation.cc 2990 2016-05-20 22:11:34 -07:00
014literal_string.cc 3081 2016-06-30 10:45:26 -07:00
015literal_noninteger.cc 2803 2016-03-21 02:25:52 -07:00
016dilated_reagent.cc 2990 2016-05-20 22:11:34 -07:00
017parse_tree.cc 2971 2016-05-17 18:25:26 -07:00
020run.cc 3137 2016-07-22 23:36:19 -07:00
021check_instruction.cc 3120 2016-07-21 19:22:03 -07:00
022arithmetic.cc 3120 2016-07-21 19:22:03 -07:00
023boolean.cc 3120 2016-07-21 19:22:03 -07:00
024jump.cc 3120 2016-07-21 19:22:03 -07:00
025compare.cc 3120 2016-07-21 19:22:03 -07:00
026call.cc 3120 2016-07-21 19:22:03 -07:00
027call_ingredient.cc 3120 2016-07-21 19:22:03 -07:00
028call_reply.cc 3136 2016-07-22 22:42:34 -07:00
029tools.cc 3163 2016-08-09 16:48:49 -07:00
030container.cc 3143 2016-07-24 02:19:40 -07:00
031merge.cc 3120 2016-07-21 19:22:03 -07:00
032array.cc 3120 2016-07-21 19:22:03 -07:00
033exclusive_container.cc 3123 2016-07-22 13:55:25 -07:00
034address.cc 3120 2016-07-21 19:22:03 -07:00
035lookup.cc 3119 2016-07-21 12:04:55 -07:00
036refcount.cc 3124 2016-07-22 14:00:48 -07:00
037abandon.cc 3070 2016-06-28 19:33:43 -07:00
038new_text.cc 3079 2016-06-29 16:08:13 -07:00
039location_array.cc 3120 2016-07-21 19:22:03 -07:00
040brace.cc 3083 2016-07-01 22:34:50 -07:00
041jump_target.cc 2990 2016-05-20 22:11:34 -07:00
042name.cc 3120 2016-07-21 19:22:03 -07:00
043space.cc 2990 2016-05-20 22:11:34 -07:00
044space_surround.cc 2990 2016-05-20 22:11:34 -07:00
045closure_name.cc 3108 2016-07-10 21:47:24 -07:00
046global.cc 2990 2016-05-20 22:11:34 -07:00
047check_type_by_name.cc 3108 2016-07-10 21:47:24 -07:00
050scenario.cc 3161 2016-08-09 16:04:02 -07:00
051scenario_test.mu 3015 - more symbolic names in tests 2016-05-26 16:46:38 -07:00
052tangle.cc 3108 2016-07-10 21:47:24 -07:00
053recipe_header.cc 3120 2016-07-21 19:22:03 -07:00
054static_dispatch.cc 3120 2016-07-21 19:22:03 -07:00
055shape_shifting_container.cc 3104 2016-07-06 14:25:23 -07:00
056shape_shifting_recipe.cc 3120 2016-07-21 19:22:03 -07:00
057immutable.cc 3120 2016-07-21 19:22:03 -07:00
060rewrite_literal_string.cc 3113 2016-07-20 09:27:31 -07:00
061text.mu 3155 2016-07-27 22:20:12 -07:00
062rewrite_stash.cc 3144 2016-07-24 15:04:41 -07:00
063array.mu 3055 2016-06-13 16:25:45 -07:00
064list.mu 3058 - 'remove' for lists 2016-06-16 17:02:08 -07:00
065duplex_list.mu 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
066stream.mu 3156 2016-07-27 22:33:18 -07:00
067random.cc 3157 - more testable 'random' interface 2016-07-27 22:47:04 -07:00
068random.mu 3157 - more testable 'random' interface 2016-07-27 22:47:04 -07:00
069hash.cc 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
070table.mu 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
071recipe.cc 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
072scheduler.cc 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
073wait.cc 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
074deep_copy.cc 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
075channel.mu 3154 - reorg before making 'random' more testable 2016-07-27 21:58:47 -07:00
080display.cc 3164 2016-08-09 20:09:58 -07:00
081print.mu 3015 - more symbolic names in tests 2016-05-26 16:46:38 -07:00
082scenario_screen.cc 3126 2016-07-22 14:13:25 -07:00
083scenario_screen_test.mu 3020 - names in chessboard tests 2016-05-26 21:43:38 -07:00
084console.mu 2870 - fix the final long-standing failing test 2016-04-27 00:10:20 -07:00
085scenario_console.cc 3083 2016-07-01 22:34:50 -07:00
086scenario_console_test.mu 2864 - replace all address:shared with just address 2016-04-24 11:54:30 -07:00
087file.cc 2788 2016-03-16 17:06:01 -07:00
090trace_browser.cc 2773 - switch to 'int' 2016-03-13 20:26:47 -07:00
091run_interactive.cc 3160 2016-08-09 09:39:24 -07:00
092persist.cc 3120 2016-07-21 19:22:03 -07:00
998check_type_pointers.cc 2463 - make edit/ robust to errors in client code 2015-11-19 09:22:46 -08:00
999spaces.cc 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
Readme.md 3101 - purge .traces/ dir from repo history 2016-07-05 00:53:12 -07:00
build_and_test_until 2964 2016-05-15 10:42:16 -07:00
build_until 2946 2016-05-08 23:37:07 -07:00
cannot_write_tests_for 3140 2016-07-23 14:20:26 -07:00
channel.mu 2880 2016-04-27 22:28:55 -07:00
chessboard.mu 3020 - names in chessboard tests 2016-05-26 21:43:38 -07:00
console.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
counters.mu 2864 - replace all address:shared with just address 2016-04-24 11:54:30 -07:00
display.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
example1.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
exuberant-ctags-rc 3066 2016-06-24 18:17:11 -07:00
factorial.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
fork.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
git_log_filtered 3101 - purge .traces/ dir from repo history 2016-07-05 00:53:12 -07:00
global.mu 2864 - replace all address:shared with just address 2016-04-24 11:54:30 -07:00
index.html 3158 2016-07-27 22:51:11 -07:00
lambda-to-mu.mu 3159 - pause the lambda->mu translator for a bit 2016-08-07 13:52:48 -07:00
makefile 3107 - just always support small VPS servers 2016-07-10 10:05:56 -07:00
mu 3137 2016-07-22 23:36:19 -07:00
mu.vim 2838 2016-04-16 08:02:17 -07:00
new_lesson 2995 2016-05-21 11:00:33 -07:00
nqueens.mu 2996 2016-05-21 17:44:53 -07:00
relayout 1276 - make C++ version the default 2015-05-05 21:17:24 -07:00
screen.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
static_dispatch.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00
tangle.mu 2746 2016-03-09 14:54:41 -08:00
test_layers 3029 2016-06-02 13:17:21 -07:00
update_github_page 1377 - handle github errors 2015-05-14 16:05:24 -07:00
update_html 2745 2016-03-09 03:40:41 -08:00
vimrc.vim 1555 - mu.vim: distinguish control-flow from constants 2015-06-12 22:32:28 -07:00
x.mu 2735 - define recipes using 'def' 2016-03-08 01:46:47 -08:00

Readme.md

Mu explores ways to turn arbitrary manual tests into reproducible automated tests. Hoped-for benefits:

  1. Projects release with confidence without requiring manual QA or causing regressions for their users.

  2. Open source projects become easier for outsiders to comprehend, since they can more confidently try out changes with the knowledge that they'll get rapid feedback if they break something. As a result projects become more rewrite-friendly: it's easier to leave your project's historical accidents and other baggage behind if you can be confident of not causing regressions.

  3. It becomes easier to teach programming by emphasizing tests far earlier than we do today.

In this quest, Mu is currently experimenting with the following mechanisms:

  1. New, testable interfaces for the operating system. Currently manual tests are hard to automate because a file you assumed might vanish, the network might go down, etc. To make manual tests reproducible it suffices to improve the 15 or so OS syscalls through which a computer talks to the outside world. We have to allow programs to transparently write to a fake screen, read from a fake disk/network, etc. In Mu, printing to screen explicitly takes a screen object, so it can be called on the real screen, or on a fake screen inside tests, so that we can then check the expected state of the screen at the end of a test. Here's a test for a little text-mode chessboard program in Mu (delimiting the edge of the 'screen' with dots):
          a screen test
    I'm building up similarly dependency-injected interfaces to the keyboard, mouse, touch screen, disk, network, etc.

  2. Support for testing side-effects like performance, deadlock-freedom, race-freeness, memory usage, etc. Mu's white-box tests can check not just the results of a function call, but also the presence or absence of specific events in the log of its progress. For example, here's a test that our string-comparison function doesn't scan individual characters unless it has to:
          white-box test
    Another example: if a sort function logs each swap, a performance test can ensure that the number of swaps doesn't quadruple when the size of the input doubles.

    Besides expanding the scope of tests, this ability also allows more radical refactoring without needing to modify tests. All Mu's tests call a top-level function rather than individual sub-systems directly. As a result the way the subsystems are invoked can be radically changed (interface changes, making synchronous functions asynchronous, etc.). As long as the new versions emit the same implementation-independent events in the logs, the tests will continue to pass. ([More information.](http://akkartik.name/post/tracing-tests))

  3. Organizing code and tests in layers of functionality, so that outsiders can build simple and successively more complex versions of a project, gradually enabling more peripheral features. Think of it as a cleaned-up git log for the project. (More information.)

Since I don't understand how Linux and other modern platforms work, Mu is built on an idealized VM while I learn. Eventually the plan is to transplant what I learn back to Linux.

To minimize my workload, Mu doesn't have a high-level language yet. Instead, I've been programming directly in the VM's idealized assembly language. I expected this to be painful, but it's had some surprising benefits. First, programs as lists of instructions seem to be easier for non-programmers to comprehend than programs as trees of expressions. Second, I've found that Literate Programming using layers makes assembly much more ergonomic. Third, labels for gotos turn out to be great waypoints to insert code at from future layers; when I tried to divide C programs into layers, I sometimes had to split statements in two so I could insert code between them. Labels also seem a promising representation for providing advanced mechanisms like continuations and lisp-like macros.

High level languages today seem to provide three kinds of benefits: expressiveness (e.g. nested expressions, classes), safety (e.g. type checking) and automation (e.g. garbage collection). An idealized assembly language gives up some expressiveness, but doesn't seem to affect the other benefits. So far Mu provides strong memory safety, lexical scope, generics, higher-order functions and safe concurrency.

Taking Mu for a spin

Mu is currently implemented in C++ and requires a unix-like environment. It's been tested on Ubuntu and Mac OS X, 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.

Build Status

Running Mu will always recompile it if necessary:

$ cd mu
$ ./mu

As a simple example, here's a program with some arithmetic:

code example

As I said before, Mu functions 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 consists of a name and 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 and even arbitrary trees, like:

x:array:number:3  # x is an array of 3 numbers
y:list:number  # y is a list of numbers
# ':' is just syntactic sugar
{z: (map (address array character) (list number))}   # map from string to list of numbers

Try out the program now:

$ ./mu example1.mu
$

Not much to see yet, since it doesn't print anything. To print the result, try adding the instruction $print a to the function.


Here's a second example, of a function that can take ingredients:

fahrenheit to celsius

Functions can specify headers showing their expected ingredients and products, separated by -> (unlike the <- in calls).

Since Mu is a low-level VM language, it provides extra control at the cost of verbosity. Using local-scope, you have explicit control over stack frames to isolate your functions in a type-safe manner. You can also create more sophisticated setups like closures. One consequence of this extra control: you have to explicitly load-ingredients after you set up the stack.

An alternative syntax is what the above example is converted to internally:

fahrenheit to celsius desugared

The header gets dropped after checking types at call-sites, and after replacing load-ingredients with explicit instructions to load each ingredient separately, and to explicitly return products to the caller. After this translation functions are once again just lists of instructions.

This alternative syntax isn't just an implementation detail. I've actually found it easier to teach functions to non-programmers by starting with this syntax, so that they can visualize a pipe from caller to callee, and see the names of variables get translated one by one through the pipe.


A third example, this time illustrating conditionals:

factorial example

In spite of how it looks, this is still just a list of instructions and labels. Internally, the instructions break and loop get converted to jump instructions to after the enclosing } or { labels, respectively.

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, you've seen earlier how chessboard.mu checks the initial position of a game of chess (delimiting the edges of the screen with dots):

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.


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.

literate programming

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

By convention we use the prefix '+' to indicate function-local label names you can jump to, and surround in '<>' global label names for inserting code at.


Another example, this time with concurrency.

forking concurrent routines
$ ./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

d) Try out the programming environment:

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

Screenshot:

programming environment

You write functions 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 dependency-injected interfaces for the network and file system.

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.
  • Rust for demonstrating that a system-programming language can be safe.