about summary refs log blame commit diff stats
path: root/subx/059stop.subx
blob: dbe8a66316f9cddb4f6f7d07da447e313ad45926 (plain) (tree)
1
2
3
4
5
6

                                                             


                                                                            
 






                                                                              
 

                                                                                    
 
                                         

                                                              
 
                                                                           
 
                                                                              

                                                                           
 
                                                                               




                                                                                     

       


                                                                                                                                                 
 


                                                                           
                                                                                 
              

                                                                                                                                                                       
                      


                  
                                                                                                                                                                             







                                                                                                            
                                                                             









                                                                 
                           

                                                                                                                                                                              
                           


                                                                                                                                                                              
                                                                                                                                                                            

                                                                                                                                                                        
                                                                                                                                                                        
                            
                         

                 
              


                                                                                                                                                                       
 
                                                    


                                                                                                                                                                            
                                      

                                                                                                                                                                    
                            
                                                                                                                                                                            
                                
                         
           
                

                                                                                                                                                                            
                    
                                                                                                                                                                            
                                            
                                                                                                                                                                        
          
                                         

                                












                                                                                                                                                                         
                                                                                                                                                                       

                                                                                  
                   

                                                    
              
                                          
                      
                                                                                                                                                                  
                        
                   
               
              
                                
                                                         
                  


                                                                              
                   

                                                        
                        
                                                                                                                                                                     
              
                                    
                      
                                                                                                                                                                  
              
                                                         
                                                                                                                                                                  
                 
             
 
                                               
              


                                                                                                                                                                       
                   
                                                                                                                                                                     
              
                                
                                      
                       
                      

                                                                                                                                                                  
                   


                                                        
              
                                    
                      
                                                                                                                                                                  
              


                                                                                                                                                                       
 
                                               
              

                                                                                                                                                                       
                   
                   
                    
                                                                                                                                                                     
              
                        
                                      
                       
              


                                                                                                                                                                       
 
                            
# stop: dependency-injected wrapper around the exit() syscall
#
# We'd like to be able to write tests for functions that call exit(), and to
# make assertions about whether they exit() or not in a given situation. To
# achieve this we'll call exit() via a smarter wrapper called 'stop'.
#
# In the context of a test, calling a function X that calls 'stop' (directly
# or through further intervening calls) will unwind the stack until X returns,
# so that we can say check any further assertions after the execution of X. To
# achieve this end, we'll pass the return address of X as a 'target' argument
# into X, plumbing it through to 'stop'. When 'stop' gets a non-null target it
# unwinds the stack until the target. If it gets a null target it calls
# exit().
#
# We'd also like to get the exit status out of 'stop', so we'll combine the
# input target with an output status parameter into a type called 'exit-descriptor'.
#
# So the exit-descriptor looks like this:
#   target : address  # return address for 'stop' to unwind to
#   value : int  # exit status stop was called with
#
# 'stop' thus takes two parameters: an exit-descriptor and the exit status.
#
# 'stop' won't bother cleaning up any other processor state besides the stack,
# such as registers. Only ESP will have a well-defined value after 'stop'
# returns. (This is a poor man's setjmp/longjmp, if you know what that is.)
#
# Before you can call any function that may call 'stop', you need to pass in an
# exit-descriptor to it. To create an exit-descriptor use 'tailor-exit-descriptor'
# below. It's not the most pleasant abstraction in the world.
#
# An exit-descriptor's target is its input, computed during 'tailor-exit-descriptor'.
# Its value is its output, computed during stop and available to the test.

== code
#   instruction                     effective address                                                   register    displacement    immediate
# . op          subop               mod             rm32          base        index         scale       r32
# . 1-3 bytes   3 bits              2 bits          3 bits        3 bits      3 bits        2 bits      2 bits      0/1/2/4 bytes   0/1/2/4 bytes

