## running code from the editor and creating sandboxes # # Running code in the sandbox editor prepends its contents to a list of # (non-editable) sandboxes below the editor, showing the result and a maybe # few other things. # # This layer draws the menubar buttons non-editable sandboxes but they don't # do anything yet. Later layers implement each button. container environment [ sandbox:&:sandbox # list of sandboxes, from top to bottom render-from:num number-of-sandboxes:num ] after [ *result <- put *result, render-from:offset, -1 ] container sandbox [ data:text response:text # coordinates to track clicks # constraint: will be 0 for sandboxes at positions before env.render-from starting-row-on-screen:num code-ending-row-on-screen:num # past end of code screen:&:screen # prints in the sandbox go here next-sandbox:&:sandbox ] scenario run-and-show-results [ local-scope trace-until 100/app # trace too long assume-screen 50/width, 15/height # sandbox editor contains an instruction without storing outputs env:&:environment <- new-programming-environment screen, [divide-with-remainder 11, 3] # run the code in the editors assume-console [ press F4 ] run [ event-loop screen, console, env ] # check that screen prints the results screen-should-contain [ . run (F4) . . . .──────────────────────────────────────────────────. .0 edit copy delete . .divide-with-remainder 11, 3 . .3 . .2 . .──────────────────────────────────────────────────. . . ] screen-should-contain-in-color 7/white, [ . . . . . . . . .divide-with-remainder 11, 3 . . . . . . . . . ] screen-should-contain-in-color 245/grey, [ . . . . .──────────────────────────────────────────────────. . . . . .3 . .2 . .──────────────────────────────────────────────────. . . ] # run another command assume-console [ left-click 1, 80 type [add 2, 2] press F4 ] run [ event-loop screen, console, env ] # check that screen prints both sandboxes screen-should-contain [ . run (F4) . . . .──────────────────────────────────────────────────. .0 edit copy delete . .add 2, 2 . .4 . .──────────────────────────────────────────────────. .1 edit copy delete . .divide-with-remainder 11, 3 . .3 . .2 . .──────────────────────────────────────────────────. . . ] ] after [ # F4? load all code and run all sandboxes. { do-run?:bool <- equal k, 65532/F4 break-unless do-run? screen <- update-status screen, [running... ], 245/grey test-recipes:text, _/optional <- next-ingredient error?:bool, env, screen <- run-sandboxes env, screen, test-recipes #? test-recipes <- copy 0 # abandon # F4 might update warnings and results on both sides screen <- render-all screen, env, render { break-if error? screen <- update-status screen, [ ], 245/grey } screen <- update-cursor screen, current-sandbox, env loop +next-event } ] def run-sandboxes env:&:environment, screen:&:screen, test-recipes:text -> errors-found?:bool, env:&:environment, screen:&:screen [ local-scope load-ingredients errors-found?:bool, env, screen <- update-recipes env, screen, test-recipes # check contents of editor current-sandbox:&:editor <- get *env, current-sandbox:offset { sandbox-contents:text <- editor-contents current-sandbox break-unless sandbox-contents # if contents exist, first save them # run them and turn them into a new sandbox new-sandbox:&:sandbox <- new sandbox:type *new-sandbox <- put *new-sandbox, data:offset, sandbox-contents # push to head of sandbox list dest:&:sandbox <- get *env, sandbox:offset *new-sandbox <- put *new-sandbox, next-sandbox:offset, dest *env <- put *env, sandbox:offset, new-sandbox # update sandbox count sandbox-count:num <- get *env, number-of-sandboxes:offset sandbox-count <- add sandbox-count, 1 *env <- put *env, number-of-sandboxes:offset, sandbox-count # clear sandbox editor init:&:duplex-list:char <- push 167/§, 0/tail *current-sandbox <- put *current-sandbox, data:offset, init *current-sandbox <- put *current-sandbox, top-of-screen:offset, init } # save all sandboxes before running, just in case we die when running save-sandboxes env # run all sandboxes curr:&:sandbox <- get *env, sandbox:offset idx:num <- copy 0 { break-unless curr curr <- update-sandbox curr, env, idx curr <- get *curr, next-sandbox:offset idx <- add idx, 1 loop } ] # load code from recipes.mu, or from test-recipes in tests # replaced in a later layer (whereupon errors-found? will actually be set) def update-recipes env:&:environment, screen:&:screen, test-recipes:text -> errors-found?:bool, env:&:environment, screen:&:screen [ local-scope load-ingredients { break-if test-recipes in:text <- restore [recipes.mu] # newlayer: persistence reload in } { break-unless test-recipes reload test-recipes } errors-found? <- copy 0/false ] # replaced in a later layer def! update-sandbox sandbox:&:sandbox, env:&:environment, idx:num -> sandbox:&:sandbox, env:&:environment [ local-scope load-ingredients data:text <- get *sandbox, data:offset response:text, _, fake-screen:&:screen <- run-sandboxed data *sandbox <- put *sandbox, response:offset, response *sandbox <- put *sandbox, screen:offset, fake-screen ] def update-status screen:&:screen, msg:text, color:num -> screen:&:screen [ local-scope load-ingredients screen <- move-cursor screen, 0, 2 screen <- print screen, msg, color, 238/grey/background ] def save-sandboxes env:&:environment [ local-scope load-ingredients current-sandbox:&:editor <- get *env, current-sandbox:offset # first clear previous versions, in case we deleted some sandbox $system [rm lesson/[0-9]* >/dev/null 2>/dev/null] # some shells can't handle '>&' curr:&:sandbox <- get *env, sandbox:offset idx:num <- copy 0 { break-unless curr data:text <- get *curr, data:offset filename:text <- to-text idx save filename, data idx <- add idx, 1 curr <- get *curr, next-sandbox:offset loop } ] def! render-sandbox-side screen:&:screen, env:&:environment, {render-editor: (recipe (address screen) (address editor) -> number number (address screen) (address editor))} -> screen:&:screen, env:&:environment [ local-scope load-ingredients trace 11, [app], [render sandbox side] current-sandbox:&:editor <- get *env, current-sandbox:offset row:num, column:num <- copy 1, 0 left:num <- get *current-sandbox, left:offset right:num <- get *current-sandbox, right:offset # render sandbox editor render-from:num <- get *env, render-from:offset { render-current-sandbox?:bool <- equal render-from, -1 break-unless render-current-sandbox? row, column, screen, current-sandbox <- call render-editor, screen, current-sandbox clear-screen-from screen, row, column, left, right row <- add row, 1 } # render sandboxes draw-horizontal screen, row, left, right sandbox:&:sandbox <- get *env, sandbox:offset row, screen <- render-sandboxes screen, sandbox, left, right, row, render-from, 0, env clear-rest-of-screen screen, row, left, right ] def render-sandboxes screen:&:screen, sandbox:&:sandbox, left:num, right:num, row:num, render-from:num, idx:num -> row:num, screen:&:screen, sandbox:&:sandbox [ local-scope load-ingredients env:&:environment, _/optional <- next-ingredient return-unless sandbox screen-height:num <- screen-height screen at-bottom?:bool <- greater-or-equal row, screen-height return-if at-bottom?:bool hidden?:bool <- lesser-than idx, render-from { break-if hidden? # render sandbox menu row <- add row, 1 screen <- move-cursor screen, row, left screen <- render-sandbox-menu screen, idx, left, right # save menu row so we can detect clicks to it later *sandbox <- put *sandbox, starting-row-on-screen:offset, row # render sandbox contents row <- add row, 1 screen <- move-cursor screen, row, left sandbox-data:text <- get *sandbox, data:offset row, screen <- render-code screen, sandbox-data, left, right, row *sandbox <- put *sandbox, code-ending-row-on-screen:offset, row # render sandbox warnings, screen or response, in that order sandbox-response:text <- get *sandbox, response:offset { sandbox-screen:&:screen <- get *sandbox, screen:offset empty-screen?:bool <- fake-screen-is-empty? sandbox-screen break-if empty-screen? row, screen <- render-screen screen, sandbox-screen, left, right, row } { break-unless empty-screen? row, screen <- render-text screen, sandbox-response, left, right, 245/grey, row } +render-sandbox-end at-bottom?:bool <- greater-or-equal row, screen-height return-if at-bottom? # draw solid line after sandbox draw-horizontal screen, row, left, right } # if hidden, reset row attributes { break-unless hidden? *sandbox <- put *sandbox, starting-row-on-screen:offset, 0 *sandbox <- put *sandbox, code-ending-row-on-screen:offset, 0 } # draw next sandbox next-sandbox:&:sandbox <- get *sandbox, next-sandbox:offset next-idx:num <- add idx, 1 row, screen <- render-sandboxes screen, next-sandbox, left, right, row, render-from, next-idx, env ] def render-sandbox-menu screen:&:screen, sandbox-index:num, left:num, right:num -> screen:&:screen [ local-scope load-ingredients move-cursor-to-column screen, left edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, delete-button-left:num <- sandbox-menu-columns left, right print screen, sandbox-index, 232/dark-grey, 245/grey start-buttons:num <- subtract edit-button-left, 1 clear-line-until screen, start-buttons, 245/grey print screen, [edit], 232/black, 94/background-orange clear-line-until screen, edit-button-right, 94/background-orange _, col:num <- cursor-position screen at-start-of-copy-button?:bool <- equal col, copy-button-left assert at-start-of-copy-button?, [aaa] print screen, [copy], 232/black, 58/background-green clear-line-until screen, copy-button-right, 58/background-green _, col:num <- cursor-position screen at-start-of-delete-button?:bool <- equal col, delete-button-left assert at-start-of-delete-button?, [bbb] print screen, [delete], 232/black, 52/background-red clear-line-until screen, right, 52/background-red ] # divide up the menu bar for a sandbox into 3 segments, for edit/copy/delete buttons # delete-button-right == right # all left/right pairs are inclusive def sandbox-menu-columns left:num, right:num -> edit-button-left:num, edit-button-right:num, copy-button-left:num, copy-button-right:num, delete-button-left:num [ local-scope load-ingredients start-buttons:num <- add left, 4/space-for-sandbox-index buttons-space:num <- subtract right, start-buttons button-width:num <- divide-with-remainder buttons-space, 3 # integer division buttons-wide-enough?:bool <- greater-or-equal button-width, 8 assert buttons-wide-enough?, [sandbox must be at least 30 or so characters wide] edit-button-left:num <- copy start-buttons copy-button-left:num <- add start-buttons, button-width edit-button-right:num <- subtract copy-button-left, 1 delete-button-left:num <- subtract right, button-width copy-button-right:num <- subtract delete-button-left, 1 ] # print a text 's' to 'editor' in 'color' starting at 'row' # clear rest of last line, move cursor to next line def render-text screen:&:screen, s:text, left:num, right:num, color:num, row:num -> row:num, screen:&:screen [ local-scope load-ingredients return-unless s column:num <- copy left screen <- move-cursor screen, row, column screen-height:num <- screen-height screen i:num <- copy 0 len:num <- length *s { +next-character done?:bool <- greater-or-equal i, len break-if done? done? <- greater-or-equal row, screen-height break-if done? c:char <- index *s, i { # at right? wrap. at-right?:bool <- equal column, right break-unless at-right? # print wrap icon wrap-icon:char <- copy 8617/loop-back-to-left print screen, wrap-icon, 245/grey column <- copy left row <- add row, 1 screen <- move-cursor screen, row, column loop +next-character # retry i } i <- add i, 1 { # newline? move to left rather than 0 newline?:bool <- equal c, 10/newline break-unless newline? # clear rest of line in this window { done?:bool <- greater-than column, right break-if done? space:char <- copy 32/space print screen, space column <- add column, 1 loop } row <- add row, 1 column <- copy left screen <- move-cursor screen, row, column loop +next-character } print screen, c, color column <- add column, 1 loop } was-at-left?:bool <- equal column, left clear-line-until screen, right { break-if was-at-left? row <- add row, 1 } move-cursor screen, row, left ] # assumes programming environment has no sandboxes; restores them from previous session def! restore-sandboxes env:&:environment -> env:&:environment [ local-scope load-ingredients # read all scenarios, pushing them to end of a list of scenarios idx:num <- copy 0 curr:&:sandbox <- copy 0 prev:&:sandbox <- copy 0 { filename:text <- to-text idx contents:text <- restore filename break-unless contents # stop at first error; assuming file didn't exist # todo: handle empty sandbox # create new sandbox for file curr <- new sandbox:type *curr <- put *curr, data:offset, contents { break-if idx *env <- put *env, sandbox:offset, curr } { break-unless idx *prev <- put *prev, next-sandbox:offset, curr } idx <- add idx, 1 prev <- copy curr loop } # update sandbox count *env <- put *env, number-of-sandboxes:offset, idx ] # print the fake sandbox screen to 'screen' with appropriate delimiters # leave cursor at start of next line def render-screen screen:&:screen, sandbox-screen:&:screen, left:num, right:num, row:num -> row:num, screen:&:screen [ local-scope load-ingredients return-unless sandbox-screen # print 'screen:' row <- render-text screen, [screen:], left, right, 245/grey, row screen <- move-cursor screen, row, left # start printing sandbox-screen column:num <- copy left s-width:num <- screen-width sandbox-screen s-height:num <- screen-height sandbox-screen buf:&:@:screen-cell <- get *sandbox-screen, data:offset stop-printing:num <- add left, s-width, 3 max-column:num <- min stop-printing, right i:num <- copy 0 len:num <- length *buf screen-height:num <- screen-height screen { done?:bool <- greater-or-equal i, len break-if done? done? <- greater-or-equal row, screen-height break-if done? column <- copy left screen <- move-cursor screen, row, column # initial leader for each row: two spaces and a '.' space:char <- copy 32/space print screen, space, 245/grey print screen, space, 245/grey full-stop:char <- copy 46/period print screen, full-stop, 245/grey column <- add left, 3 { # print row row-done?:bool <- greater-or-equal column, max-column break-if row-done? curr:screen-cell <- index *buf, i c:char <- get curr, contents:offset color:num <- get curr, color:offset { # damp whites down to grey white?:bool <- equal color, 7/white break-unless white? color <- copy 245/grey } print screen, c, color column <- add column, 1 i <- add i, 1 loop } # print final '.' print screen, full-stop, 245/grey column <- add column, 1 { # clear rest of current line line-done?:bool <- greater-than column, right break-if line-done? print screen, space column <- add column, 1 loop } row <- add row, 1 loop } ] scenario run-updates-results [ local-scope trace-until 100/app # trace too long assume-screen 50/width, 12/height # define a recipe (no indent for the 'add' line below so column numbers are more obvious) recipes:text <- new [ recipe foo [ local-scope z:num <- add 2, 2 reply z ]] # sandbox editor contains an instruction without storing outputs env:&:environment <- new-programming-environment screen, [foo] # run the code in the editors assume-console [ press F4 ] event-loop screen, console, env, recipes screen-should-contain [ . run (F4) . . . .──────────────────────────────────────────────────. .0 edit copy delete . .foo . .4 . .──────────────────────────────────────────────────. . . ] # make a change (incrementing one of the args to 'add'), then rerun recipes:
//: The goal of layers is to make programs more easy to understand and more
//: malleable, easy to rewrite in radical ways without accidentally breaking
//: some corner case. Tests further both goals. They help understandability by
//: letting one make small changes and get feedback. What if I wrote this line
//: like so? What if I removed this function call, is it really necessary?
//: Just try it, see if the tests pass. Want to explore rewriting this bit in
//: this way? Tests put many refactorings on a firmer footing.
//:
//: But the usual way we write tests seems incomplete. Refactorings tend to
//: work in the small, but don't help with changes to function boundaries. If
//: you want to extract a new function you have to manually test-drive it to
//: create tests for it. If you want to inline a function its tests are no
//: longer valid. In both cases you end up having to reorganize code as well as
//: tests, an error-prone activity.
//:
//: In response, this layer introduces the notion of domain-driven *white-box*
//: testing. We focus on the domain of inputs the whole program needs to
//: handle rather than the correctness of individual functions. All white-box
//: tests invoke the program in a single way: by calling run() with some
//: input. As the program operates on the input, it traces out a list of
//: _facts_ deduced about the domain:
//:   trace("label") << "fact 1: " << val;
//:
//: Tests can now check for these facts in the trace:
//:   CHECK_TRACE_CONTENTS("label", "fact 1: 34\n"
//:                                 "fact 2: 35\n");
//:
//: Since we never call anything but the run() function directly, we never have
//: to rewrite the tests when we reorganize the internals of the program. We
//: just have to make sure our rewrite deduces the same facts about the domain,
//: and that's something we're going to have to do anyway.
//:
//: To avoid the combinatorial explosion of integration tests, each layer
//: mainly logs facts to the trace with a common *label*. All tests in a layer
//: tend to check facts with this label. Validating the facts logged with a
//: specific label is like calling functions of that layer directly.
//:
//: To build robust tests, trace facts about your domain rather than details of
//: how you computed them.
//:
//: More details: http://akkartik.name/blog/tracing-tests
//:
//: ---
//:
//: Between layers and domain-driven testing, programming starts to look like a
//: fundamentally different activity. Instead of focusing on a) superficial,
//: b) local rules on c) code [like say http://blog.bbv.ch/2013/06/05/clean-code-cheat-sheet],
//: we allow programmers to engage with the a) deep, b) global structure of
//: the c) domain. If you can systematically track discontinuities in the
//: domain, you don't care if the code used gotos as long as it passed all
//: tests. If tests become more robust to run, it becomes easier to try out
//: radically different implementations for the same program. If code is
//: super-easy to rewrite, it becomes less important what indentation style it
//: uses, or that the objects are appropriately encapsulated, or that the
//: functions are referentially transparent.
//:
//: Instead of plumbing, programming becomes building and gradually refining a
//: map of the environment the program must operate under. Whether a program
//: is 'correct' at a given point in time is a red herring; what matters is
//: avoiding regression by monotonically nailing down the more 'eventful'
//: parts of the terrain. It helps readers new and old, and rewards curiosity,
//: to organize large programs in self-similar hierarchies of example tests
//: colocated with the code that makes them work.
//:
//:   "Programming properly should be regarded as an activity by which
//:   programmers form a mental model, rather than as production of a program."
//:   -- Peter Naur (http://alistair.cockburn.us/ASD+book+extract%3A+%22Naur,+Ehn,+Musashi%22)

