mu/mu.arc.t

1100 lines
38 KiB
Perl
Raw Normal View History

; Mu: An exploration on making the global structure of programs more accessible.
;
; "Is it a language, or an operating system, or a virtual machine? Mu."
; (with apologies to Robert Pirsig: http://en.wikipedia.org/wiki/Mu_%28negative%29#In_popular_culture)
;
; I want to live in a world where I can have an itch to tweak a program, clone
; its open-source repository, orient myself on how it's organized, and make
; the simple change I envisioned, all in an afternoon. This codebase tries to
; make this possible for its readers. (More details: http://akkartik.name/about)
;
; What helps comprehend the global structure of programs? For starters, let's
; enumerate what doesn't: idiomatic code, adherence to a style guide or naming
; convention, consistent indentation, API documentation for each class, etc.
; These conventional considerations improve matters in the small, but don't
; help understand global organization. They help existing programmers manage
; day-to-day operations, but they can't turn outsider programmers into
; insiders. (Elaboration: http://akkartik.name/post/readable-bad)
;
; In my experience, two things have improved matters so far: version control
; and automated tests. Version control lets me rewind back to earlier, simpler
; times when the codebase was simpler, when its core skeleton was easier to
; ascertain. Indeed, arguably what came first is by definition the skeleton of
; a program, modulo major rewrites. Once you understand the skeleton, it
; becomes tractable to 'play back' later major features one by one. (Previous
; project that fleshed out this idea: http://akkartik.name/post/wart-layers)
;
; The second and biggest boost to comprehension comes from tests. Tests are
; good for writers for well-understood reasons: they avoid regressions, and
; they can influence code to be more decoupled and easier to change. In
; addition, tests are also good for the outsider reader because they permit
; active reading. If you can't build a program and run its tests it can't help
; you understand it. It hangs limp at best, and might even be actively
; misleading. If you can run its tests, however, it comes alive. You can step
; through scenarios in a debugger. You can add logging and scan logs to make
; sense of them. You can run what-if scenarios: "why is this line not written
; like this?" Make a change, rerun tests: "Oh, that's why." (Elaboration:
; http://akkartik.name/post/literate-programming)
;
; However, tests are only useful to the extent that they exist. Think back to
; your most recent codebase. Do you feel comfortable releasing a new version
; just because the tests pass? I'm not aware of any such project. There's just
; too many situations envisaged by the authors that were never encoded in a
; test. Even disciplined authors can't test for performance or race conditions
; or fault tolerance. If a line is phrased just so because of some subtle
; performance consideration, it's hard to communicate to newcomers.
;
; This isn't an arcane problem, and it isn't just a matter of altruism. As
; more and more such implicit considerations proliferate, and as the original
; authors are replaced by latecomers for day-to-day operations, knowledge is
; actively forgotten and lost. The once-pristine codebase turns into legacy
; code that is hard to modify without expensive and stress-inducing
; regressions.
;
; How to write tests for performance, fault tolerance, race conditions, etc.?
; How can we state and verify that a codepath doesn't ever perform memory
; allocation, or write to disk? It requires better, more observable primitives
; than we currently have. Modern operating systems have their roots in the
; 70s. Their interfaces were not designed to be testable. They provide no way
; to simulate a full disk, or a specific sequence of writes from different
; threads. We need something better.
;
; This project tries to move, groping, towards that 'something better', a
; platform that is both thoroughly tested and allows programs written for it
; to be thoroughly tested. It tries to answer the question:
;
; If Denis Ritchie and Ken Thompson were to set out today to co-design unix
; and C, knowing what we know about automated tests, what would they do
; differently?
;
; To try to impose *some* constraints on this gigantic yak-shave, we'll try to
; keep both language and OS as simple as possible, focused entirely on
; permitting more kinds of tests, on first *collecting* all the information
; about implicit considerations in some form so that readers and tools can
; have at least some hope of making sense of it.
;
; The initial language will be just assembly. We'll try to make it convenient
; to program in with some simple localized rewrite rules inspired by lisp
; macros and literate programming. Programmers will have to do their own
; memory management and register allocation, but we'll provide libraries to
; help with them.
;
; The initial OS will provide just memory management and concurrency
; primitives. No users or permissions (we don't live on mainframes anymore),
; no kernel- vs user-mode, no virtual memory or process abstraction, all
; threads sharing a single address space (use VMs for security and
; sandboxing). The only use case we care about is getting a test harness to
; run some code, feed it data through blocking channels, stop it and observe
; its internals. The code under test is expected to cooperate in such testing,
; by logging important events for the test harness to observe. (More info:
; http://akkartik.name/post/tracing-tests)
;
; The common thread here is elimination of abstractions, and it's not an
; accident Abstractions help insiders manage the evolution of a codebase, but
; they actively hinder outsiders in understanding it from scratch. This
; matters, because the funnel to turn outsiders into insiders is critical to
; the long-term life of a codebase. Perhaps authors should raise their
; estimation of the costs of abstraction, and go against their instincts for
; introducing it. That's what I'll be trying to do: question every abstraction
; before I introduce it. We'll see how it goes.
; ---
; Mu is currently built atop Racket and Arc, but this is temporary and
; contingent. We want to keep our options open, whether to port to a different
; host language, and easy to rewrite to native code for any platform. So we'll
; try to avoid cheating by using host functionality 'for free'.
;
; Other than that, we'll say no more about the code, and focus in the rest of
; this file on the scenarios the code cares about.
2014-07-06 07:07:03 +00:00
(load "mu.arc")
; Every test below is conceptually a run right after our virtual machine
; starts up. When it starts up we assume it knows about the following types.
(on-init
(= types* (obj
; Each type must be scalar or array, sum or product or primitive
type (obj size 1) ; implicitly scalar and primitive
type-array (obj array t elem 'type)
type-array-address (obj size 1 address t elem 'type-array)
location (obj size 1)
integer (obj size 1)
boolean (obj size 1)
boolean-address (obj size 1 address t)
byte (obj size 1)
;? string (obj array t elem 'byte) ; inspired by Go
character (obj size 1) ; int32 like a Go rune
character-address (obj size 1 address t elem 'character)
string (obj size 1) ; temporary hack
; arrays consist of an integer length followed by the right number of elems
integer-array (obj array t elem 'integer)
integer-address (obj size 1 address t elem 'integer) ; pointer to int
; records consist of a series of elems, corresponding to a list of types
integer-boolean-pair (obj size 2 record t elems '(integer boolean))
integer-boolean-pair-address (obj size 1 address t elem 'integer-boolean-pair)
integer-boolean-pair-array (obj array t elem 'integer-boolean-pair)
integer-integer-pair (obj size 2 record t elems '(integer integer))
integer-point-pair (obj size 2 record t elems '(integer integer-integer-pair))
)))
; Our language is assembly-like in that functions consist of series of
; statements, and statements consist of an operation and its arguments (input
; and output).
;
; oarg1, oarg2, ... <- op arg1, arg2, ...
;
; Args must be atomic, like an integer or a memory address, they can't be
; expressions doing arithmetic or function calls. But we can have any number
; of them.
;
; Since we're building on lisp, our code samples won't look quite like the
; idealized syntax above. For now they will be lists of lists:
;
; (function-name
; ((oarg1 oarg2 ... <- op arg1 arg2 ...)
; ...
; ...))
;
; Each arg/oarg is itself a list, with the payload value at the head, and
; various metadata in the rest. In this first example the only metadata is types:
; 'integer' for a memory location containing an integer, and 'literal' for a
; value included directly in code. (Assembly languages traditionally call them
; 'immediate' operands.)
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "literal")
2014-08-19 18:58:22 +00:00
(add-fns
'((test1
((1 integer) <- copy (23 literal)))))
(run 'test1)
2014-08-19 18:58:22 +00:00
;? (prn memory*)
(if (~is memory*.1 23)
(prn "F - 'copy' writes its lone 'arg' after the instruction name to its lone 'oarg' or output arg before the arrow. After this test, the value 23 is stored in memory address 1."))
2014-08-29 02:23:38 +00:00
;? (quit)
2014-07-17 15:16:22 +00:00
; Our basic arithmetic ops can operate on memory locations or literals.
; (Ignore hardware details like registers for now.)
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "add")
2014-08-19 18:58:22 +00:00
(add-fns
'((test1
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
2014-08-19 18:58:22 +00:00
((3 integer) <- add (1 integer) (2 integer)))))
(run 'test1)
2014-07-06 07:07:03 +00:00
(if (~iso memory* (obj 1 1 2 3 3 4))
2014-07-17 15:16:22 +00:00
(prn "F - 'add' operates on two addresses"))
2014-07-06 08:53:18 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "add-literal")
(add-fns
'((test1
((1 integer) <- add (2 literal) (3 literal)))))
(run 'test1)
(if (~is memory*.1 5)
(prn "F - ops can take 'literal' operands (but not return them)"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "sub-literal")
2014-07-12 04:04:38 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- sub (1 literal) (3 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-12 04:04:38 +00:00
;? (prn memory*)
(if (~is memory*.1 -2)
(prn "F - 'sub' subtracts the second arg from the first"))
2014-07-12 04:04:38 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "mul-literal")
2014-07-12 04:04:38 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- mul (2 literal) (3 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-12 04:04:38 +00:00
;? (prn memory*)
(if (~is memory*.1 6)
2014-07-17 15:16:22 +00:00
(prn "F - 'mul' multiplies like 'add' adds"))
2014-07-12 04:04:38 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "div-literal")
2014-07-12 04:04:38 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- div (8 literal) (3 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-12 04:04:38 +00:00
;? (prn memory*)
(if (~is memory*.1 (/ real.8 3))
(prn "F - 'div' divides like 'sub' subtracts"))
2014-07-12 04:04:38 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "idiv-literal")
2014-07-12 04:04:38 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) (2 integer) <- idiv (8 literal) (3 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-12 04:04:38 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 2))
2014-10-11 02:57:31 +00:00
(prn "F - 'idiv' performs integer division, returning quotient and retest1der"))
2014-07-12 04:22:32 +00:00
; Basic boolean operations: and, or, not
; There are easy ways to encode booleans in binary, but we'll skip past those
; details.
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "and-literal")
2014-07-12 05:50:55 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 boolean) <- and (t literal) (nil literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-12 05:50:55 +00:00
;? (prn memory*)
(if (~is memory*.1 nil)
2014-07-17 15:16:22 +00:00
(prn "F - logical 'and' for booleans"))
2014-07-12 05:50:55 +00:00
; Basic comparison operations: lt, le, gt, ge, eq, neq
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "lt-literal")
2014-07-14 04:27:23 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 boolean) <- lt (4 literal) (3 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-14 04:27:23 +00:00
;? (prn memory*)
(if (~is memory*.1 nil)
2014-07-17 15:16:22 +00:00
(prn "F - 'lt' is the less-than inequality operator"))
2014-07-14 04:27:23 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "le-literal-false")
2014-07-14 04:27:23 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 boolean) <- le (4 literal) (3 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-14 04:27:23 +00:00
;? (prn memory*)
(if (~is memory*.1 nil)
2014-07-17 15:16:22 +00:00
(prn "F - 'le' is the <= inequality operator"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "le-literal-true")
2014-07-17 15:16:22 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 boolean) <- le (4 literal) (4 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-17 15:16:22 +00:00
;? (prn memory*)
(if (~is memory*.1 t)
2014-07-17 15:16:22 +00:00
(prn "F - 'le' returns true for equal operands"))
2014-07-14 04:27:23 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "le-literal-true-2")
2014-07-14 04:27:23 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 boolean) <- le (4 literal) (5 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-14 04:27:23 +00:00
;? (prn memory*)
(if (~is memory*.1 t)
2014-07-17 15:16:22 +00:00
(prn "F - le is the <= inequality operator - 2"))
2014-07-14 04:27:23 +00:00
; Control flow operations: jmp, jif
; These introduce a new type -- 'offset' -- for literals that refer to memory
; locations relative to the current location.
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "jmp-skip")
2014-07-12 04:22:32 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (8 literal))
(jmp (1 offset))
((2 integer) <- copy (3 literal)) ; should be skipped
2014-07-12 04:22:32 +00:00
(reply))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-12 04:22:32 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 8))
2014-07-17 15:16:22 +00:00
(prn "F - 'jmp' skips some instructions"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "jmp-target")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (8 literal))
(jmp (1 offset))
((2 integer) <- copy (3 literal)) ; should be skipped
(reply)
((3 integer) <- copy (34 literal))))) ; never reached
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 8))
2014-07-17 15:16:22 +00:00
(prn "F - 'jmp' doesn't skip too many instructions"))
2014-08-28 23:40:28 +00:00
;? (quit)
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "jif-skip")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((2 integer) <- copy (1 literal))
((1 boolean) <- eq (1 literal) (2 integer))
(jif (1 boolean) (1 offset))
((2 integer) <- copy (3 literal))
(reply)
((3 integer) <- copy (34 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 t 2 1))
2014-07-17 15:16:22 +00:00
(prn "F - 'jif' is a conditional 'jmp'"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "jif-fallthrough")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 boolean) <- eq (1 literal) (2 literal))
(jif (3 boolean) (1 offset))
((2 integer) <- copy (3 literal))
(reply)
((3 integer) <- copy (34 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 nil 2 3))
2014-07-17 15:16:22 +00:00
(prn "F - if 'jif's first arg is false, it doesn't skip any instructions"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "jif-backward")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (2 literal))
((2 integer) <- copy (1 literal))
; loop
((2 integer) <- add (2 integer) (2 integer))
((3 boolean) <- eq (1 integer) (2 integer))
(jif (3 boolean) (-3 offset)) ; to loop
((4 integer) <- copy (3 literal))
(reply)
((3 integer) <- copy (34 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 4 3 nil 4 3))
(prn "F - 'jif' can take a negative offset to make backward jumps"))
; Data movement relies on addressing modes:
; 'direct' - refers to a memory location; default for most types.
; 'literal' - directly encoded in the code; implicit for some types like 'offset'.
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "direct-addressing")
2014-07-31 09:18:00 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
2014-07-31 09:18:00 +00:00
((2 integer) <- copy (1 integer)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-31 09:18:00 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 34))
(prn "F - 'copy' performs direct addressing"))
; 'Indirect' addressing refers to an address stored in a memory location.
; Indicated by the metadata 'deref'. Usually requires an address type.
; In the test below, the memory location 1 contains '2', so an indirect read
; of location 1 returns the value of location 2.
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "indirect-addressing")
2014-07-31 09:18:00 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer-address) <- copy (2 literal))
((2 integer) <- copy (34 literal))
((3 integer) <- copy (1 integer-address deref)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-07-31 09:18:00 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 34 3 34))
(prn "F - 'copy' performs indirect addressing"))
2014-07-31 09:18:00 +00:00
; Output args can use indirect addressing. In the test below the value is
; stored at the location stored in location 1 (i.e. location 2).
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "indirect-addressing-oarg")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer-address) <- copy (2 literal))
((2 integer) <- copy (34 literal))
((1 integer-address deref) <- add (2 integer) (2 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 36))
2014-08-22 03:08:22 +00:00
(prn "F - instructions can perform indirect addressing on output arg"))
; Until now we've dealt with scalar types like integers and booleans and
; addresses. We can also have compound types: arrays and records.
;
; 'get' accesses fields in records
; 'index' accesses indices in arrays
2014-08-20 04:33:48 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "get-record")
2014-08-20 04:33:48 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
((2 boolean) <- copy (nil literal))
2014-08-20 04:33:48 +00:00
((3 boolean) <- get (1 integer-boolean-pair) (1 offset))
((4 integer) <- get (1 integer-boolean-pair) (0 offset)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-08-20 04:33:48 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 nil 3 nil 4 34))
(prn "F - 'get' accesses fields of records"))
2014-10-05 22:02:28 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "get-indirect")
2014-10-05 22:02:28 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
((2 boolean) <- copy (nil literal))
((3 integer-boolean-pair-address) <- copy (1 literal))
2014-10-05 22:02:28 +00:00
((4 boolean) <- get (3 integer-boolean-pair-address deref) (1 offset))
((5 integer) <- get (3 integer-boolean-pair-address deref) (0 offset)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-10-05 22:02:28 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 nil 3 1 4 nil 5 34))
(prn "F - 'get' accesses fields of record address"))
2014-08-22 03:08:22 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "get-compound-field")
2014-08-22 03:08:22 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
((2 integer) <- copy (35 literal))
((3 integer) <- copy (36 literal))
2014-08-22 03:08:22 +00:00
((4 integer-integer-pair) <- get (1 integer-point-pair) (1 offset)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-08-22 03:08:22 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 35 3 36 4 35 5 36))
(prn "F - 'get' accesses fields spanning multiple locations"))
2014-10-05 18:34:23 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "get-address")
2014-10-05 18:34:23 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
((2 integer) <- copy (t literal))
2014-10-05 22:05:26 +00:00
((3 boolean-address) <- get-address (1 integer-boolean-pair) (1 offset)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-10-05 18:34:23 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 t 3 2))
(prn "F - 'get-address' returns address of fields of records"))
2014-10-05 22:10:29 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "get-address-indirect")
2014-10-05 22:10:29 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
((2 integer) <- copy (t literal))
((3 integer-boolean-pair-address) <- copy (1 literal))
2014-10-05 22:10:29 +00:00
((4 boolean-address) <- get-address (3 integer-boolean-pair-address deref) (1 offset)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-10-05 22:10:29 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 t 3 1 4 2))
(prn "F - 'get-address' accesses fields of record address"))
2014-10-05 22:10:29 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "index-array-literal")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (2 literal))
((2 integer) <- copy (23 literal))
((3 boolean) <- copy (nil literal))
((4 integer) <- copy (24 literal))
((5 boolean) <- copy (t literal))
((6 integer-boolean-pair) <- index (1 integer-boolean-pair-array) (1 literal)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 23 3 nil 4 24 5 t 6 24 7 t))
(prn "F - 'index' accesses indices of arrays"))
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "index-array-direct")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (2 literal))
((2 integer) <- copy (23 literal))
((3 boolean) <- copy (nil literal))
((4 integer) <- copy (24 literal))
((5 boolean) <- copy (t literal))
((6 integer) <- copy (1 literal))
((7 integer-boolean-pair) <- index (1 integer-boolean-pair-array) (6 integer)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 23 3 nil 4 24 5 t 6 1 7 24 8 t))
(prn "F - 'index' accesses indices of arrays"))
2014-10-05 18:34:23 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "index-address")
2014-10-05 18:34:23 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (2 literal))
((2 integer) <- copy (23 literal))
((3 boolean) <- copy (nil literal))
((4 integer) <- copy (24 literal))
((5 boolean) <- copy (t literal))
((6 integer) <- copy (1 literal))
((7 integer-boolean-pair-address) <- index-address (1 integer-boolean-pair-array) (6 integer)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-10-05 18:34:23 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 23 3 nil 4 24 5 t 6 1 7 4))
(prn "F - 'index-address' returns addresses of indices of arrays"))
2014-10-05 18:34:23 +00:00
; todo: test that out-of-bounds access throws an error
; Array values know their length. Record lengths are saved in the types table.
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "len-array")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (2 literal))
((2 integer) <- copy (23 literal))
((3 boolean) <- copy (nil literal))
((4 integer) <- copy (24 literal))
((5 boolean) <- copy (t literal))
((6 integer) <- len (1 integer-boolean-pair-array)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~iso memory* (obj 1 2 2 23 3 nil 4 24 5 t 6 2))
(prn "F - 'len' accesses length of array"))
; 'sizeof' is a helper to determine the amount of memory required by a type.
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "sizeof-record")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- sizeof (integer-boolean-pair type)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~is memory*.1 2)
(prn "F - 'sizeof' returns space required by arg"))
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "sizeof-record-not-len")
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- sizeof (integer-point-pair type)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
;? (prn memory*)
(if (~is memory*.1 3)
(prn "F - 'sizeof' is different from number of elems"))
; Regardless of a type's length, you can move it around with 'copy'.
2014-08-20 06:37:50 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "compound-operand")
2014-08-20 06:37:50 +00:00
(add-fns
2014-10-11 02:57:31 +00:00
'((test1
((1 integer) <- copy (34 literal))
((2 boolean) <- copy (nil literal))
((4 boolean) <- copy (t literal))
2014-08-20 06:37:50 +00:00
((3 integer-boolean-pair) <- copy (1 integer-boolean-pair)))))
2014-10-11 02:57:31 +00:00
(run 'test1)
2014-08-20 06:37:50 +00:00
;? (prn memory*)
(if (~iso memory* (obj 1 34 2 nil 3 34 4 nil))
2014-08-22 03:08:22 +00:00
(prn "F - ops can operate on records spanning multiple locations"))
2014-08-20 06:37:50 +00:00
; Just like the table of types is centralized, functions are conceptualized as
; a centralized table of operations just like the 'primitives' we've seen so
; far. If you create a function you can call it like any other op.
(reset)
(new-trace "new-fn")
(add-fns
'((test1
((3 integer) <- add (1 integer) (2 integer)))
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
(test1))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 1 2 3 3 4))
(prn "F - calling a user-defined function runs its instructions"))
;? (quit)
(reset)
(new-trace "new-fn-once")
(add-fns
'((test1
((1 integer) <- copy (1 literal)))
(main
(test1))))
(if (~is 2 (run 'main))
(prn "F - calling a user-defined function runs its instructions exactly once"))
;? (quit)
; User-defined functions communicate with their callers through two
; primitives:
;
; 'arg' - to access inputs
; 'reply' - to return outputs
(reset)
(new-trace "new-fn-reply")
(add-fns
'((test1
((3 integer) <- add (1 integer) (2 integer))
(reply)
((4 integer) <- copy (34 literal)))
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
(test1))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 1 2 3 3 4))
(prn "F - 'reply' stops executing the current function"))
;? (quit)
(reset)
(new-trace "new-fn-reply-nested")
(add-fns
`((test1
((3 integer) <- test2))
(test2
(reply (2 integer)))
(main
((2 integer) <- copy (34 literal))
(test1))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 2 34 3 34))
(prn "F - 'reply' stops executing any callers as necessary"))
;? (quit)
(reset)
(new-trace "new-fn-reply-once")
(add-fns
'((test1
((3 integer) <- add (1 integer) (2 integer))
(reply)
((4 integer) <- copy (34 literal)))
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
(test1))))
(if (~is 4 (run 'main)) ; last reply sometimes not counted. worth fixing?
(prn "F - 'reply' executes instructions exactly once"))
;? (quit)
(reset)
(new-trace "new-fn-arg-sequential")
(add-fns
'((test1
((4 integer) <- arg)
((5 integer) <- arg)
((3 integer) <- add (4 integer) (5 integer))
(reply)
((4 integer) <- copy (34 literal)))
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
(test1 (1 integer) (2 integer))
)))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 1 2 3 3 4
; add-fn's temporaries
4 1 5 3))
(prn "F - 'arg' accesses in order the operands of the most recent function call (the caller)"))
;? (quit)
(reset)
(new-trace "new-fn-arg-random-access")
(add-fns
'((test1
((5 integer) <- arg 1)
((4 integer) <- arg 0)
((3 integer) <- add (4 integer) (5 integer))
(reply)
((4 integer) <- copy (34 literal))) ; should never run
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
(test1 (1 integer) (2 integer))
)))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 1 2 3 3 4
; add-fn's temporaries
4 1 5 3))
(prn "F - 'arg' with index can access function call arguments out of order"))
;? (quit)
; todo: test that too few args throws an error
; how should errors be handled? will be unclear until we support concurrency and routine trees.
(reset)
(new-trace "new-fn-reply-oarg")
(add-fns
'((test1
((4 integer) <- arg)
((5 integer) <- arg)
((6 integer) <- add (4 integer) (5 integer))
(reply (6 integer))
((4 integer) <- copy (34 literal)))
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
((3 integer) <- test1 (1 integer) (2 integer)))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 1 2 3 3 4
; add-fn's temporaries
4 1 5 3 6 4))
(prn "F - 'reply' can take aguments that are returned, or written back into output args of caller"))
(reset)
(new-trace "new-fn-reply-oarg-multiple")
(add-fns
'((test1
((4 integer) <- arg)
((5 integer) <- arg)
((6 integer) <- add (4 integer) (5 integer))
(reply (6 integer) (5 integer))
((4 integer) <- copy (34 literal)))
(main
((1 integer) <- copy (1 literal))
((2 integer) <- copy (3 literal))
((3 integer) (7 integer) <- test1 (1 integer) (2 integer)))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 1 2 3 3 4 7 3
; add-fn's temporaries
4 1 5 3 6 4))
(prn "F - 'reply' permits a function to return multiple values at once"))
; 'type' and 'otype' let us create generic functions that run different code
; based on what args the caller provides, or what oargs the caller expects.
;
; These operations are more experimental than their surroundings; we might
; eventually need more detailed access to the calling instruction.
;
; There's also the open question of how to deal with dynamic-typing situations
; where the caller doesn't know the type of its arg/oarg.
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "dispatch-otype")
(add-fns
2014-07-17 15:13:52 +00:00
'((test1
((4 type) <- otype 0)
((5 boolean) <- neq (4 type) (integer literal))
(jif (5 boolean) (3 offset))
((6 integer) <- arg)
((7 integer) <- arg)
((8 integer) <- add (6 integer) (7 integer))
(reply (8 integer)))
(main
((1 integer) <- test1 (1 literal) (3 literal)))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 4
; add-fn's temporaries
4 'integer 5 nil 6 1 7 3 8 4))
(prn "F - an example function that checks that its oarg is an integer"))
;? (quit)
; todo - test that reply increments pc for caller frame after popping current frame
2014-07-17 15:16:22 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "dispatch-otype-multiple-clauses")
2014-07-17 15:16:22 +00:00
(add-fns
'((test-fn
((4 type) <- otype 0)
; integer needed? add args
((5 boolean) <- neq (4 type) (integer literal))
(jif (5 boolean) (4 offset))
((6 integer) <- arg)
((7 integer) <- arg)
((8 integer) <- add (6 integer) (7 integer))
(reply (8 integer))
; boolean needed? 'or' args
((5 boolean) <- neq (4 type) (boolean literal))
(jif (5 boolean) (4 offset))
((6 boolean) <- arg)
((7 boolean) <- arg)
((8 boolean) <- or (6 boolean) (7 boolean))
(reply (8 boolean)))
2014-07-17 15:16:22 +00:00
(main
((1 boolean) <- test-fn (t literal) (t literal)))))
(run 'main)
2014-07-17 15:16:22 +00:00
;? (prn memory*)
(if (~is memory*.1 t)
2014-07-17 15:16:22 +00:00
(prn "F - an example function that can do different things (dispatch) based on the type of its args or oargs"))
(if (~iso memory* (obj 1 t
; add-fn's temporaries
4 'boolean 5 nil 6 t 7 t 8 t))
(prn "F - an example function that can do different things (dispatch) based on the type of its args or oargs (internals)"))
;? (quit)
2014-07-17 15:16:22 +00:00
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "dispatch-otype-multiple-calls")
2014-07-17 15:16:22 +00:00
(add-fns
'((test-fn
((4 type) <- otype 0)
((5 boolean) <- neq (4 type) (integer literal))
(jif (5 boolean) (4 offset))
((6 integer) <- arg)
((7 integer) <- arg)
((8 integer) <- add (6 integer) (7 integer))
(reply (8 integer))
((5 boolean) <- neq (4 type) (boolean literal))
(jif (5 boolean) (6 offset))
((6 boolean) <- arg)
((7 boolean) <- arg)
((8 boolean) <- or (6 boolean) (7 boolean))
(reply (8 boolean)))
2014-07-17 15:16:22 +00:00
(main
((1 boolean) <- test-fn (t literal) (t literal))
((2 integer) <- test-fn (3 literal) (4 literal)))))
(run 'main)
;? (prn memory*)
(if (~and (is memory*.1 t) (is memory*.2 7))
2014-07-17 15:16:22 +00:00
(prn "F - different calls can exercise different clauses of the same function"))
(if (~iso memory* (obj ; results of first and second calls to test-fn
1 t 2 7
; temporaries for most recent call to test-fn
4 'integer 5 nil 6 3 7 4 8 7))
(prn "F - different calls can exercise different clauses of the same function (internals)"))
; Our control operators are quite inconvenient to use, so mu provides a
; lightweight tool called 'convert-braces' to work in a slightly more
; convenient format with nested braces:
;
; {
; some instructions
; {
; more instructions
; }
; }
;
; Braces are just labels, they require no special parsing. The operations
; 'break' and 'continue' jump to just after the enclosing '}' and '{'
; respectively.
;
; Conditional and unconditional 'break' and 'continue' should give us 80% of
; the benefits of the control-flow primitives we're used to in other
; languages, like 'if', 'while', 'for', etc.
2014-10-07 17:26:14 +00:00
(reset)
(new-trace "convert-braces")
(if (~iso (convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
{ begin ; 'begin' is just a hack because racket turns curlies into parens
((4 boolean) <- neq (1 integer) (3 integer))
(breakif (4 boolean))
((5 integer) <- copy (34 literal))
}
(reply)))
'(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
((4 boolean) <- neq (1 integer) (3 integer))
(jif (4 boolean) (1 offset))
((5 integer) <- copy (34 literal))
(reply)))
(prn "F - convert-braces replaces breakif with a jif to after the next close curly"))
2014-10-07 17:26:14 +00:00
(reset)
(new-trace "convert-braces-empty-block")
(if (~iso (convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
{ begin
(break)
}
(reply)))
'(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
(jmp (0 offset))
(reply)))
(prn "F - convert-braces works for degenerate blocks"))
2014-10-07 17:26:14 +00:00
(reset)
(new-trace "convert-braces-nested-break")
(if (~iso (convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
{ begin
((4 boolean) <- neq (1 integer) (3 integer))
(breakif (4 boolean))
{ begin
((5 integer) <- copy (34 literal))
}
}
(reply)))
'(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
((4 boolean) <- neq (1 integer) (3 integer))
(jif (4 boolean) (1 offset))
((5 integer) <- copy (34 literal))
(reply)))
2014-07-19 02:04:43 +00:00
(prn "F - convert-braces balances curlies when converting break"))
2014-10-07 17:26:14 +00:00
(reset)
(new-trace "convert-braces-nested-continue")
(if (~iso (convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
{ begin
((3 integer) <- add (2 integer) (2 integer))
{ begin
((4 boolean) <- neq (1 integer) (3 integer))
}
(continueif (4 boolean))
((5 integer) <- copy (34 literal))
}
(reply)))
'(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
((3 integer) <- add (2 integer) (2 integer))
((4 boolean) <- neq (1 integer) (3 integer))
(jif (4 boolean) (-3 offset))
((5 integer) <- copy (34 literal))
(reply)))
2014-07-19 02:04:43 +00:00
(prn "F - convert-braces balances curlies when converting continue"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "continue")
2014-10-07 20:26:01 +00:00
;? (set dump-trace*)
(add-fns `((main ,@(convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (1 literal))
{ begin
((2 integer) <- add (2 integer) (2 integer))
((3 boolean) <- neq (1 integer) (2 integer))
(continueif (3 boolean))
((4 integer) <- copy (34 literal))
}
(reply))))))
;? (each stmt function*!main
;? (prn stmt))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 4 2 4 3 nil 4 34))
(prn "F - continue correctly loops"))
; todo: fuzz-test invariant: convert-braces offsets should be robust to any
; number of inner blocks inside but not around the continue block.
(reset)
(new-trace "continue-nested")
;? (set dump-trace*)
(add-fns `((main ,@(convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (1 literal))
{ begin
((2 integer) <- add (2 integer) (2 integer))
{ begin
((3 boolean) <- neq (1 integer) (2 integer))
}
(continueif (3 boolean))
((4 integer) <- copy (34 literal))
}
(reply))))))
2014-10-07 20:26:01 +00:00
;? (each stmt function*!main
;? (prn stmt))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 4 2 4 3 nil 4 34))
(prn "F - continue correctly loops"))
2014-08-19 17:31:58 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "continue-fail")
(add-fns `((main ,@(convert-braces '(((1 integer) <- copy (4 literal))
((2 integer) <- copy (2 literal))
{ begin
((2 integer) <- add (2 integer) (2 integer))
{ begin
((3 boolean) <- neq (1 integer) (2 integer))
}
(continueif (3 boolean))
((4 integer) <- copy (34 literal))
}
(reply))))))
(run 'main)
;? (prn memory*)
(if (~iso memory* (obj 1 4 2 4 3 nil 4 34))
(prn "F - continue might never trigger"))
; A rudimentary memory allocator. Eventually we want to write this in mu.
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "new-primitive")
(let before Memory-in-use-until
(add-fns
'((main
((1 integer-address) <- new (integer type)))))
(run 'main)
;? (prn memory*)
(if (~iso memory*.1 before)
(prn "F - 'new' returns current high-water mark"))
(if (~iso Memory-in-use-until (+ before 1))
(prn "F - 'new' on primitive types increments high-water mark by their size")))
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "new-array-literal")
(let before Memory-in-use-until
(add-fns
'((main
((1 type-array-address) <- new (type-array type) (5 literal)))))
(run 'main)
;? (prn memory*)
(if (~iso memory*.1 before)
2014-10-07 05:58:06 +00:00
(prn "F - 'new' on array with literal size returns current high-water mark"))
(if (~iso Memory-in-use-until (+ before 6))
(prn "F - 'new' on primitive arrays increments high-water mark by their size")))
2014-08-29 03:44:16 +00:00
2014-10-07 05:58:06 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "new-array-direct")
2014-10-07 05:58:06 +00:00
(let before Memory-in-use-until
(add-fns
'((main
((1 integer) <- copy (5 literal))
2014-10-07 05:58:06 +00:00
((2 type-array-address) <- new (type-array type) (1 integer)))))
(run 'main)
;? (prn memory*)
(if (~iso memory*.2 before)
(prn "F - 'new' on array with variable size returns current high-water mark"))
(if (~iso Memory-in-use-until (+ before 6))
2014-10-07 05:58:06 +00:00
(prn "F - 'new' on primitive arrays increments high-water mark by their (variable) size")))
; A rudimentary process scheduler. You can 'run' multiple functions at once,
; and they share the virtual processor.
; There's also a 'fork' primitive to let functions create new threads of
; execution.
; Eventually we want to allow callers to influence how much of their CPU they
; give to their 'children', or to rescind a child's running privileges.
2014-08-29 03:44:16 +00:00
(reset)
2014-10-07 17:26:14 +00:00
(new-trace "scheduler")
2014-08-29 03:44:16 +00:00
(add-fns
'((f1
((1 integer) <- copy (3 literal)))
2014-08-29 03:44:16 +00:00
(f2
((2 integer) <- copy (4 literal)))))
2014-08-31 18:27:58 +00:00
(let ninsts (run 'f1 'f2)
2014-08-29 03:44:16 +00:00
(when (~iso 2 ninsts)
(prn "F - scheduler didn't run the right number of instructions: " ninsts)))
(if (~iso memory* (obj 1 3 2 4))
(prn "F - scheduler runs multiple functions: " memory*))
(check-trace-contents "scheduler orders functions correctly"
'(("schedule" "f1")
("schedule" "f2")
))
(check-trace-contents "scheduler orders schedule and run events correctly"
'(("schedule" "f1")
("run" "f1 0")
("schedule" "f2")
("run" "f2 0")
))
2014-10-07 17:26:14 +00:00
; The scheduler needs to keep track of the call stack for each thread.
; Eventually we'll want to save this information in mu's address space itself,
; along with the types array, the magic buffers for args and oargs, and so on.
;
; Eventually we want the right stack-management primitives to build delimited
; continuations in mu.
(reset) ; end file with this to persist the trace for the final test