about summary refs log tree commit diff stats
path: root/edit/005-sandbox.mu
diff options
context:
space:
mode:
Diffstat (limited to 'edit/005-sandbox.mu')
-rw-r--r--edit/005-sandbox.mu683
1 files changed, 683 insertions, 0 deletions
diff --git a/edit/005-sandbox.mu b/edit/005-sandbox.mu
new file mode 100644
index 00000000..239112ce
--- /dev/null
+++ b/edit/005-sandbox.mu
@@ -0,0 +1,683 @@
+## running code from the editor and creating sandboxes
+
+container sandbox-data [
+  data:address:array:character
+  response:address:array:character
+  warnings:address:array:character
+  trace:address:array:character
+  expected-response:address:array:character
+  # coordinates to track clicks
+  starting-row-on-screen:number
+  code-ending-row-on-screen:number
+  response-starting-row-on-screen:number
+  display-trace?:boolean
+  screen:address:screen  # prints in the sandbox go here
+  next-sandbox:address:sandbox-data
+]
+
+scenario run-and-show-results [
+  $close-trace  # trace too long
+  assume-screen 100/width, 15/height
+  # recipe editor is empty
+  1:address:array:character <- new []
+  # sandbox editor contains an instruction without storing outputs
+  2:address:array:character <- new [divide-with-remainder 11, 3]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that screen prints the results
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊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, [
+    .                                                                                                    .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊                                                 .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+  # run another command
+  assume-console [
+    left-click 1, 80
+    type [add 2, 2]
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that screen prints the results
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊add 2, 2                                         .
+    .                                                  ┊4                                                .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊divide-with-remainder 11, 3                      .
+    .                                                  ┊3                                                .
+    .                                                  ┊2                                                .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+]
+
+# hook into event-loop recipe: read non-unicode keypress from k, process it if
+# necessary, then go to next level
+after <global-keypress> [
+  # F4? load all code and run all sandboxes.
+  {
+    do-run?:boolean <- equal *k, 65532/F4
+    break-unless do-run?
+    status:address:array:character <- new [running...  ]
+    screen <- update-status screen, status, 245/grey
+    screen, error?:boolean <- run-sandboxes env, screen
+    # F4 might update warnings and results on both sides
+    screen <- render-all screen, env
+    {
+      break-if error?
+      status:address:array:character <- new [            ]
+      screen <- update-status screen, status, 245/grey
+    }
+    screen <- update-cursor screen, recipes, current-sandbox, *sandbox-in-focus?
+    loop +next-event:label
+  }
+]
+
+recipe run-sandboxes [
+  local-scope
+  env:address:programming-environment-data <- next-ingredient
+  screen:address <- next-ingredient
+  recipes:address:editor-data <- get *env, recipes:offset
+  # copy code from recipe editor, persist, load into mu, save any warnings
+  in:address:array:character <- editor-contents recipes
+  save [recipes.mu], in
+  recipe-warnings:address:address:array:character <- get-address *env, recipe-warnings:offset
+  *recipe-warnings <- reload in
+  # if recipe editor has errors, stop
+  {
+    break-unless *recipe-warnings
+    status:address:array:character <- new [errors found]
+    update-status screen, status, 1/red
+    reply screen/same-as-ingredient:1, 1/errors-found
+  }
+  # check contents of right editor (sandbox)
+  current-sandbox:address:editor-data <- get *env, current-sandbox:offset
+  {
+    sandbox-contents:address:array:character <- editor-contents current-sandbox
+    break-unless sandbox-contents
+    # if contents exist, first save them
+    # run them and turn them into a new sandbox-data
+    new-sandbox:address:sandbox-data <- new sandbox-data:type
+    data:address:address:array:character <- get-address *new-sandbox, data:offset
+    *data <- copy sandbox-contents
+    # push to head of sandbox list
+    dest:address:address:sandbox-data <- get-address *env, sandbox:offset
+    next:address:address:sandbox-data <- get-address *new-sandbox, next-sandbox:offset
+    *next <- copy *dest
+    *dest <- copy new-sandbox
+    # clear sandbox editor
+    init:address:address:duplex-list <- get-address *current-sandbox, data:offset
+    *init <- push-duplex 167/§, 0/tail
+    top-of-screen:address:address:duplex-list <- get-address *current-sandbox, top-of-screen:offset
+    *top-of-screen <- copy *init
+  }
+  # save all sandboxes before running, just in case we die when running
+  save-sandboxes env
+  # run all sandboxes
+  curr:address:sandbox-data <- get *env, sandbox:offset
+  {
+    break-unless curr
+    data <- get-address *curr, data:offset
+    response:address:address:array:character <- get-address *curr, response:offset
+    warnings:address:address:array:character <- get-address *curr, warnings:offset
+    trace:address:address:array:character <- get-address *curr, trace:offset
+    fake-screen:address:address:screen <- get-address *curr, screen:offset
+    *response, *warnings, *fake-screen, *trace, completed?:boolean <- run-interactive *data
+    {
+      break-if *warnings
+      break-if completed?:boolean
+      *warnings <- new [took too long!
+]
+    }
+    curr <- get *curr, next-sandbox:offset
+    loop
+  }
+  reply screen/same-as-ingredient:1, 0/no-errors-found
+]
+
+recipe update-status [
+  local-scope
+  screen:address <- next-ingredient
+  msg:address:array:character <- next-ingredient
+  color:number <- next-ingredient
+  screen <- move-cursor screen, 0, 2
+  screen <- print-string screen, msg, color, 238/grey/background
+  reply screen/same-as-ingredient:0
+]
+
+recipe save-sandboxes [
+  local-scope
+  env:address:programming-environment-data <- next-ingredient
+  current-sandbox:address:editor-data <- 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:address:sandbox-data <- get *env, sandbox:offset
+  suffix:address:array:character <- new [.out]
+  idx:number <- copy 0
+  {
+    break-unless curr
+    data:address:array:character <- get *curr, data:offset
+    filename:address:array:character <- integer-to-decimal-string idx
+    save filename, data
+    {
+      expected-response:address:array:character <- get *curr, expected-response:offset
+      break-unless expected-response
+      filename <- string-append filename, suffix
+      save filename, expected-response
+    }
+    idx <- add idx, 1
+    curr <- get *curr, next-sandbox:offset
+    loop
+  }
+]
+
+recipe render-sandbox-side [
+  local-scope
+  screen:address <- next-ingredient
+  env:address:programming-environment-data <- next-ingredient
+  trace 11, [app], [render sandbox side]
+  current-sandbox:address:editor-data <- get *env, current-sandbox:offset
+  left:number <- get *current-sandbox, left:offset
+  right:number <- get *current-sandbox, right:offset
+  row:number, column:number, screen, current-sandbox <- render screen, current-sandbox
+  clear-screen-from screen, row, column, left, right
+  row <- add row, 1
+  draw-horizontal screen, row, left, right, 9473/horizontal-double
+  sandbox:address:sandbox-data <- get *env, sandbox:offset
+  row, screen <- render-sandboxes screen, sandbox, left, right, row
+  clear-rest-of-screen screen, row, left, left, right
+  reply screen/same-as-ingredient:0
+]
+
+recipe render-sandboxes [
+  local-scope
+  screen:address <- next-ingredient
+  sandbox:address:sandbox-data <- next-ingredient
+  left:number <- next-ingredient
+  right:number <- next-ingredient
+  row:number <- next-ingredient
+  reply-unless sandbox, row/same-as-ingredient:4, screen/same-as-ingredient:0
+  screen-height:number <- screen-height screen
+  at-bottom?:boolean <- greater-or-equal row, screen-height
+  reply-if at-bottom?:boolean, row/same-as-ingredient:4, screen/same-as-ingredient:0
+  # render sandbox menu
+  row <- add row, 1
+  screen <- move-cursor screen, row, left
+  clear-line-delimited screen, left, right
+  print-character screen, 120/x, 245/grey
+  # save menu row so we can detect clicks to it later
+  starting-row:address:number <- get-address *sandbox, starting-row-on-screen:offset
+  *starting-row <- copy row
+  # render sandbox contents
+  sandbox-data:address:array:character <- get *sandbox, data:offset
+  row, screen <- render-code-string screen, sandbox-data, left, right, row
+  code-ending-row:address:number <- get-address *sandbox, code-ending-row-on-screen:offset
+  *code-ending-row <- copy row
+  # render sandbox warnings, screen or response, in that order
+  response-starting-row:address:number <- get-address *sandbox, response-starting-row-on-screen:offset
+  sandbox-response:address:array:character <- get *sandbox, response:offset
+  sandbox-warnings:address:array:character <- get *sandbox, warnings:offset
+  sandbox-screen:address <- get *sandbox, screen:offset
+  <render-sandbox-results>
+  {
+    break-unless sandbox-warnings
+    *response-starting-row <- copy 0  # no response
+    row, screen <- render-string screen, sandbox-warnings, left, right, 1/red, row
+  }
+  {
+    break-if sandbox-warnings
+    empty-screen?:boolean <- fake-screen-is-empty? sandbox-screen
+    break-if empty-screen?
+    row, screen <- render-screen screen, sandbox-screen, left, right, row
+  }
+  {
+    break-if sandbox-warnings
+    break-unless empty-screen?
+    *response-starting-row <- add row, 1
+    <render-sandbox-response>
+    row, screen <- render-string screen, sandbox-response, left, right, 245/grey, row
+  }
+  +render-sandbox-end
+  at-bottom?:boolean <- greater-or-equal row, screen-height
+  reply-if at-bottom?, row/same-as-ingredient:4, screen/same-as-ingredient:0
+  # draw solid line after sandbox
+  draw-horizontal screen, row, left, right, 9473/horizontal-double
+  # draw next sandbox
+  next-sandbox:address:sandbox-data <- get *sandbox, next-sandbox:offset
+  row, screen <- render-sandboxes screen, next-sandbox, left, right, row
+  reply row/same-as-ingredient:4, screen/same-as-ingredient:0
+]
+
+# assumes programming environment has no sandboxes; restores them from previous session
+recipe restore-sandboxes [
+  local-scope
+  env:address:programming-environment-data <- next-ingredient
+  # read all scenarios, pushing them to end of a list of scenarios
+  suffix:address:array:character <- new [.out]
+  idx:number <- copy 0
+  curr:address:address:sandbox-data <- get-address *env, sandbox:offset
+  {
+    filename:address:array:character <- integer-to-decimal-string idx
+    contents:address:array:character <- restore filename
+    break-unless contents  # stop at first error; assuming file didn't exist
+    # create new sandbox for file
+    *curr <- new sandbox-data:type
+    data:address:address:array:character <- get-address **curr, data:offset
+    *data <- copy contents
+    # restore expected output for sandbox if it exists
+    {
+      filename <- string-append filename, suffix
+      contents <- restore filename
+      break-unless contents
+      expected-response:address:address:array:character <- get-address **curr, expected-response:offset
+      *expected-response <- copy contents
+    }
+    +continue
+    idx <- add idx, 1
+    curr <- get-address **curr, next-sandbox:offset
+    loop
+  }
+  reply env/same-as-ingredient:0
+]
+
+# row, screen <- render-screen screen:address, sandbox-screen:address, left:number, right:number, row:number
+# print the fake sandbox screen to 'screen' with appropriate delimiters
+# leave cursor at start of next line
+recipe render-screen [
+  local-scope
+  screen:address <- next-ingredient
+  s:address:screen <- next-ingredient
+  left:number <- next-ingredient
+  right:number <- next-ingredient
+  row:number <- next-ingredient
+  row <- add row, 1
+  reply-unless s, row/same-as-ingredient:4, screen/same-as-ingredient:0
+  # print 'screen:'
+  header:address:array:character <- new [screen:]
+  row <- subtract row, 1  # compensate for render-string below
+  row <- render-string screen, header, left, right, 245/grey, row
+  # newline
+  row <- add row, 1
+  screen <- move-cursor screen, row, left
+  # start printing s
+  column:number <- copy left
+  s-width:number <- screen-width s
+  s-height:number <- screen-height s
+  buf:address:array:screen-cell <- get *s, data:offset
+  stop-printing:number <- add left, s-width, 3
+  max-column:number <- min stop-printing, right
+  i:number <- copy 0
+  len:number <- length *buf
+  screen-height:number <- screen-height screen
+  {
+    done?:boolean <- 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 '.'
+    print-character screen, 32/space, 245/grey
+    print-character screen, 32/space, 245/grey
+    print-character screen, 46/full-stop, 245/grey
+    column <- add left, 3
+    {
+      # print row
+      row-done?:boolean <- greater-or-equal column, max-column
+      break-if row-done?
+      curr:screen-cell <- index *buf, i
+      c:character <- get curr, contents:offset
+      color:number <- get curr, color:offset
+      {
+        # damp whites down to grey
+        white?:boolean <- equal color, 7/white
+        break-unless white?
+        color <- copy 245/grey
+      }
+      print-character screen, c, color
+      column <- add column, 1
+      i <- add i, 1
+      loop
+    }
+    # print final '.'
+    print-character screen, 46/full-stop, 245/grey
+    column <- add column, 1
+    {
+      # clear rest of current line
+      line-done?:boolean <- greater-than column, right
+      break-if line-done?
+      print-character screen, 32/space
+      column <- add column, 1
+      loop
+    }
+    row <- add row, 1
+    loop
+  }
+  reply row/same-as-ingredient:4, screen/same-as-ingredient:0
+]
+
+scenario run-updates-results [
+  $close-trace  # trace too long
+  assume-screen 100/width, 12/height
+  # define a recipe (no indent for the 'add' line below so column numbers are more obvious)
+  1:address:array:character <- new [ 
+recipe foo [
+z:number <- add 2, 2
+]]
+  # sandbox editor contains an instruction without storing outputs
+  2:address:array:character <- new [foo]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that screen prints the results
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .recipe foo [                                      ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .z:number <- add 2, 2                              ┊                                                x.
+    .]                                                 ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊4                                                .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+  # make a change (incrementing one of the args to 'add'), then rerun
+  assume-console [
+    left-click 3, 28  # one past the value of the second arg
+    press backspace
+    type [3]
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that screen updates the result on the right
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .recipe foo [                                      ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .z:number <- add 2, 3                              ┊                                                x.
+    .]                                                 ┊foo                                              .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊5                                                .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-instruction-and-print-warnings [
+  $close-trace  # trace too long
+  assume-screen 100/width, 10/height
+  # left editor is empty
+  1:address:array:character <- new []
+  # right editor contains an illegal instruction
+  2:address:array:character <- new [get 1234:number, foo:offset]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the code in the editors
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that screen prints error message in red
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊get 1234:number, foo:offset                      .
+    .                                                  ┊unknown element foo in container number          .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+  screen-should-contain-in-color 7/white, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   get 1234:number, foo:offset                      .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+  ]
+  screen-should-contain-in-color 1/red, [
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                                                                    .
+    .                                                   unknown element foo in container number          .
+    .                                                                                                    .
+  ]
+  screen-should-contain-in-color 245/grey, [
+    .                                                                                                    .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-instruction-and-print-warnings-only-once [
+  $close-trace  # trace too long
+  assume-screen 100/width, 10/height
+  # left editor is empty
+  1:address:array:character <- new []
+  # right editor contains an illegal instruction
+  2:address:array:character <- new [get 1234:number, foo:offset]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the code in the editors multiple times
+  assume-console [
+    press F4
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that screen prints error message just once
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊get 1234:number, foo:offset                      .
+    .                                                  ┊unknown element foo in container number          .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario run-instruction-manages-screen-per-sandbox [
+  $close-trace  # trace too long
+  assume-screen 100/width, 20/height
+  # left editor is empty
+  1:address:array:character <- new []
+  # right editor contains an instruction
+  2:address:array:character <- new [print-integer screen:address, 4]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the code in the editor
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  # check that it prints a little toy screen
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊print-integer screen:address, 4                  .
+    .                                                  ┊screen:                                          .
+    .                                                  ┊  .4                             .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario sandbox-with-print-can-be-edited [
+  $close-trace  # trace too long
+  assume-screen 100/width, 20/height
+  # left editor is empty
+  1:address:array:character <- new []
+  # right editor contains an instruction
+  2:address:array:character <- new [print-integer screen:address, 4]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the sandbox
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊                                                 .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                x.
+    .                                                  ┊print-integer screen:address, 4                  .
+    .                                                  ┊screen:                                          .
+    .                                                  ┊  .4                             .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊  .                              .               .
+    .                                                  ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+  # edit the sandbox
+  assume-console [
+    left-click 3, 70
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .                                                  ┊print-integer screen:address, 4                  .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+    .                                                  ┊                                                 .
+  ]
+]
+
+scenario sandbox-can-handle-infinite-loop [
+  $close-trace  # trace too long
+  assume-screen 100/width, 20/height
+  # left editor is empty
+  1:address:array:character <- new [recipe foo [
+  {
+    loop
+  }
+]]
+  # right editor contains an instruction
+  2:address:array:character <- new [foo]
+  3:address:programming-environment-data <- new-programming-environment screen:address, 1:address:array:character, 2:address:array:character
+  # run the sandbox
+  assume-console [
+    press F4
+  ]
+  run [
+    event-loop screen:address, console:address, 3:address:programming-environment-data
+  ]
+  screen-should-contain [
+    .                                                                                 run (F4)           .
+    .recipe foo [                                      ┊                                                 .
+    .  {                                               ┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .    loop                                          ┊                                                x.
+    .  }                                               ┊foo                                              .
+    .]                                                 ┊took too long!                                   .
+    .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━.
+    .                                                  ┊                                                 .
+  ]
+]
+
+recipe editor-contents [
+  local-scope
+  editor:address:editor-data <- next-ingredient
+  buf:address:buffer <- new-buffer 80
+  curr:address:duplex-list <- get *editor, data:offset
+  # skip § sentinel
+  assert curr, [editor without data is illegal; must have at least a sentinel]
+  curr <- next-duplex curr
+  reply-unless curr, 0
+  {
+    break-unless curr
+    c:character <- get *curr, value:offset
+    buffer-append buf, c
+    curr <- next-duplex curr
+    loop
+  }
+  result:address:array:character <- buffer-to-array buf
+  reply result
+]
+
+scenario editor-provides-edited-contents [
+  assume-screen 10/width, 5/height
+  1:address:array:character <- new [abc]
+  2:address:editor-data <- new-editor 1:address:array:character, screen:address, 0/left, 10/right
+  assume-console [
+    left-click 1, 2
+    type [def]
+  ]
+  run [
+    editor-event-loop screen:address, console:address, 2:address:editor-data
+    3:address:array:character <- editor-contents 2:address:editor-data
+    4:array:character <- copy *3:address:array:character
+  ]
+  memory-should-contain [
+    4:string <- [abdefc]
+  ]
+]