//:: == Core data structures

:(before "End Globals")
trace_stream* Trace_stream = NULL;

:(before "End Types")
struct trace_stream {
  vector<trace_line> past_lines;
  // End trace_stream Fields

  trace_stream() {
    // End trace_stream Constructor
  }
  ~trace_stream() {
    // End trace_stream Destructor
  }
  // End trace_stream Methods
};

//:: == Adding to the trace

//: Top-level method is trace() which can be used like an ostream. Usage:
//:   trace(depth, label) << ... << end();
//: Don't forget the 'end()' to actually append to the trace.
:(before "End Includes")
// No brackets around the expansion so that it prints nothing if Trace_stream
// isn't initialized.
#define trace(...)  !Trace_stream ? cerr : Trace_stream->stream(__VA_ARGS__)

:(before "End trace_stream Fields")
// accumulator for current trace_line
ostringstream* curr_stream;
string curr_label;
int curr_depth;
// other stuff
int collect_depth;  // avoid tracing lower levels for speed
ofstream null_stream;  // never opened, so writes to it silently fail

//: Some constants.
:(before "struct trace_stream")  // include constants in all cleaved compilation units
const int Max_depth = 9999;
:(before "End trace_stream Constructor")
curr_stream = NULL;
curr_depth = Max_depth;
collect_depth = Max_depth;

:(before "struct trace_stream")
struct trace_line {
  string contents;
  string label;
  int depth;  // 0 is 'sea level'; positive integers are progressively 'deeper' and lower level
  trace_line(string c, string l) {
    contents = c;
    label = l;
    depth = 0;
  }
  trace_line(string c, string l, int d) {
    contents = c;
    label = l;
    depth = d;
  }
};

//: Starting a new trace line.
:(before "End trace_stream Methods")
ostream& stream(string label) {
  return stream(Max_depth, label);
}

ostream& stream(int depth