# Configure an exit-descriptor for a call pushing 'nbytes' bytes of args to
# the stack.
# Ugly that we need to know the size of args, but so it goes.
tailor-exit-descriptor:  # ed : (address exit-descriptor), nbytes : int -> <void>
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . save registers
    50/push-EAX
    51/push-ECX
    # EAX = nbytes
    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           0/r32/EAX   0xc/disp8       .                 # copy *(EBP+12) to EAX
    # Let X be the value of ESP in the caller, before the call to tailor-exit-descriptor.
    # The return address for a call in the caller's body will be at:
    #   X-8 if the caller takes 4 bytes of args for the exit-descriptor (add 4 bytes for the return address)
    #   X-12 if the caller takes 8 bytes of args
    #   ..and so on
    # That's the value we need to return: X-nbytes-4
    #
    # However, we also need to account for the perturbance to ESP caused by the
    # call to tailor-exit-descriptor. It pushes 8 bytes of args followed by 4
    # bytes for the return address and 4 bytes to push EBP above.
    # So EBP at this point is X-16.
    #
    # So the return address for the next call in the caller is:
    #   EBP+8 if the caller takes 4 bytes of args
    #   EBP+4 if the caller takes 8 bytes of args
    #   EBP if the caller takes 12 bytes of args
    #   EBP-4 if the caller takes 16 bytes of args
    #   ..and so on
    # That's EBP+12-nbytes.
    # option 1: 6 + 3 bytes
