; 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) ; ;; Motivation ; ; 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. ; --- ;; Getting started ; ; 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': relying on the host platform for advanced ; functionality. ; ; 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. (selective-load "mu.arc" section-level) ;? (quit) (section 20 ; 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 look like this: ; ; (function f [ ; (oarg1 oarg2 ... <- op arg1 arg2 ...) ; ... ; ... ; ]) ; ; Each arg/oarg can contain metadata separated by slashes and colons. In this ; first example below, 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.) ; In the future a simple tool will check that the types line up as expected in ; each op. A different tool might add types where they aren't provided. ; Instead of a monolithic compiler I want to build simple, lightweight tools ; that can be combined in various ways, say for using different typecheckers ; in different subsystems. ; ; In our tests we'll define such mu functions using a call to 'add-code', so ; look for it when reading the code examples. Everything outside 'add-code' is ; just test-harness details that can be skipped at first. (reset) ;? (set dump-trace*) (new-trace "literal") (add-code '((function main [ (1:integer <- copy 23:literal) ]))) ;? (set dump-trace*) (run 'main) ;? (prn memory*) (when (~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.")) ;? (quit) ; Our basic arithmetic ops can operate on memory locations or literals. ; (Ignore hardware details like registers for now.) (reset) (new-trace "add") (add-code '((function main [ (1:integer <- copy 1:literal) (2:integer <- copy 3:literal) (3:integer <- add 1:integer 2:integer) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 1 2 3 3 4)) (prn "F - 'add' operates on two addresses")) ;? (quit) (reset) (new-trace "add-literal") (add-code '((function main [ (1:integer <- add 2:literal 3:literal) ]))) (run 'main) (when (~is memory*.1 5) (prn "F - ops can take 'literal' operands (but not return them)")) (reset) (new-trace "sub-literal") (add-code '((function main [ (1:integer <- subtract 1:literal 3:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 -2) (prn "F - 'subtract'")) (reset) (new-trace "mul-literal") (add-code '((function main [ (1:integer <- multiply 2:literal 3:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 6) (prn "F - 'multiply'")) (reset) (new-trace "div-literal") (add-code '((function main [ (1:integer <- divide 8:literal 3:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 (/ real.8 3)) (prn "F - 'divide'")) (reset) (new-trace "idiv-literal") (add-code '((function main [ (1:integer 2:integer <- divide-with-remainder 23:literal 6:literal) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 3 2 5)) (prn "F - 'divide-with-remainder' performs integer division")) (reset) (new-trace "dummy-oarg") ;? (set dump-trace*) (add-code '((function main [ (_ 2:integer <- divide-with-remainder 23:literal 6:literal) ]))) (run 'main) (when (~iso memory* (obj 2 5)) (prn "F - '_' oarg can ignore some results")) ;? (quit) ; Basic boolean operations: and, or, not ; There are easy ways to encode booleans in binary, but we'll skip past those ; details for now. (reset) (new-trace "and-literal") (add-code '((function main [ (1:boolean <- and t:literal nil:literal) ]))) ;? (set dump-trace*) (run 'main) ;? (prn memory*) (when (~is memory*.1 nil) (prn "F - logical 'and' for booleans")) ; Basic comparison operations (reset) (new-trace "lt-literal") (add-code '((function main [ (1:boolean <- less-than 4:literal 3:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 nil) (prn "F - 'less-than' inequality operator")) (reset) (new-trace "le-literal-false") (add-code '((function main [ (1:boolean <- lesser-or-equal 4:literal 3:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 nil) (prn "F - 'lesser-or-equal'")) (reset) (new-trace "le-literal-true") (add-code '((function main [ (1:boolean <- lesser-or-equal 4:literal 4:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 t) (prn "F - 'lesser-or-equal' returns true for equal operands")) (reset) (new-trace "le-literal-true-2") (add-code '((function main [ (1:boolean <- lesser-or-equal 4:literal 5:literal) ]))) (run 'main) ;? (prn memory*) (when (~is memory*.1 t) (prn "F - 'lesser-or-equal' - 2")) ; Control flow operations: jump, jump-if, jump-unless ; These introduce a new type -- 'offset' -- for literals that refer to memory ; locations relative to the current location. (reset) (new-trace "jump-skip") (add-code '((function main [ (1:integer <- copy 8:literal) (jump 1:offset) (2:integer <- copy 3:literal) ; should be skipped (reply) ]))) ;? (set dump-trace*) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 8)) (prn "F - 'jump' skips some instructions")) ;? (quit) (reset) (new-trace "jump-target") (add-code '((function main [ (1:integer <- copy 8:literal) (jump 1:offset) (2:integer <- copy 3:literal) ; should be skipped (reply) (3:integer <- copy 34:literal) ]))) ; never reached (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 8)) (prn "F - 'jump' doesn't skip too many instructions")) ;? (quit) (reset) (new-trace "jump-if-skip") (add-code '((function main [ (2:integer <- copy 1:literal) (1:boolean <- equal 1:literal 2:integer) (jump-if 1:boolean 1:offset) (2:integer <- copy 3:literal) (reply) (3:integer <- copy 34:literal) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 t 2 1)) (prn "F - 'jump-if' is a conditional 'jump'")) (reset) (new-trace "jump-if-fallthrough") (add-code '((function main [ (1:boolean <- equal 1:literal 2:literal) (jump-if 3:boolean 1:offset) (2:integer <- copy 3:literal) (reply) (3:integer <- copy 34:literal) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 nil 2 3)) (prn "F - if 'jump-if's first arg is false, it doesn't skip any instructions")) (reset) (new-trace "jump-if-backward") (add-code '((function main [ (1:integer <- copy 2:literal) (2:integer <- copy 1:literal) ; loop (2:integer <- add 2:integer 2:integer) (3:boolean <- equal 1:integer 2:integer) (jump-if 3:boolean -3:offset) ; to loop (4:integer <- copy 3:literal) (reply) (3:integer <- copy 34:literal) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 2 2 4 3 nil 4 3)) (prn "F - 'jump-if' can take a negative offset to make backward jumps")) (reset) (new-trace "jump-label") (add-code '((function main [ (1:integer <- copy 2:literal) (2:integer <- copy 1:literal) loop (2:integer <- add 2:integer 2:integer) (3:boolean <- equal 1:integer 2:integer) (jump-if 3:boolean loop:offset) (4:integer <- copy 3:literal) (reply) (3:integer <- copy 34:literal) ]))) ;? (set dump-trace*) ;? (= dump-trace* (obj whitelist '("-"))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 2 2 4 3 nil 4 3)) (prn "F - 'jump-if' can take a negative offset to make backward jumps")) ;? (quit) ; 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'. (reset) (new-trace "direct-addressing") (add-code '((function main [ (1:integer <- copy 34:literal) (2:integer <- copy 1:integer) ]))) (run 'main) ;? (prn memory*) (when (~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. (reset) (new-trace "indirect-addressing") (add-code '((function main [ (1:integer-address <- copy 2:literal) ; unsafe; can't do this in general (2:integer <- copy 34:literal) (3:integer <- copy 1:integer-address/deref) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 2 2 34 3 34)) (prn "F - 'copy' performs indirect addressing")) ; 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). (reset) (new-trace "indirect-addressing-oarg") (add-code '((function main [ (1:integer-address <- copy 2:literal) (2:integer <- copy 34:literal) (1:integer-address/deref <- add 2:integer 2:literal) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 2 2 36)) (prn "F - instructions can perform indirect addressing on output arg")) ;; Compound data types ; ; Until now we've dealt with scalar types like integers and booleans and ; addresses, where mu looks like other assembly languages. In addition, mu ; provides first-class support for compound types: arrays and and-records. ; ; 'get' accesses fields in and-records ; 'index' accesses indices in arrays ; ; Both operations require knowledge about the types being worked on, so all ; types used in mu programs are defined in a single global system-wide table ; (see type* in mu.arc for the complete list of types; we'll add to it over ; time). ; first a sanity check that the table of types is consistent (reset) (each (typ typeinfo) type* (when typeinfo!and-record (assert (is typeinfo!size (len typeinfo!elems))) (when typeinfo!fields (assert (is typeinfo!size (len typeinfo!fields)))))) (reset) (new-trace "get-record") (add-code '((function main [ (1:integer <- copy 34:literal) (2:boolean <- copy nil:literal) (3:boolean <- get 1:integer-boolean-pair 1:offset) (4:integer <- get 1:integer-boolean-pair 0:offset) ]))) ;? (set dump-trace*) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 34 2 nil 3 nil 4 34)) (prn "F - 'get' accesses fields of and-records")) ;? (quit) (reset) (new-trace "get-indirect") (add-code '((function main [ (1:integer <- copy 34:literal) (2:boolean <- copy nil:literal) (3:integer-boolean-pair-address <- copy 1:literal) (4:boolean <- get 3:integer-boolean-pair-address/deref 1:offset) (5:integer <- get 3:integer-boolean-pair-address/deref 0:offset) ]))) ;? (set dump-trace*) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 34 2 nil 3 1 4 nil 5 34)) (prn "F - 'get' accesses fields of and-record address")) (reset) (new-trace "get-indirect-repeated") (add-code '((function main [ (1:integer <- copy 34:literal) (2:integer <- copy 35:literal) (3:integer <- copy 36:literal) (4:integer-point-pair-address <- copy 1:literal) ; unsafe (5:integer-point-pair-address-address <- copy 4:literal) ; unsafe (6:integer-integer-pair <- get 5:integer-point-pair-address-address/deref/deref 1:offset) (8:integer <- get 5:integer-point-pair-address-address/deref/deref 0:offset) ]))) (run 'main) (when (~memory-contains 6 '(35 36 34)) (prn "F - 'get' can deref multiple times")) ;? (quit) (reset) (new-trace "get-compound-field") (add-code '((function main [ (1:integer <- copy 34:literal) (2:integer <- copy 35:literal) (3:integer <- copy 36:literal) (4:integer-integer-pair <- get 1:integer-point-pair 1:offset) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 34 2 35 3 36 4 35 5 36)) (prn "F - 'get' accesses fields spanning multiple locations")) (reset) (new-trace "get-address") (add-code '((function main [ (1:integer <- copy 34:literal) (2:boolean <- copy t:literal) (3:boolean-address <- get-address 1:integer-boolean-pair 1:offset) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 34 2 t 3 2)) (prn "F - 'get-address' returns address of fields of and-records")) (reset) (new-trace "get-address-indirect") (add-code '((function main [ (1:integer <- copy 34:literal) (2:boolean <- copy t:literal) (3:integer-boolean-pair-address <- copy 1:literal) (4:boolean-address <- get-address 3:integer-boolean-pair-address/deref 1:offset) ]))) (run 'main) ;? (prn memory*) (when (~iso memory* (obj 1 34 2 t 3 1 4 2)) (prn "F - 'get-address' accesses fields of and-record address")) (reset) (new-trace "index-literal") (add-code '((function main [ (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:liter /* Generic.Subheading */ .highlight .gt { color: #aa0000 } /* Generic.Traceback */ .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #008800 } /* Keyword.Pseudo */ .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
//: Take raw control of the text-mode display and console, putting it in
//: 'console' mode rather than the usual automatically-scrolling 'typewriter'
//: mode.
//:: Display management
:(before "End Globals")
int Display_row = 0;
int Display_column = 0;
:(before "End Includes")
#define CHECK_SCREEN \
if (!tb_is_active()) { \
if (Run_tests) \
raise << maybe(current_recipe_name()) << "tried to print to real screen in a test!\n" << end(); \
else \
raise << maybe(current_recipe_name()) << "tried to print to real screen before 'open-console' or after 'close-console'\n" << end(); \
break; \
}
#define CHECK_CONSOLE \
if (!tb_is_active()) { \
if (Run_tests) \
raise << maybe(current_recipe_name()) << "tried to read event from real keyboard/mouse in a test!\n" << end(); \
else \
raise << maybe(current_recipe_name()) << "tried to read event from real keyboard/mouse before 'open-console' or after 'close-console'\n" << end(); \
break; \
}
:(before "End Primitive Recipe Declarations")
OPEN_CONSOLE,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "open-console", OPEN_CONSOLE);
:(before "End Primitive Recipe Checks")
case OPEN_CONSOLE: {
break;
}
:(before "End Primitive Recipe Implementations")
case OPEN_CONSOLE: {
tb_init();
std::setvbuf(stdout, NULL, _IONBF, 0); // disable buffering in cout
Display_row = Display_column = 0;
int width = tb_width();
int height = tb_height();
if (width > 222 || height > 222) {
if (width > 222)
raise << "sorry, Mu doesn't support windows wider than 222 characters in console mode. Please resize your window.\n" << end();
if (height > 222)
raise << "sorry, Mu doesn't support windows taller than 222 characters in console mode. Please resize your window.\n" << end();
exit(1);
}
break;
}
:(before "End Primitive Recipe Declarations")
CLOSE_CONSOLE,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "close-console", CLOSE_CONSOLE);
:(before "End Primitive Recipe Checks")
case CLOSE_CONSOLE: {
break;
}
:(before "End Primitive Recipe Implementations")
case CLOSE_CONSOLE: {
tb_shutdown();
break;
}
:(before "End Primitive Recipe Declarations")
CLEAR_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "clear-display", CLEAR_DISPLAY);
:(before "End Primitive Recipe Checks")
case CLEAR_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case CLEAR_DISPLAY: {
CHECK_SCREEN;
tb_clear();
Display_row = Display_column = 0;
break;
}
:(before "End Primitive Recipe Declarations")
PRINT_CHARACTER_TO_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "print-character-to-display", PRINT_CHARACTER_TO_DISPLAY);
:(before "End Primitive Recipe Checks")
case PRINT_CHARACTER_TO_DISPLAY: {
if (inst.ingredients.empty()) {
raise << maybe(get(Recipe, r).name) << "'print-character-to-display' requires at least one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
break;
}
if (!is_mu_number(inst.ingredients.at(0))) {
raise << maybe(get(Recipe, r).name) << "first ingredient of 'print-character-to-display' should be a character, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
break;
}
if (SIZE(inst.ingredients) > 1) {
if (!is_mu_number(inst.ingredients.at(1))) {
raise << maybe(get(Recipe, r).name) << "second ingredient of 'print-character-to-display' should be a foreground color number, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
break;
}
}
if (SIZE(inst.ingredients) > 2) {
if (!is_mu_number(inst.ingredients.at(2))) {
raise << maybe(get(Recipe, r).name) << "third ingredient of 'print-character-to-display' should be a background color number, but got '" << inst.ingredients.at(2).original_string << "'\n" << end();
break;
}
}
break;
}
:(before "End Primitive Recipe Implementations")
case PRINT_CHARACTER_TO_DISPLAY: {
CHECK_SCREEN;
int h=tb_height(), w=tb_width();
int height = (h >= 0) ? h : 0;
int width = (w >= 0) ? w : 0;
int c = ingredients.at(0).at(0);
int color = TB_WHITE;
if (SIZE(ingredients) > 1) {
color = ingredients.at(1).at(0);
}
int bg_color = TB_BLACK;
if (SIZE(ingredients) > 2) {
bg_color = ingredients.at(2).at(0);
if (bg_color == 0) bg_color = TB_BLACK;
}
tb_print(c, color, bg_color);
// track row and column, mimicking what happens on screen
if (c == '\n') {
if (Display_row < height-1) ++Display_row; // otherwise we scroll and Display_row remains unchanged
}
else if (c == '\r') {
Display_column = 0;
}
else if (c == '\b') {
if (Display_column > 0) --Display_column;
}
else {
++Display_column;
if (Display_column >= width) {
Display_column = 0;
if (Display_row < height-1) ++Display_row;
}
}
break;
}
:(before "End Primitive Recipe Declarations")
CURSOR_POSITION_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "cursor-position-on-display", CURSOR_POSITION_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case CURSOR_POSITION_ON_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case CURSOR_POSITION_ON_DISPLAY: {
CHECK_SCREEN;
products.resize(2);
products.at(0).push_back(Display_row);
products.at(1).push_back(Display_column);
break;
}
:(before "End Primitive Recipe Declarations")
MOVE_CURSOR_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "move-cursor-on-display", MOVE_CURSOR_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case MOVE_CURSOR_ON_DISPLAY: {
if (SIZE(inst.ingredients) != 2) {
raise << maybe(get(Recipe, r).name) << "'move-cursor-on-display' requires two ingredients, but got '" << to_original_string(inst) << "'\n" << end();
break;
}
if (!is_mu_number(inst.ingredients.at(0))) {
raise << maybe(get(Recipe, r).name) << "first ingredient of 'move-cursor-on-display' should be a row number, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
break;
}
if (!is_mu_number(inst.ingredients.at(1))) {
raise << maybe(get(Recipe, r).name) << "second ingredient of 'move-cursor-on-display' should be a column number, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
break;
}
break;
}
:(before "End Primitive Recipe Implementations")
case MOVE_CURSOR_ON_DISPLAY: {
CHECK_SCREEN;
Display_row = ingredients.at(0).at(0);
Display_column = ingredients.at(1).at(0);
tb_set_cursor(Display_column, Display_row);
break;
}
:(before "End Primitive Recipe Declarations")
MOVE_CURSOR_DOWN_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "move-cursor-down-on-display", MOVE_CURSOR_DOWN_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case MOVE_CURSOR_DOWN_ON_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case MOVE_CURSOR_DOWN_ON_DISPLAY: {
CHECK_SCREEN;
int h=tb_height();
int height = (h >= 0) ? h : 0;
if (Display_row < height-1) {
++Display_row;
tb_set_cursor(Display_column, Display_row);
}
break;
}
:(before "End Primitive Recipe Declarations")
MOVE_CURSOR_UP_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "move-cursor-up-on-display", MOVE_CURSOR_UP_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case MOVE_CURSOR_UP_ON_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case MOVE_CURSOR_UP_ON_DISPLAY: {
CHECK_SCREEN;
if (Display_row > 0) {
--Display_row;
tb_set_cursor(Display_column, Display_row);
}
break;
}
:(before "End Primitive Recipe Declarations")
MOVE_CURSOR_RIGHT_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "move-cursor-right-on-display", MOVE_CURSOR_RIGHT_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case MOVE_CURSOR_RIGHT_ON_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case MOVE_CURSOR_RIGHT_ON_DISPLAY: {
CHECK_SCREEN;
int w=tb_width();
int width = (w >= 0) ? w : 0;
if (Display_column < width-1) {
++Display_column;
tb_set_cursor(Display_column, Display_row);
}
break;
}
:(before "End Primitive Recipe Declarations")
MOVE_CURSOR_LEFT_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "move-cursor-left-on-display", MOVE_CURSOR_LEFT_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case MOVE_CURSOR_LEFT_ON_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case MOVE_CURSOR_LEFT_ON_DISPLAY: {
CHECK_SCREEN;
if (Display_column > 0) {
--Display_column;
tb_set_cursor(Display_column, Display_row);
}
break;
}
//: as a convenience, make $print mostly work in console mode
:(before "End $print 10/newline Special-cases")
else if (tb_is_active()) {
move_cursor_to_start_of_next_line_on_display();
}
:(code)
void move_cursor_to_start_of_next_line_on_display() {
if (Display_row < tb_height()-1) ++Display_row;
else Display_row = 0;
Display_column = 0;
tb_set_cursor(Display_column, Display_row);
}
:(before "End Primitive Recipe Declarations")
DISPLAY_WIDTH,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "display-width", DISPLAY_WIDTH);
:(before "End Primitive Recipe Checks")
case DISPLAY_WIDTH: {
break;
}
:(before "End Primitive Recipe Implementations")
case DISPLAY_WIDTH: {
CHECK_SCREEN;
products.resize(1);
products.at(0).push_back(tb_width());
break;
}
:(before "End Primitive Recipe Declarations")
DISPLAY_HEIGHT,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "display-height", DISPLAY_HEIGHT);
:(before "End Primitive Recipe Checks")
case DISPLAY_HEIGHT: {
break;
}
:(before "End Primitive Recipe Implementations")
case DISPLAY_HEIGHT: {
CHECK_SCREEN;
products.resize(1);
products.at(0).push_back(tb_height());
break;
}
//:: Keyboard/mouse management
:(before "End Primitive Recipe Declarations")
WAIT_FOR_SOME_INTERACTION,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "wait-for-some-interaction", WAIT_FOR_SOME_INTERACTION);
:(before "End Primitive Recipe Checks")
case WAIT_FOR_SOME_INTERACTION: {
break;
}
:(before "End Primitive Recipe Implementations")
case WAIT_FOR_SOME_INTERACTION: {
CHECK_SCREEN;
tb_event event;
tb_poll_event(&event);
break;
}
:(before "End Primitive Recipe Declarations")
CHECK_FOR_INTERACTION,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "check-for-interaction", CHECK_FOR_INTERACTION);
:(before "End Primitive Recipe Checks")
case CHECK_FOR_INTERACTION: {
break;
}
:(before "End Primitive Recipe Implementations")
case CHECK_FOR_INTERACTION: {
CHECK_CONSOLE;
products.resize(2); // result and status
tb_event event;
int event_type = tb_peek_event(&event, 5/*ms*/);
if (event_type == TB_EVENT_KEY && event.ch) {
products.at(0).push_back(/*text event*/0);
products.at(0).push_back(event.ch);
products.at(0).push_back(0);
products.at(0).push_back(0);
products.at(1).push_back(/*found*/true);
break;
}
// treat keys within ascii as unicode characters
if (event_type == TB_EVENT_KEY && event.key < 0xff) {
products.at(0).push_back(/*text event*/0);
if (event.key == TB_KEY_CTRL_C) exit(1);
if (event.key == TB_KEY_BACKSPACE2) event.key = TB_KEY_BACKSPACE;
if (event.key == TB_KEY_CARRIAGE_RETURN) event.key = TB_KEY_NEWLINE;
products.at(0).push_back(event.key);
products.at(0).push_back(0);
products.at(0).push_back(0);
products.at(1).push_back(/*found*/true);
break;
}
// keys outside ascii aren't unicode characters but arbitrary termbox inventions
if (event_type == TB_EVENT_KEY) {
products.at(0).push_back(/*keycode event*/1);
products.at(0).push_back(event.key);
products.at(0).push_back(0);
products.at(0).push_back(0);
products.at(1).push_back(/*found*/true);
break;
}
if (event_type == TB_EVENT_MOUSE) {
products.at(0).push_back(/*touch event*/2);
products.at(0).push_back(event.key); // which button, etc.
products.at(0).push_back(event.y); // row
products.at(0).push_back(event.x); // column
products.at(1).push_back(/*found*/true);
break;
}
if (event_type == TB_EVENT_RESIZE) {
products.at(0).push_back(/*resize event*/3);
products.at(0).push_back(event.w); // width
products.at(0).push_back(event.h); // height
products.at(0).push_back(0);
products.at(1).push_back(/*found*/true);
break;
}
assert(event_type == 0);
products.at(0).push_back(0);
products.at(0).push_back(0);
products.at(0).push_back(0);
products.at(0).push_back(0);
products.at(1).push_back(/*found*/false);
break;
}
:(before "End Primitive Recipe Declarations")
INTERACTIONS_LEFT,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "interactions-left?", INTERACTIONS_LEFT);
:(before "End Primitive Recipe Checks")
case INTERACTIONS_LEFT: {
break;
}
:(before "End Primitive Recipe Implementations")
case INTERACTIONS_LEFT: {
CHECK_CONSOLE;
products.resize(1);
products.at(0).push_back(tb_event_ready());
break;
}
//: hacks to make text-mode apps more responsive under Unix
:(before "End Primitive Recipe Declarations")
CLEAR_LINE_ON_DISPLAY,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "clear-line-on-display", CLEAR_LINE_ON_DISPLAY);
:(before "End Primitive Recipe Checks")
case CLEAR_LINE_ON_DISPLAY: {
break;
}
:(before "End Primitive Recipe Implementations")
case CLEAR_LINE_ON_DISPLAY: {
CHECK_SCREEN;
int width = tb_width();
for (int x = Display_column; x < width; ++x)
tb_print(' ', TB_WHITE, TB_BLACK);
tb_set_cursor(Display_column, Display_row);
break;
}
:(before "End Primitive Recipe Declarations")
CLEAR_DISPLAY_FROM,
:(before "End Primitive Recipe Numbers")
put(Recipe_ordinal, "clear-display-from", CLEAR_DISPLAY_FROM);
:(before "End Primitive Recipe Checks")
case CLEAR_DISPLAY_FROM: {
break;
}
:(before "End Primitive Recipe Implementations")
case CLEAR_DISPLAY_FROM: {
CHECK_SCREEN;
// todo: error checking
int row = ingredients.at(0).at(0);
int column = ingredients.at(1).at(0);
int left = ingredients.at(2).at(0);
int right = ingredients.at(3).at(0);
int height=tb_height();
for (/*nada*/; row < height; ++row, column=left) { // start column from left in every inner loop except first
tb_set_cursor(column, row);
for (/*nada*/; column <= right; ++column)
tb_print(' ', TB_WHITE, TB_BLACK);
}
tb_set_cursor(Display_column, Display_row);
break;
}