#lang racket/unit ;; this module implements the UI side of the stepper; it ;; opens a window, starts the stepper thread running, ;; and handles the resulting calls to 'break'. ;; this module lies outside of the "testing boundary" ;; of through-tests; it is not tested automatically at all. ;; this version of the view-controller just collects the steps up front rather ;; than blocking until the user presses the "next" button. (require racket/class racket/match racket/list drracket/tool mred string-constants racket/async-channel (prefix-in model: "model.rkt") (prefix-in x: "mred-extensions.rkt") "shared.rkt" "model-settings.rkt" "xml-sig.rkt" images/compile-time images/gui (for-syntax racket/base images/icons/control images/icons/style images/logos)) (import drracket:tool^ xml^ stepper-frame^) (export view-controller^) (define drracket-eventspace (current-eventspace)) (define (definitions-text->settings definitions-text) (send definitions-text get-next-settings)) ;; the stored representation of a step (define-struct step (text kind posns) #:transparent) (define (show-about-dialog parent) (define dlg (new logo-about-dialog% [label "About the Stepper"] [parent parent] [bitmap (compiled-bitmap (stepper-logo))] [messages '("The Algebraic Stepper is formalized and proved correct in\n" "\n" " John Clements, Matthew Flatt, Matthias Felleisen\n" " Modeling an Algebraic Stepper\n" " European Symposium on Programming, 2001\n")])) (send dlg show #t)) (define (go drracket-tab program-expander selection-start selection-end) ;; get the language-level: (define language-settings (definitions-text->settings (send drracket-tab get-defs))) (define language-level (drracket:language-configuration:language-settings-language language-settings)) (define simple-settings (drracket:language-configuration:language-settings-settings language-settings)) ;; VALUE CONVERSION CODE: ;; render-to-string : TST -> string (define (render-to-string val) (let ([string-port (open-output-string)]) (send language-level render-value val simple-settings string-port) (get-output-string string-port))) ;; render-to-sexp : TST -> sexp (define (render-to-sexp val) (send language-level stepper:render-to-sexp val simple-settings language-level)) ;; channel for incoming views (define view-channel (make-async-channel)) ;; the first-step semaphore (define first-step-sema (make-semaphore 0)) ;; the list of available views (define view-history null) ;; the number of available steps (define num-steps-available 0) ;; the view in the stepper window (define view #f) ;; wait for steps to show up on the channel. ;; When they do, add them to the list. (define (start-listener-thread stepper-frame-eventspace) ;; as of 2012-06-20, I no longer believe there's any ;; need for this thread, as the queue-callback handles ;; the needed separation. (thread (lambda () (let loop () (define new-result (async-channel-get view-channel)) (receive-result-from-target new-result) (loop))))) ;; handles an incoming result. Either adds it to the list of ;; steps, or prompts user to see whether to continue running. (define (receive-result-from-target result) (cond [(runaway-process? result) (parameterize ([current-eventspace stepper-frame-eventspace]) (queue-callback (lambda () (when (confirm-running) (semaphore-post (runaway-process-sema result))) (void))))] [else (define new-step (format-result result)) (parameterize ([current-eventspace stepper-frame-eventspace]) (queue-callback (lambda () (set! view-history (append view-history (list new-step))) (set! num-steps-available (length view-history)) ;; this is only necessary the first time, but it's cheap: (semaphore-post first-step-sema) (update-status-bar))))])) ;; find-later-step : given a predicate on history-entries, search through ;; the history for the first step that satisfies the predicate and whose ;; number is greater than n (or -1 if n is #f), return # of step on success, ;; on failure return (list 'nomatch last-step) or (list 'nomatch/seen-final ;; last-step) if we went past the final step (define (find-later-step p n) (let* ([n-as-num (or n -1)]) (let loop ([step 0] [remaining view-history] [seen-final? #f]) (cond [(null? remaining) (cond [seen-final? (list `nomatch/seen-final (- step 1))] [else (list `nomatch (- step 1))])] [(and (> step n-as-num) (p (car remaining))) step] [else (loop (+ step 1) (cdr remaining) (or seen-final? (finished-stepping-step? (car remaining))))])))) ;; find-earlier-step : like find-later-step, but searches backward from ;; the given step. (define (find-earlier-step p n) (unless (number? n) (error 'find-earlier-step "can't find earlier step when no step is displayed.")) (let* ([to-search (reverse (take view-history n))]) (let loop ([step (- n 1)] [remaining to-search]) (cond [(null? remaining) `nomatch] [(p (car remaining)) step] [else (loop (- step 1) (cdr remaining))])))) ;; STEP PREDICATES: ;; is this an application step? (define (application-step? history-entry) (match history-entry [(struct step (text (or 'user-application 'finished-or-error) posns)) #t] [else #f])) ;; is this the finished-stepping step? (define (finished-stepping-step? history-entry) (match (step-kind history-entry) ['finished-or-error #t] [else #f])) ;; is this step on the selected expression? (define (selected-exp-step? history-entry) (ormap (span-overlap selection-start selection-end) (step-posns history-entry))) ;; build gui object: ;; next-of-specified-kind : starting at the current view, search forward for the ;; desired step or wait for it if not found (define (next-of-specified-kind right-kind? msg) (next-of-specified-kind/helper right-kind? view msg)) ;; first-of-specified-kind : similar to next-of-specified-kind, but ;; always start at zero (define (first-of-specified-kind right-kind? msg) (next-of-specified-kind/helper right-kind? #f msg)) ;; next-of-specified-kind/helper : if the desired step ;; is already in the list, display it; otherwise, give up. (define (next-of-specified-kind/helper right-kind? starting-step msg) (match (find-later-step right-kind? starting-step) [(? number? n) (update-view/existing n)] [(list `nomatch step) (message-box (string-constant stepper-no-such-step/title) msg) (when (>= num-steps-available 0) (update-view/existing step))] [(list `nomatch/seen-final step) (message-box (string-constant stepper-no-such-step/title) msg) (when (>= num-steps-available 0) (update-view/existing step))])) ;; prior-of-specified-kind: if the desired step is already in the list, display ;; it; otherwise, put up a dialog and jump to the first step. (define (prior-of-specified-kind right-kind? msg) (match (find-earlier-step right-kind? view) [(? number? found-step) (update-view/existing found-step)] [`nomatch (message-box (string-constant stepper-no-such-step/title) msg) (when (>= num-steps-available 0) (update-view/existing 0))])) ;; BUTTON/CHOICE BOX PROCEDURES ;; respond to a click on the "next" button (define (next) (next-of-specified-kind (lambda (x) #t) (string-constant stepper-no-later-step))) ;; previous : the action of the 'previous' button (define (previous) (prior-of-specified-kind (lambda (x) #t) (string-constant stepper-no-earlier-step))) ;; respond to a click on the "Jump To..." choice (define (jump-to control event) ((second (list-ref pulldown-choices (send control get-selection))))) ;; jump-to-beginning : the action of the choice menu entry (define (jump-to-beginning) (first-of-specified-kind (lambda (x) #t) ;; I don't believe this can fail... "internal error 2010-01-10 21:48")) ;; jump-to-end : the action of the jump-to-end choice box option (define (jump-to-end) (first-of-specified-kind finished-stepping-step? (string-constant stepper-no-last-step))) ;; jump-to-selected : the action of the jump to selected choice box option (define (jump-to-selected) (first-of-specified-kind selected-exp-step? (string-constant stepper-no-selected-step))) ;; jump-to-next-application : the action of the jump to next application ;; choice box option (define (jump-to-next-application) (next-of-specified-kind application-step? (string-constant stepper-no-later-application-step))) ;; jump-to-prior-application : the action of the "jump to prior application" ;; choice box option (define (jump-to-prior-application) (prior-of-specified-kind application-step? (string-constant stepper-no-earlier-application-step))) ;; GUI ELEMENTS: (define s-frame (make-object stepper-frame% drracket-tab)) (define top-panel (new horizontal-panel% [parent (send s-frame get-area-container)] [horiz-margin 5] ;[style '(border)] ; for layout testing only [stretchable-width #t] [stretchable-height #f])) (define button-panel (new horizontal-panel% [parent top-panel] [alignment '(center top)] ;[style '(border)] ; for layout testing only [stretchable-width #t] [stretchable-height #f])) (define logo-canvas (new (class bitmap-canvas% (super-new [parent top-panel] [bitmap (compiled-bitmap (stepper-logo #:height 32))]) (define/override (on-event evt) (when (eq? (send evt get-event-type) 'left-up) (show-about-dialog s-frame)))))) (define prev-img (compiled-bitmap (step-back-icon #:color run-icon-color #:height (toolbar-icon-height)))) (define previous-button (new button% [label (list prev-img (string-constant stepper-previous) 'left)] [parent button-panel] [callback (λ (_1 _2) (previous))] [enabled #f])) (define next-img (compiled-bitmap (step-icon #:color run-icon-color #:height (toolbar-icon-height)))) (define next-button (new button% [label (list next-img (string-constant stepper-next) 'right)] [parent button-panel] [callback (λ (_1 _2) (next))] [enabled #f])) (define pulldown-choices `((,(string-constant stepper-jump-to-beginning) ,jump-to-beginning) (,(string-constant stepper-jump-to-end) ,jump-to-end) (,(string-constant stepper-jump-to-selected) ,jump-to-selected) (,(string-constant stepper-jump-to-next-application) ,jump-to-next-application) (,(string-constant stepper-jump-to-previous-application) ,jump-to-prior-application))) (define jump-button (new choice% [label (string-constant stepper-jump)] [choices (map first pulldown-choices)] [parent button-panel] [callback jump-to] [enabled #f])) (define canvas (make-object x:stepper-canvas% (send s-frame get-area-container))) ;; counting steps... (define status-text (new text%)) (define status-canvas (new editor-canvas% [parent button-panel] [editor status-text] [stretchable-width #f] [style '(transparent no-border no-hscroll no-vscroll)] ;; some way to get the height of a line of text? [min-width 100])) ;; update-view/existing : set an existing step as the one shown in the ;; frame (define (update-view/existing new-view) (set! view new-view) (let ([e (step-text (list-ref view-history view))]) (send e begin-edit-sequence) (send canvas set-editor e) (send e reset-width canvas) ;; why set the position within the step? I'm confused by this.--JBC (send e set-position (send e last-position)) (send e end-edit-sequence)) (update-status-bar)) ;; set the status bar to the correct m/n text. (define (update-status-bar) (send status-text begin-edit-sequence) (send status-text lock #f) (send status-text delete 0 (send status-text last-position)) ;; updated to yield 1-based step numbering rather than 0-based numbering. (send status-text insert (format "~a/~a" (if view (+ 1 view) "none") (length view-history))) (send status-text lock #t) (send status-text end-edit-sequence)) (define update-status-bar-semaphore (make-semaphore 1)) (define (enable-all-buttons) (send previous-button enable #t) (send next-button enable #t) (send jump-button enable #t)) (define (print-current-view item evt) (send (send canvas get-editor) print)) ;; code for dealing with runaway processes: (define runaway-counter-limit 500) (define disable-runaway-counter #f) (define runaway-counter 0) ;; runs on the stepped-process side. ;; checks to see if the process has taken too ;; many steps. If so, send a message and block ;; for a response, then send the result. Otherwise, ;; just send the result. (define (deliver-result-to-gui result) (when (not disable-runaway-counter) (set! runaway-counter (+ runaway-counter 1))) (when (= runaway-counter runaway-counter-limit) (define runaway-semaphore (make-semaphore 0)) (async-channel-put view-channel (runaway-process runaway-semaphore)) ;; wait for a signal to continue running: (semaphore-wait runaway-semaphore)) (async-channel-put view-channel result)) (define keep-running-message (string-append "The program running in the stepper has taken a whole bunch of steps. " "Do you want to continue running it for now, halt, or let it run " "without asking again?")) (define (confirm-running) (define message-box-result (message-box/custom "Keep Running Program?" keep-running-message "Continue for now" "Halt" "Continue uninterrupted" #f ;; use the stepper window instead? '(stop disallow-close default=1) )) (match message-box-result ;; continue-for-now: [1 (set! runaway-counter 0) #t] ;; halt: [2 #f] ;; continue-forever: [3 (set! runaway-counter 0) (set! disable-runaway-counter #t) #t])) ;; translates a result into a step ;; format-result : step-result -> step? (define (format-result result) (match result [(struct before-after-result (pre-exps post-exps kind pre-src post-src)) (make-step (new x:stepper-text% [left-side pre-exps] [right-side post-exps] [show-inexactness? (send language-level stepper:show-inexactness?)]) kind (list pre-src post-src))] [(struct before-error-result (pre-exps err-msg pre-src)) (make-step (new x:stepper-text% [left-side pre-exps] [right-side err-msg] [show-inexactness? (send language-level stepper:show-inexactness?)]) 'finished-or-error (list pre-src))] [(struct error-result (err-msg)) (make-step (new x:stepper-text% [left-side null] [right-side err-msg] [show-inexactness? (send language-level stepper:show-inexactness?)]) 'finished-or-error (list))] [(struct finished-stepping ()) (make-step x:finished-text 'finished-or-error (list))])) ;; program-expander-prime : wrap the program-expander for a couple of reasons: ;; 1) we need to capture the custodian as the thread starts up: ;; ok, it was just one. ;; (define (program-expander-prime init iter) (program-expander (lambda args (send s-frame set-custodian! (current-custodian)) (apply init args)) iter)) ;; CONFIGURE GUI ELEMENTS (send s-frame set-printing-proc print-current-view) (send canvas stretchable-height #t) (send (send s-frame edit-menu:get-undo-item) enable #f) (send (send s-frame edit-menu:get-redo-item) enable #f) (define stepper-frame-eventspace (send s-frame get-eventspace)) ;; START THE MODEL (start-listener-thread stepper-frame-eventspace) (model:go program-expander-prime ;; what do do with the results: deliver-result-to-gui (get-render-settings render-to-string render-to-sexp (send language-level stepper:enable-let-lifting?) (send language-level stepper:show-consumed-and/or-clauses?) (send language-level stepper:show-lambdas-as-lambdas?))) (send s-frame show #t) ;; turn on the buttons and display the first step when it shows up: (thread (lambda () (semaphore-wait first-step-sema) (parameterize ([current-eventspace stepper-frame-eventspace]) (queue-callback (lambda () (jump-to-beginning) (enable-all-buttons)))))) s-frame) ;; UTILITY FUNCTIONS: ;; span-overlap : number number -> posn-info -> boolean ;; return true if the selection is of zero length and precedes a char of the ;; stepping expression, *or* if the selection has positive overlap with the ;; stepping expression. (define ((span-overlap selection-start selection-end) source-posn-info) (match source-posn-info [#f #f] [(struct model:posn-info (posn span)) (let ([end (+ posn span)]) (and posn ;; you can *almost* combine these two, but not quite. (cond [(= selection-start selection-end) (and (<= posn selection-start) (< selection-start end))] [else (let ([overlap-begin (max selection-start posn)] ;; nb: we don't want zero-length overlaps at the end. ;; compensate by subtracting one from the end of the ;; current expression. [overlap-end (min selection-end end)]) ;; #t if there's positive overlap: (< overlap-begin overlap-end))])))])) ;; a few unit tests. Use them if changing span-overlap. ;; ...oops, can't use module+ inside of a unit. #;(module+ test (require rackunit) ;; zero-length selection cases: (check-equal? ((span-overlap 13 13) (model:make-posn-info 14 4)) #f) (check-equal? ((span-overlap 14 14) (model:make-posn-info 14 4)) #t) (check-equal? ((span-overlap 18 18) (model:make-posn-info 14 4)) #f) ;; nonzero-length selection cases: (check-equal? ((span-overlap 13 14) (model:make-posn-info 14 4)) #f) (check-equal? ((span-overlap 13 15) (model:make-posn-info 14 4)) #t) (check-equal? ((span-overlap 13 23) (model:make-posn-info 14 4)) #t) (check-equal? ((span-overlap 16 17) (model:make-posn-info 14 4)) #t) (check-equal? ((span-overlap 16 24) (model:make-posn-info 14 4)) #t) (check-equal? ((span-overlap 18 21) (model:make-posn-info 14 4)) #f))