#?     2d/subtract                     3/mod/direct    0/rm32/EAX    .           .             .           .           .               8/imm32           # subtract from EAX
#?     8d/copy-address                 0/mod/indirect  4/rm32/sib    5/base/EBP  0/index/EAX   .           0/r32/EAX   .               .                 # copy EBP+EAX to EAX
    # option 2: 2 + 4 bytes
    f7          3/subop/negate      3/mod/direct    0/rm32/EAX    .           .             .           .           .               .                 # negate EAX
    8d/copy-address                 1/mod/*+disp8   4/rm32/sib    5/base/EBP  0/index/EAX   .           0/r32/EAX   0xc/disp8         .               # copy EBP+EAX+12 to EAX
    # copy EAX to ed->target
    8b/copy                         1/mod/*+disp8   5/rm32/EBP    .           .             .           1/r32/ECX   8/disp8         .                 # copy *(EBP+8) to ECX
    89/copy                         0/mod/indirect  1/rm32/ECX    .           .             .           0/r32/EAX   .               .                 # copy EAX to *ECX
    # initialize ed->value
    c7          0/subop/copy        1/mod/*+disp8   1/rm32/ECX    .           .             .           .           4/disp8         0/imm32           # copy to *(ECX+4)
$tailor-exit-descriptor:end:
    # . restore registers
    59/pop-to-ECX
    58/pop-to-EAX
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

stop:  # ed : (address exit-descriptor), value : int
    # no prolog; one way or another, we're going to clobber registers
    # EAX = ed
    8b/copy                         1/mod/*+disp8   4/rm32/sib    4/base/ESP  4/index/none  .           0/r32/EAX   4/disp8         .                 # copy *(ESP+4) to EAX
    # if (ed->target == 0) really exit
    81          7/subop/compare     0/mod/indirect  0/rm32/EAX    .           .             .           .           .               0/imm32           # compare *EAX
    75/jump-if-not-equal  $stop:fake/disp8
    # . syscall(exit, value)
    8b/copy                         1/mod/*+disp8   4/rm32/sib    4/base/ESP  4/index/none  .           3/r32/EBX   8/disp8         .                 # copy *(ESP+8) to EBX
    b8/copy-to-EAX  1/imm32/exit
    cd/syscall  0x80/imm8
$stop:fake:
    # otherwise:
    # ed->value = value+1
    8b/copy                         1/mod/*+disp8   4/rm32/sib    4/base/ESP  4/index/none  .           1/r32/ECX   8/disp8         .                 # copy *(ESP+8) to ECX
    41/increment-ECX
    89/copy                         1/mod/*+disp8   0/rm32/EAX    .           .             .           1/r32/ECX   4/disp8         .                 # copy ECX to *(EAX+4)
    # perform a non-local jump to ed->target
    8b/copy                         0/mod/indirect  0/rm32/EAX    .           .             .           4/r32/ESP   .               .                 # copy *EAX to ESP
$stop:end:
    c3/return  # doesn't return to caller

test-stop-skips-returns-on-exit:
    # This looks like the standard prolog, but is here for different reasons.
    # A function calling 'stop' can't rely on EBP persisting past the call.
    #
    # Use EBP here as a stable base to refer to locals and arguments from in the
    # presence of push/pop/call instructions.
    # *Don't* use EBP as a way to restore ESP.
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # Make room for an exit descriptor on the stack. That's almost always the
    # right place for it, available only as long as it's legal to use. Once this
    # containing function returns we'll need a new exit descriptor.
    # var ed/EAX : (address exit-descriptor)
    81          5/subop/subtract    3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # subtract from ESP
    89/copy                         3/mod/direct    0/rm32/EAX    .           .             .           4/r32/ESP   .               .                 # copy ESP to EAX
    # Size the exit-descriptor precisely for the next call below, to _test-stop-1.
    # tailor-exit-descriptor(ed, 4)
    # . . push args
    68/push  4/imm32/nbytes-of-args-for-_test-stop-1
    50/push-EAX
    # . . call
    e8/call  tailor-exit-descriptor/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    # . _test-stop-1(ed)
    # . . push args
    50/push-EAX
    # . . call
    e8/call  _test-stop-1/disp32
    # registers except ESP may be clobbered at this point
    # restore args
    58/pop-to-EAX
    # check that _test-stop-1 tried to call exit(1)
    # check-ints-equal(ed->value, 2, msg)  # i.e. stop was called with value 1
    # . . push args
    68/push  "F - test-stop-skips-returns-on-exit"/imm32
    68/push  2/imm32
    # . . push ed->value
    ff          6/subop/push        1/mod/*+disp8   0/rm32/EAX    .           .             .           .           4/disp8         .                 # push *(EAX+4)
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # . epilog
    # don't restore ESP from EBP; manually reclaim locals
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               8/imm32           # add to ESP
    5d/pop-to-EBP
    c3/return

_test-stop-1:  # ed : (address exit-descriptor)
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # _test-stop-2(ed)
    # . . push args
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  _test-stop-2/disp32
    # should never get past this point
$_test-stop-1:dead-end:
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               4/imm32           # add to ESP
    # signal test failed: check-ints-equal(1, 0, msg)
    # . . push args
    68/push  "F - test-stop-skips-returns-on-exit"/imm32
    68/push  0/imm32
    68/push  1/imm32
    # . . call
    e8/call  check-ints-equal/disp32
    # . . discard args
    81          0/subop/add         3/mod/direct    4/rm32/ESP    .           .             .           .           .               0xc/imm32         # add to ESP
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

_test-stop-2:  # ed : (address exit-descriptor)
    # . prolog
    55/push-EBP
    89/copy                         3/mod/direct    5/rm32/EBP    .           .             .           4/r32/ESP   .               .                 # copy ESP to EBP
    # . stop(ed, 1)
    # . . push args
    68/push  1/imm32
    ff          6/subop/push        1/mod/*+disp8   5/rm32/EBP    .           .             .           .           8/disp8         .                 # push *(EBP+8)
    # . . call
    e8/call  stop/disp32
    # should never get past this point
$_test-stop-2:dead-end:
    # . epilog
    89/copy                         3/mod/direct    4/rm32/ESP    .           .             .           5/r32/EBP   .               .                 # copy EBP to ESP
    5d/pop-to-EBP
    c3/return

# . . vim:nowrap:textwidth=0
ignore this in tests $system [./snapshot_lesson] } ] # load code from disk # replaced in a later layer (whereupon errors-found? will actually be set) def update-recipes env:&:environment, resources:&:resources, screen:&:screen -> errors-found?:bool, env:&:environment, resources:&:resources, screen:&:screen [ local-scope load-ingredients recipes:&:editor <- get *env, recipes:offset in:text <- editor-contents recipes resources <- dump resources, [lesson/recipes.mu], in reload in 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, resources:&:resources -> resources:&:resources [ 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 <- append [lesson/], idx resources <- dump resources, filename, data <end-save-sandbox> 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 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 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 <render-sandbox-results> { 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? <render-sandbox-response> 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 <end-render-sandbox-reset-hidden> } # 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 ] 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 { # 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 i <- add i, 1 loop +next-character } { # 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 # don't increment i loop +next-character } i <- add i, 1 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 ] scenario read-text-wraps-barely-long-lines [ local-scope assume-screen 5/width, 5/height s:text <- new [abcde] run [ render-text screen, s, 0/left, 4/right, 7/white, 1/row ] screen-should-contain [ . . .abcd. .e . . . ] ] # like 'render-text', but with colorization for comments like in the editor def render-code screen:&:screen, s:text, left:num, right:num, row:num -> row:num, screen:&:screen [ local-scope load-ingredients return-unless s color:num <- copy 7/white 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 <character-c-received> # only line different from 'render-text' { # 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 i <- add i, 1 loop +next-character } { # 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 # don't increment i loop +next-character } i <- add i, 1 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, resources:&:resources -> 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 <- append [lesson/], idx contents:text <- slurp resources, 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 <end-restore-sandbox> { 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 100/width, 12/height # define a recipe (no indent for the 'add' line below so column numbers are more obvious) assume-resources [ [lesson/recipes.mu] <- [ || |recipe foo [| | local-scope| | z:num <- add 2, 2| | reply z| |]| ] ] # sandbox editor contains an instruction without storing outputs env:&:environment <- new-programming-environment resources, screen, [foo] # contents of sandbox editor # run the code in the editors assume-console [ press F4 ] event-loop screen, console, env, resources screen-should-contain [ . run (F4) . . . .recipe foo [ ┊─────────────────────────────────────────────────. . local-scope 0 edit copy delete . . z:num <- add 2, 2 foo . . reply z 4 . .] ┊─────────────────────────────────────────────────. . . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . . ] # make a change (incrementing one of the args to 'add'), then rerun assume-console [ left-click 4, 28 # one past the value of the second arg press backspace type [3] press F4 ] run [ event-loop screen, console, env, resources ] # check that screen updates the result on the right screen-should-contain [ . run (F4) . . . .recipe foo [ ┊─────────────────────────────────────────────────. . local-scope 0 edit copy delete . . z:num <- add 2, 3 foo . . reply z 5 . .] ┊─────────────────────────────────────────────────. . . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . . ] ] scenario run-instruction-manages-screen-per-sandbox [ local-scope trace-until 100/app # trace too long assume-screen 100/width, 20/height # empty recipes assume-resources [ ] # sandbox editor contains an instruction env:&:environment <- new-programming-environment resources, screen, [print screen, 4] # contents of sandbox editor # run the code in the editor assume-console [ press F4 ] run [ event-loop screen, console, env, resources ] # check that it prints a little toy screen screen-should-contain [ . run (F4) . . . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . print screen, 4 . . screen: . . .4 . . . . . . . . . . . . . . . . . . . ┊─────────────────────────────────────────────────. . . ] ] def editor-contents editor:&:editor -> result:text [ local-scope load-ingredients buf:&:buffer:char <- new-buffer 80 curr:&:duplex-list:char <- get *editor, data:offset # skip § sentinel assert curr, [editor without data is illegal; must have at least a sentinel] curr <- next curr return-unless curr, 0 { break-unless curr c:char <- get *curr, value:offset buf <- append buf, c curr <- next curr loop } result <- buffer-to-array buf ] scenario editor-provides-edited-contents [ local-scope assume-screen 10/width, 5/height e:&:editor <- new-editor [abc], 0/left, 10/right assume-console [ left-click 1, 2 type [def] ] run [ editor-event-loop screen, console, e s:text <- editor-contents e 1:@:char/raw <- copy *s ] memory-should-contain [ 1:array:character <- [abdefc] ] ] # keep the bottom of recipes from scrolling off the screen scenario scrolling-down-past-bottom-of-recipe-editor [ local-scope trace-until 100/app assume-screen 100/width, 10/height assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [] render-all screen, env, render assume-console [ press enter press down-arrow ] event-loop screen, console, env, resources # no scroll screen-should-contain [ . run (F4) . . . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . . ] ] scenario cursor-down-in-recipe-editor [ local-scope trace-until 100/app assume-screen 100/width, 10/height assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [] render-all screen, env, render assume-console [ press enter press up-arrow press down-arrow # while cursor isn't at bottom ] event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor # cursor moves back to bottom screen-should-contain [ . run (F4) . . . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . . ] ] # we'll not use the recipe-editor's 'bottom' element directly, because later # layers will add other stuff to the left side below the editor (error messages) container environment [ recipe-bottom:num ] after <render-recipe-components-end> [ *env <- put *env, recipe-bottom:offset, row ] after <global-keypress> [ { break-if sandbox-in-focus? down-arrow?:bool <- equal k, 65516/down-arrow break-unless down-arrow? recipe-editor:&:editor <- get *env, recipes:offset recipe-cursor-row:num <- get *recipe-editor, cursor-row:offset recipe-editor-bottom:num <- get *recipe-editor, bottom:offset at-bottom-of-editor?:bool <- greater-or-equal recipe-cursor-row, recipe-editor-bottom break-unless at-bottom-of-editor? more-to-scroll?:bool <- more-to-scroll? env, screen break-if more-to-scroll? loop +next-event } { break-if sandbox-in-focus? page-down?:bool <- equal k, 65518/page-down break-unless page-down? more-to-scroll?:bool <- more-to-scroll? env, screen break-if more-to-scroll? loop +next-event } ] after <global-type> [ { break-if sandbox-in-focus? page-down?:bool <- equal k, 6/ctrl-f break-unless page-down? more-to-scroll?:bool <- more-to-scroll? env, screen break-if more-to-scroll? loop +next-event } ] def more-to-scroll? env:&:environment, screen:&:screen -> result:bool [ local-scope load-ingredients recipe-bottom:num <- get *env, recipe-bottom:offset height:num <- screen-height screen result <- greater-or-equal recipe-bottom, height ] scenario scrolling-down-past-bottom-of-recipe-editor-2 [ local-scope trace-until 100/app assume-screen 100/width, 10/height assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [] render-all screen, env, render assume-console [ # add a line press enter # cursor back to top line press up-arrow # try to scroll press page-down # or ctrl-f ] event-loop screen, console, env, resources # no scroll, and cursor remains at top line screen-should-contain [ . run (F4) . . . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊ . . . ] ] scenario scrolling-down-past-bottom-of-recipe-editor-3 [ local-scope trace-until 100/app assume-screen 100/width, 10/height assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [ab cd] render-all screen, env, render assume-console [ # add a line press enter # switch to sandbox press ctrl-n # move cursor press down-arrow ] event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor # no scroll on recipe side, cursor moves on sandbox side screen-should-contain [ . run (F4) . . ab . . ┊␣d . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . . ] ] # scrolling through sandboxes scenario scrolling-down-past-bottom-of-sandbox-editor [ local-scope trace-until 100/app # trace too long assume-screen 100/width, 10/height # initialize assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [add 2, 2] render-all screen, env, render assume-console [ # create a sandbox press F4 ] event-loop screen, console, env, resources screen-should-contain [ . run (F4) . . . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 2, 2 . ] # switch to sandbox window and hit 'page-down' assume-console [ press ctrl-n press page-down ] run [ event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor ] # sandbox editor hidden; first sandbox displayed # cursor moves to first sandbox screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣ edit copy delete . . add 2, 2 . . 4 . ] # hit 'page-up' assume-console [ press page-up ] run [ event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor ] # sandbox editor displays again, cursor is in editor screen-should-contain [ . run (F4) . . ┊␣ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 2, 2 . ] ] # page-down on sandbox side updates render-from to scroll sandboxes after <global-keypress> [ { break-unless sandbox-in-focus? page-down?:bool <- equal k, 65518/page-down break-unless page-down? sandbox:&:sandbox <- get *env, sandbox:offset break-unless sandbox # slide down if possible { render-from:num <- get *env, render-from:offset number-of-sandboxes:num <- get *env, number-of-sandboxes:offset max:num <- subtract number-of-sandboxes, 1 at-end?:bool <- greater-or-equal render-from, max loop-if at-end?, +next-event # render nothing render-from <- add render-from, 1 *env <- put *env, render-from:offset, render-from } hide-screen screen screen <- render-sandbox-side screen, env, render screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env show-screen screen loop +next-event } ] # update-cursor takes render-from into account after <update-cursor-special-cases> [ { break-unless sandbox-in-focus? render-from:num <- get *env, render-from:offset scrolling?:bool <- greater-or-equal render-from, 0 break-unless scrolling? cursor-column:num <- get *current-sandbox, left:offset screen <- move-cursor screen, 2/row, cursor-column # highlighted sandbox will always start at row 2 return } ] # 'page-up' on sandbox side is like 'page-down': updates render-from when necessary after <global-keypress> [ { break-unless sandbox-in-focus? page-up?:bool <- equal k, 65519/page-up break-unless page-up? render-from:num <- get *env, render-from:offset at-beginning?:bool <- equal render-from, -1 break-if at-beginning? render-from <- subtract render-from, 1 *env <- put *env, render-from:offset, render-from hide-screen screen screen <- render-sandbox-side screen, env, render screen <- update-cursor screen, recipes, current-sandbox, sandbox-in-focus?, env show-screen screen loop +next-event } ] # sandbox belonging to 'env' whose next-sandbox is 'in' # return 0 if there's no such sandbox, either because 'in' doesn't exist in 'env', or because it's the first sandbox def previous-sandbox env:&:environment, in:&:sandbox -> out:&:sandbox [ local-scope load-ingredients curr:&:sandbox <- get *env, sandbox:offset return-unless curr, 0/nil next:&:sandbox <- get *curr, next-sandbox:offset { return-unless next, 0/nil found?:bool <- equal next, in break-if found? curr <- copy next next <- get *curr, next-sandbox:offset loop } return curr ] scenario scrolling-down-past-bottom-on-recipe-side [ local-scope trace-until 100/app # trace too long assume-screen 100/width, 10/height # initialize sandbox side and create a sandbox assume-resources [ [lesson/recipes.mu] <- [ || # file containing just a newline ] ] # create a sandbox env:&:environment <- new-programming-environment resources, screen, [add 2, 2] render-all screen, env, render assume-console [ press F4 ] event-loop screen, console, env, resources # hit 'down' in recipe editor assume-console [ press page-down ] run [ event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor ] # cursor doesn't move when the end is already on-screen screen-should-contain [ . run (F4) . . . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0 edit copy delete . . add 2, 2 . ] ] scenario scrolling-through-multiple-sandboxes [ local-scope trace-until 100/app # trace too long assume-screen 100/width, 10/height # initialize environment assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [] render-all screen, env, render # create 2 sandboxes assume-console [ press ctrl-n type [add 2, 2] press F4 type [add 1, 1] press F4 ] event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor screen-should-contain [ . run (F4) . . ┊␣ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . 1 edit copy delete . . add 2, 2 . . 4 . ] # hit 'page-down' assume-console [ press page-down ] run [ event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor ] # sandbox editor hidden; first sandbox displayed # cursor moves to first sandbox screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊␣ edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . 1 edit copy delete . . add 2, 2 . . 4 . ] # hit 'page-down' again assume-console [ press page-down ] run [ event-loop screen, console, env, resources ] # just second sandbox displayed screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1 edit copy delete . . add 2, 2 . . 4 . . ┊─────────────────────────────────────────────────. . . ] # hit 'page-down' again assume-console [ press page-down ] run [ event-loop screen, console, env, resources ] # no change screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊1 edit copy delete . . add 2, 2 . . 4 . . ┊─────────────────────────────────────────────────. . . ] # hit 'page-up' assume-console [ press page-up ] run [ event-loop screen, console, env, resources ] # back to displaying both sandboxes without editor screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . 1 edit copy delete . . add 2, 2 . . 4 . ] # hit 'page-up' again assume-console [ press page-up ] run [ event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor ] # back to displaying both sandboxes as well as editor screen-should-contain [ . run (F4) . . ┊␣ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . 1 edit copy delete . . add 2, 2 . . 4 . ] # hit 'page-up' again assume-console [ press page-up ] run [ event-loop screen, console, env, resources cursor:char <- copy 9251/ print screen, cursor ] # no change screen-should-contain [ . run (F4) . . ┊␣ . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . 1 edit copy delete . . add 2, 2 . . 4 . ] ] scenario scrolling-manages-sandbox-index-correctly [ local-scope trace-until 100/app # trace too long assume-screen 100/width, 10/height # initialize environment assume-resources [ ] env:&:environment <- new-programming-environment resources, screen, [] render-all screen, env, render # create a sandbox assume-console [ press ctrl-n type [add 1, 1] press F4 ] event-loop screen, console, env, resources screen-should-contain [ . run (F4) . . . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . . ] # hit 'page-down' and 'page-up' a couple of times. sandbox index should be stable assume-console [ press page-down ] run [ event-loop screen, console, env, resources ] # sandbox editor hidden; first sandbox displayed # cursor moves to first sandbox screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . . ] # hit 'page-up' again assume-console [ press page-up ] run [ event-loop screen, console, env, resources ] # back to displaying both sandboxes as well as editor screen-should-contain [ . run (F4) . . . .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊─────────────────────────────────────────────────. . 0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . . ] # hit 'page-down' assume-console [ press page-down ] run [ event-loop screen, console, env, resources ] # sandbox editor hidden; first sandbox displayed # cursor moves to first sandbox screen-should-contain [ . run (F4) . . ┊─────────────────────────────────────────────────. .┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┊0 edit copy delete . . add 1, 1 . . 2 . . ┊─────────────────────────────────────────────────. . . ] ]