about summary refs log tree commit diff stats
path: root/edit
Commit message (Expand)AuthorAgeFilesLines
* 3490Kartik K. Agaram2016-10-098-155/+155
* 3489Kartik K. Agaram2016-10-0811-751/+751
* 3457Kartik K. Agaram2016-10-0611-751/+751
* 3445Kartik K. Agaram2016-10-0611-507/+507
* 3429 - standardize Mu scenariosKartik K. Agaram2016-09-2812-1034/+1100
* 3428Kartik K. Agaram2016-09-281-2/+2
* 3396Kartik K. Agaram2016-09-1712-916/+916
* 3391 - type abbreviations everywhereKartik K. Agaram2016-09-1712-1771/+1771
* 3369Kartik K. Agaram2016-09-151-12/+0
* 3341Kartik K. Agaram2016-09-124-9/+9
* 3338Kartik K. Agaram2016-09-124-6/+6
* 3337 - first use of type abbreviations: textKartik K. Agaram2016-09-1212-435/+435
* 3233 - change how Mu escapes stringsKartik K. Agaram2016-08-201-4/+3
* 3168 - skip loading recipe 'main' in edit/Kartik K. Agaram2016-08-123-3/+3
* 3111Stephen Malina2016-07-142-4/+10
* 3067Kartik K. Agaram2016-06-255-69/+69
* 3053 - keep cursor stable on resizeKartik K. Agaram2016-06-121-1/+85
* 3052 - make edit/ render functions higher-orderKartik K. Agaram2016-06-127-44/+44
* 3039 - bug fix in edit/Kartik K. Agaram2016-06-092-0/+43
* 3038 - track down a long-standing bugKartik K. Agaram2016-06-081-25/+56
* 3025 - fix a minor annoyance in edit/Kartik K. Agaram2016-05-291-3/+157
* 2990Kartik K. Agaram2016-05-201-17/+17
* 2983 - migrate buttons over to sandbox/Kartik K. Agaram2016-05-193-30/+3
* 2982Kartik K. Agaram2016-05-191-6/+6
* 2981 - sandbox buttons implemented in edit/Kartik K. Agaram2016-05-194-119/+373
* 2980Kartik K. Agaram2016-05-191-0/+3
* 2979Kartik K. Agaram2016-05-195-0/+0
* 2978 - support new 'edit' button in edit/Kartik K. Agaram2016-05-193-26/+101
* 2977 - draw new sandbox menu in edit/Kartik K. Agaram2016-05-197-376/+401
* 2976Kartik K. Agaram2016-05-191-4/+18
* 2975Kartik K. Agaram2016-05-194-10/+11
* 2954 - bugfix: $systemKartik K. Agaram2016-05-112-9/+7
* 2953 - use pgup/pgdn to scroll through sandboxesKartik K. Agaram2016-05-113-59/+48
* 2928 - fix sandbox restore in edit/ and sandbox/Kartik K. Agaram2016-05-051-3/+7
* 2890Kartik K. Agaram2016-05-021-1/+1
* 2864 - replace all address:shared with just addressKartik K. Agaram2016-04-2411-1333/+1333
* 2861 - 'maybe-convert' no longer returns addressKartik K. Agaram2016-04-239-123/+127
* 2853 - purge get-address from edit/ appKartik K. Agaram2016-04-2211-470/+563
* 2852Kartik K. Agaram2016-04-211-1/+2
* 2851Kartik K. Agaram2016-04-211-109/+109
* 2830 - bring back deleted test from 2829Kartik K. Agaram2016-04-101-2/+0
* 2806 - bugfix: cleaning up in 'reload'Kartik K. Agaram2016-03-211-1/+1
* 2782 - directly use string literals everywhereKartik K. Agaram2016-03-144-20/+10
* 2777 - pass literal strings into functionsKartik K. Agaram2016-03-131-9/+10
* 2735 - define recipes using 'def'Kartik K. Agaram2016-03-0811-170/+170
* 2716 - more holes in immutability checksKartik K. Agaram2016-02-262-4/+4
* 2707Kartik K. Agaram2016-02-253-14/+0
* 2705 - eradicate 'warning' from appsKartik K. Agaram2016-02-251-49/+49
* 2661 - warn if a reply doesn't match recipe headerKartik K. Agaram2016-02-151-1/+2
* 2624Kartik K. Agaram2016-02-012-15/+23
ighlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */ .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .highlight .ge { font-style: italic } /* Generic.Emph */ .highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ .highlight .gr { color: #aa0000 } /* Generic.Error */ .highlight .gh { color: #333333 } /* Generic.Heading */ .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ .highlight .go { color: #888888 } /* Generic.Output */ .highlight .gp { color: #555555 } /* Generic.Prompt */ .highlight .gs { font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #666666 } /* Generic.Subheading */ .highlight .gt { color: #aa0000 } /* Generic.Traceback */ .highlight .kc { color: #008800; font-weight: bold } /* Keyword.Constant */ .highlight .kd { color: #008800; font-weight: bold } /* Keyword.Declaration */ .highlight .kn { color: #008800; font-weight: bold } /* Keyword.Namespace */ .highlight .kp { color: #008800 } /* Keyword.Pseudo */ .highlight .kr { color: #008800; font-weight: bold } /* Keyword.Reserved */ .highlight .kt { color: #888888; font-weight: bold } /* Keyword.Type */ .highlight .m { color: #0000DD; font-weight: bold } /* Literal.Number */ .highlight .s { color: #dd2200; background-color: #fff0f0 } /* Literal.String */ .highlight .na { color: #336699 } /* Name.Attribute */ .highlight .nb { color: #003388 } /* Name.Builtin */ .highlight .nc { color: #bb0066; font-weight: bold } /* Name.Class */ .highlight .no { color: #003366; font-weight: bold } /* Name.Constant */ .highlight .nd { color: #555555 } /* Name.Decorator */ .highlight .ne { color: #bb0066; font-weight: bold } /* Name.Exception */ .highlight .nf { color: #0066bb; font-weight: bold } /* Name.Function */ .highlight .nl { color: #336699; font-style: italic } /* Name.Label */ .highlight .nn { color: #bb0066; font-weight: bold } /* Name.Namespace */ .highlight .py { color: #336699; font-weight: bold } /* Name.Property */ .highlight .nt { color: #bb0066; font-weight: bold } /* Name.Tag */ .highlight .nv { color: #336699 } /* Name.Variable */ .highlight .ow { color: #008800 } /* Operator.Word */ .highlight .w { color: #bbbbbb } /* Text.Whitespace */ .highlight .mb { color: #0000DD; font-weight: bold } /* Literal.Number.Bin */ .highlight .mf { color: #0000DD; font-weight: bold } /* Literal.Number.Float */ .highlight .mh { color: #0000DD; font-weight: bold } /* Literal.Number.Hex */ .highlight .mi { color: #0000DD; font-weight: bold } /* Literal.Number.Integer */ .highlight .mo { color: #0000DD; font-weight: bold } /* Literal.Number.Oct */ .highlight .sa { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Affix */ .highlight .sb { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Backtick */ .highlight .sc { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Char */ .highlight .dl { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Delimiter */ .highlight .sd { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Doc */ .highlight .s2 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Double */ .highlight .se { color: #0044dd; background-color: #fff0f0 } /* Literal.String.Escape */ .highlight .sh { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Heredoc */ .highlight .si { color: #3333bb; background-color: #fff0f0 } /* Literal.String.Interpol */ .highlight .sx { color: #22bb22; background-color: #f0fff0 } /* Literal.String.Other */ .highlight .sr { color: #008800; background-color: #fff0ff } /* Literal.String.Regex */ .highlight .s1 { color: #dd2200; background-color: #fff0f0 } /* Literal.String.Single */ .highlight .ss { color: #aa6600; background-color: #fff0f0 } /* Literal.String.Symbol */ .highlight .bp { color: #003388 } /* Name.Builtin.Pseudo */ .highlight .fm { color: #0066bb; font-weight: bold } /* Name.Function.Magic */ .highlight .vc { color: #336699 } /* Name.Variable.Class */ .highlight .vg { color: #dd7700 } /* Name.Variable.Global */ .highlight .vi { color: #3333bb } /* Name.Variable.Instance */ .highlight .vm { color: #336699 } /* Name.Variable.Magic */ .highlight .il { color: #0000DD; font-weight: bold } /* Literal.Number.Integer.Long */
; a simple line editor for reading lisp expressions.
; colors strings and comments. nested parens get different colors.
;
; needs to do its own raw keyboard/screen management since we need to decide
; how to color each key right as it is printed.
; lots of logic devoted to handling backspace correctly.

; keyboard screen abort continuation -> string
(function read-expression [
  (default-space:space-address <- new space:literal 60:literal)
  (k:keyboard-address <- next-input)
  (screen:terminal-address <- next-input)
  (abort:continuation <- next-input)
  (history:buffer-address <- next-input)  ; buffer of strings
  (history-length:integer <- get history:buffer-address/deref length:offset)
  (current-history-index:integer <- copy history-length:integer)
  (result:buffer-address <- init-buffer 10:literal)  ; string to maybe add to
  (open-parens:integer <- copy 0:literal)  ; for balancing parens and tracking nesting depth
  ; we can change color when backspacing over parens or comments or strings,
  ; but we need to know that they aren't escaped
  (escapes:buffer-address <- init-buffer 5:literal)
  ; to not return after just a comment
  (not-empty?:boolean <- copy nil:literal)
  { begin
    ; repeatedly read keys from the keyboard
    ;   test: 34<enter>
    (done?:boolean <- process-key default-space:space-address k:keyboard-address screen:terminal-address)
    (loop-unless done?:boolean)
  }
  ; trim trailing newline in result (easier history management below)
  { begin
    (l:character <- last result:buffer-address)
    (trailing-newline?:boolean <- equal l:character ((#\newline literal)))
    (break-unless trailing-newline?:boolean)
    (len:integer-address <- get-address result:buffer-address/deref length:offset)
    (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
  }
  ; test: 3<enter> => size of s is 2
  (s:string-address <- to-array result:buffer-address)
  (reply s:string-address)
])

(function process-key [  ; return t to signal end of expression
  (default-space:space-address <- new space:literal 60:literal)
  (0:space-address/names:read-expression <- next-input)
  (k:keyboard-address <- next-input)
  (screen:terminal-address <- next-input)
  (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
  (len:integer-address <- get-address result:buffer-address/space:1/deref length:offset)
  (maybe-cancel-this-expression c:character abort:continuation/space:1)
  ; check for ctrl-d and exit
  { begin
    (eof?:boolean <- equal c:character ((ctrl-d literal)))
    (break-unless eof?:boolean)
    ; return empty expression
    (s:string-address-address <- get-address result:buffer-address/space:1/deref data:offset)
    (s:string-address-address/deref <- copy nil:literal)
    (reply t:literal)
  }
  ; check for backspace
  ;   test: 3<backspace>4<enter>
  ;   todo: backspace past newline
  { begin
    (backspace?:boolean <- equal c:character ((#\backspace literal)))
    (break-unless backspace?:boolean)
    (print-character screen:terminal-address c:character/backspace)
    { begin
      ; delete last character if any
      (zero?:boolean <- lesser-or-equal len:integer-address/deref 0:literal)
      (break-if zero?:boolean)
      (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
      ; switch colors
      ;   test: "a"<backspace>bc"
      ;   test: "a\"<backspace>bc"
      { begin
        (backspaced-over-close-quote?:boolean <- backspaced-over-unescaped? result:buffer-address/space:1 ((#\" literal)) escapes:buffer-address/space:1)  ; "
        (break-unless backspaced-over-close-quote?:boolean)
        (slurp-string result:buffer-address/space:1 escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
        (reply nil:literal)
      }
      ;   test: (+ 1 (<backspace>2)
      ;   test: (+ 1 #\(<backspace><backspace><backspace>2)
      { begin
        (backspaced-over-open-paren?:boolean <- backspaced-over-unescaped? result:buffer-address/space:1 ((#\( literal)) escapes:buffer-address/space:1)
        (break-unless backspaced-over-open-paren?:boolean)
        (open-parens:integer/space:1 <- subtract open-parens:integer/space:1 1:literal)
        (reply nil:literal)
      }
      ;   test: (+ 1 2)<backspace> 3)
      ;   test: (+ 1 2#\)<backspace><backspace><backspace> 3)
      { begin
        (backspaced-over-close-paren?:boolean <- backspaced-over-unescaped? result:buffer-address/space:1 ((#\) literal)) escapes:buffer-address/space:1)
        (break-unless backspaced-over-close-paren?:boolean)
        (open-parens:integer/space:1 <- add open-parens:integer/space:1 1:literal)
        (reply nil:literal)
      }
    }
    (reply nil:literal)
  }
  ; up arrow; switch to previous item in history
  { begin
    (up-arrow?:boolean <- equal c:character ((up literal)))
    (break-unless up-arrow?:boolean)
    ; if history exists
    ;   test: <up><enter>  up without history has no effect
    { begin
      (empty-history?:boolean <- lesser-or-equal history-length:integer/space:1 0:literal)
      (break-unless empty-history?:boolean)
      (reply nil:literal)
    }
    ; if pointer not already at start of history
    ;   test: 34<enter><up><up><enter>  up past history has no effect
    { begin
      (at-history-start?:boolean <- lesser-or-equal current-history-index:integer/space:1 0:literal)
      (break-unless at-history-start?:boolean)
      (reply nil:literal)
    }
    ; then update history index, copy into current buffer
    ;   test: 34<enter><up><enter>  up restores previous command
    ;   test todo: 34<enter>23<up>34<down><enter>  up doesn't mess up typing on current line
    ;   test todo: 34<enter><up>5<enter><up><up>  commands don't modify history
    ;   test todo: multi-line expressions
    ; identify the history item
    (current-history-index:integer/space:1 <- subtract current-history-index:integer/space:1 1:literal)
    (switch-to-history 0:space-address screen:terminal-address)
    ; <enter> is trimmed in the history expression, so wait for the human to
    ; hit <enter> again or backspace to make edits
    (reply nil:literal)
  }
  ; down arrow; switch to next item in history
  { begin
    (down-arrow?:boolean <- equal c:character ((down literal)))
    (break-unless down-arrow?:boolean)
    ; if history exists
    ;   test: <down><enter>  down without history has no effect
    { begin
      (empty-history?:boolean <- lesser-or-equal history-length:integer/space:1 0:literal)
      (break-unless empty-history?:boolean)
      (reply nil:literal)
    }
    ; if pointer not already at end of history
    ;   test: 34<enter><down><down><enter>  up past history has no effect
    { begin
      (x:integer <- subtract history-length:integer/space:1 1:literal)
      (before-history-end?:boolean <- greater-or-equal current-history-index:integer/space:1 x:integer)
      (break-unless before-history-end?:boolean)
      (reply nil:literal)
    }
    ; then update history index, copy into current buffer
    ;   test: 34<enter><up><enter>  up restores previous command
    ;   test todo: 34<enter>23<up>34<down><enter>  up doesn't mess up typing on current line
    ;   test todo: 34<enter><up>5<enter><up><up>  commands don't modify history
    ;   test todo: multi-line expressions
    ; identify the history item
    (current-history-index:integer/space:1 <- add current-history-index:integer/space:1 1:literal)
    (switch-to-history 0:space-address screen:terminal-address)
    ; <enter> is trimmed in the history expression, so wait for the human to
    ; hit <enter> again or backspace to make edits
    (reply nil:literal)
  }
  ; if it's a newline, decide whether to return
  ;   test: <enter>34<enter>
  { begin
    (newline?:boolean <- equal c:character ((#\newline literal)))
    (break-unless newline?:boolean)
    (print-character screen:terminal-address c:character/newline)
    (at-top-level?:boolean <- lesser-or-equal open-parens:integer/space:1 0:literal)
    (end-expression?:boolean <- and at-top-level?:boolean not-empty?:boolean/space:1)
    (reply end-expression?:boolean)
  }
  ; printable character; save
;?   ($print (("append\n" literal))) ;? 2
  (result:buffer-address/space:1 <- append result:buffer-address/space:1 c:character)
;?   ($print (("done\n" literal))) ;? 2
  ; if it's backslash, read, save and print one additional character
  ;   test: (prn #\()
  { begin
    (backslash?:boolean <- equal c:character ((#\\ literal)))
    (break-unless backslash?:boolean)
    (print-character screen:terminal-address c:character/backslash 7:literal/white)
    (result:buffer-address/space:1 escapes:buffer-address/space:1 <- slurp-escaped-character result:buffer-address/space:1 7:literal/white escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
    (reply nil:literal)
  }
  ; if it's a semi-colon, parse a comment
  { begin
    (comment?:boolean <- equal c:character ((#\; literal)))
    (break-unless comment?:boolean)
    (print-character screen:terminal-address c:character/semi-colon 4:literal/fg/blue)
    (comment-read?:boolean <- slurp-comment result:buffer-address/space:1 escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
    ; return if comment was read (i.e. consumed a newline)
    ; test: ;a<backspace><backspace> (shouldn't end command until <enter>)
    { begin
      (break-if comment-read?:boolean)
      (reply nil:literal)
    }
    ; and we're not within parens
    ;   test: (+ 1 2)  ; comment<enter>
    ;   test: (+ 1<enter>; abc<enter>2)<enter>
    ;   test: ; comment<enter>(+ 1 2)<enter>
    ;   too expensive to build: 3<backspace>; comment<enter>(+ 1 2)<enter>
    (at-top-level?:boolean <- lesser-or-equal open-parens:integer/space:1 0:literal)
    (end-expression?:boolean <- and at-top-level?:boolean not-empty?:boolean/space:1)
    (reply end-expression?:boolean)
  }
  ; if it's not whitespace, set not-empty? and continue
  { begin
    (space?:boolean <- equal c:character ((#\space literal)))
    (break-if space?:boolean)
    (newline?:boolean <- equal c:character ((#\newline literal)))
    (break-if newline?:boolean)
    (tab?:boolean <- equal c:character ((tab literal)))
    (break-if tab?:boolean)
    (not-empty?:boolean/space:1 <- copy t:literal)
    ; fall through
  }
  ; if it's a quote, parse a string
  { begin
    (string-started?:boolean <- equal c:character ((#\" literal)))  ; for vim: "
    (break-unless string-started?:boolean)
    (print-character screen:terminal-address c:character/open-quote 6:literal/fg/cyan)
    (slurp-string result:buffer-address/space:1 escapes:buffer-address/space:1 abort:continuation/space:1 k:keyboard-address screen:terminal-address)
    (reply nil:literal)
  }
  ; color parens by depth, so they're easy to balance
  ;   test: (+ 1 1)<enter>
  ;   test: (def foo () (+ 1 (* 2 3)))<enter>
  { begin
    (open-paren?:boolean <- equal c:character ((#\( literal)))
    (break-unless open-paren?:boolean)
    (_ color-code:integer <- divide-with-remainder open-parens:integer/space:1 3:literal)  ; 3 distinct colors for parens
    (color-code:integer <- add color-code:integer 1:literal)
    (print-character screen:terminal-address c:character/open-paren color-code:integer)
    (open-parens:integer/space:1 <- add open-parens:integer/space:1 1:literal)
;?     ($print open-parens:integer/space:1) ;? 2
    (reply nil:literal)
  }
  { begin
    (close-paren?:boolean <- equal c:character ((#\) literal)))
    (break-unless close-paren?:boolean)
    (open-parens:integer/space:1 <- subtract open-parens:integer/space:1 1:literal)
    (_ color-code:integer <- divide-with-remainder open-parens:integer/space:1 3:literal)  ; 3 distinct colors for parens
    (color-code:integer <- add color-code:integer 1:literal)
    (print-character screen:terminal-address c:character/close-paren color-code:integer)
;?     ($print open-parens:integer/space:1) ;? 2
    (reply nil:literal)
  }
  ; if all else fails, print the character without color
  (print-character screen:terminal-address c:character/regular)
  ;   todo: error on space outside parens, like python
  ;   todo: []
  ;   todo: history on up/down
  (reply nil:literal)
])

(function switch-to-history [
  (default-space:space-address <- new space:literal 30:literal)
  (0:space-address/names:read-expression <- next-input)
  (screen:terminal-address <- next-input)
  (clear-repl-state 0:space-address)
  (curr-history:string-address <- buffer-index history:buffer-address/space:1 current-history-index:integer/space:1)
  (curr-history-len:integer <- length curr-history:string-address/deref)
  ; and retype it into the current expression
  (hist:keyboard-address <- init-keyboard curr-history:string-address)
  (hist-index:integer-address <- get-address hist:keyboard-address/deref index:offset)
  { begin
    (done?:boolean <- greater-or-equal hist-index:integer-address/deref curr-history-len:integer)
    (break-if done?:boolean)
    (sub-return:boolean <- process-key 0:space-address hist:keyboard-address screen:terminal-address)
    (assert-false sub-return:boolean (("recursive call to process keys thought it was done" literal)))
    (loop)
  }
])

(function clear-repl-state [
  (default-space:space-address/names:read-expression <- next-input)
  ; clear result
  (len:integer-address <- get-address result:buffer-address/deref length:offset)
  (backspace-over len:integer-address/deref screen:terminal-address)
  (len:integer-address/deref <- copy 0:literal)
  ; clear other state accumulated for the existing expression
  (open-parens:integer <- copy 0:literal)
  (escapes:buffer-address <- init-buffer 5:literal)
  (not-empty?:boolean <- copy nil:literal)
])

(function backspace-over [
  (default-space:space-address <- new space:literal 30:literal)
  (len:integer <- next-input)
  (screen:terminal-address <- next-input)
  { begin
    (done?:boolean <- lesser-or-equal len:integer 0:literal)
    (break-if done?:boolean)
    (print-character screen:terminal-address ((#\backspace literal)))
    (len:integer <- subtract len:integer 1:literal)
    (loop)
  }
])

; list of characters, list of indices of escaped characters, abort continuation
; -> whether a comment was consumed (can also return by backspacing past comment leader ';')
(function slurp-comment [
  (default-space:space-address <- new space:literal 30:literal)
  (in:buffer-address <- next-input)
  (escapes:buffer-address <- next-input)
  (abort:continuation <- next-input)
  (k:keyboard-address <- next-input)
  (screen:terminal-address <- next-input)
  ; test: ; abc<enter>
  { begin
    next-key-in-comment
    (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
    (maybe-cancel-this-expression c:character abort:continuation screen:terminal-address)  ; test: check needs to come before print
    (print-character screen:terminal-address c:character 4:literal/fg/blue)
    ; handle backspace
    ;   test: ; abc<backspace><backspace>def<enter>
    ;   todo: how to exit comment?
    { begin
      (backspace?:boolean <- equal c:character ((#\backspace literal)))
      (break-unless backspace?:boolean)
      (len:integer-address <- get-address in:buffer-address/deref length:offset)
      ; buffer has to have at least the semi-colon so can't be empty
      (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
      ; if we erase start of comment, return
      (comment-deleted?:boolean <- backspaced-over-unescaped? in:buffer-address ((#\; literal)) escapes:buffer-address)  ; "
      (jump-unless comment-deleted?:boolean next-key-in-comment:offset)  ; loop
      (reply nil:literal/read-comment?)
    }
    (in:buffer-address <- append in:buffer-address c:character)
    (newline?:boolean <- equal c:character ((#\newline literal)))
    (loop-unless newline?:boolean)
  }
  (reply t:literal/read-comment?)
])

(function slurp-string [
  (default-space:space-address <- new space:literal 30:literal)
  (in:buffer-address <- next-input)
  (escapes:buffer-address <- next-input)
  (abort:continuation <- next-input)
  (k:keyboard-address <- next-input)
  (screen:terminal-address <- next-input)
  ; test: "abc"
  { begin
    next-key-in-string
    (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
    (maybe-cancel-this-expression c:character abort:continuation screen:terminal-address)  ; test: check needs to come before print
    (print-character screen:terminal-address c:character 6:literal/fg/cyan)
    ; handle backspace
    ;   test: "abc<backspace>d"
    ;   todo: how to exit string?
    { begin
      (backspace?:boolean <- equal c:character ((#\backspace literal)))
      (break-unless backspace?:boolean)
      (len:integer-address <- get-address in:buffer-address/deref length:offset)
      ; typed a quote before calling slurp-string, so can't be empty
      (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
      ; if we erase start of string, return
      ;   test: "<backspace>34
      (string-deleted?:boolean <- backspaced-over-unescaped? in:buffer-address ((#\" literal)) escapes:buffer-address)  ; "
;?       ($print string-deleted?:boolean) ;? 1
      (jump-if string-deleted?:boolean end:offset)  ; break
      (jump next-key-in-string:offset)  ; loop
    }
    (in:buffer-address <- append in:buffer-address c:character)
    ; break on quote -- unless escaped by backslash
    ;   test: "abc\"ef"
    { begin
      (backslash?:boolean <- equal c:character ((#\\ literal)))
      (break-unless backslash?:boolean)
      (in:buffer-address escapes:buffer-address <- slurp-escaped-character in:buffer-address 6:literal/cyan escapes:buffer-address abort:continuation k:keyboard-address screen:terminal-address)
      (jump next-key-in-string:offset)  ; loop
    }
    ; if not backslash
    (end-quote?:boolean <- equal c:character ((#\" literal)))  ; for vim: "
    (loop-unless end-quote?:boolean)
  }
  end
])

; buffer to add character to, color to print it in to the screen, abort continuation
(function slurp-escaped-character [
  (default-space:space-address <- new space:literal 30:literal)
  (in:buffer-address <- next-input)
  (color-code:integer <- next-input)
  (escapes:buffer-address <- next-input)
  (abort:continuation <- next-input)
  (k:keyboard-address <- next-input)
  (screen:terminal-address <- next-input)
  (c:character <- wait-for-key k:keyboard-address silent:literal/terminal)
  (maybe-cancel-this-expression c:character abort:continuation screen:terminal-address)  ; test: check needs to come before print
  (print-character screen:terminal-address c:character color-code:integer)
  (len:integer-address <- get-address in:buffer-address/deref length:offset)
  (escapes:buffer-address <- append escapes:buffer-address len:integer-address/deref)
;?   ($print (("+" literal))) ;? 1
  ; handle backspace
  ;   test: "abc\<backspace>def"
  ;   test: #\<backspace>
  { begin
    (backspace?:boolean <- equal c:character ((#\backspace literal)))
    (break-unless backspace?:boolean)
    ; just typed a backslash, so buffer can't be empty
    (len:integer-address/deref <- subtract len:integer-address/deref 1:literal)
    (elen:integer-address <- get-address escapes:buffer-address/deref length:offset)
    (elen:integer-address/deref <- subtract elen:integer-address/deref 1:literal)
;?     ($print (("-" literal))) ;? 1
    (reply in:buffer-address/same-as-arg:0 escapes:buffer-address/same-as-arg:2)
  }
  ; if not backspace, save and return
  (in:buffer-address <- append in:buffer-address c:character)
  (reply in:buffer-address/same-as-arg:0 escapes:buffer-address/same-as-arg:2)
])

(function backspaced-over-unescaped? [
  (default-space:space-address <- new space:literal 30:literal)
  (in:buffer-address <- next-input)
  (expected:character <- next-input)
  (escapes:buffer-address <- next-input)
  ; char just backspaced over matches
  { begin
    (c:character <- past-last in:buffer-address)
    (char-match?:boolean <- equal c:character expected:character)
    (break-if char-match?:boolean)
    (reply nil:literal)
  }
  ; and char before cursor is not an escape
  { begin
    (most-recent-escape:integer <- last escapes:buffer-address)
    (last-idx:integer <- get in:buffer-address/deref length:offset)
;?     ($print most-recent-escape:integer) ;? 1
;?     ($print last-idx:integer) ;? 1
    (was-unescaped?:boolean <- not-equal last-idx:integer most-recent-escape:integer)
    (break-if was-unescaped?:boolean)
    (reply nil:literal)
  }
  (reply t:literal)
])

; return the character past the end of the buffer, if there's room
(function past-last [
  (default-space:space-address <- new space:literal 30:literal)
  (in:buffer-address <- next-input)
  (n:integer <- get in:buffer-address/deref length:offset)
  (s:string-address <- get in:buffer-address/deref data:offset)
  (capacity:integer <- length s:string-address/deref)
  { begin
    (no-space?:boolean <- greater-or-equal n:integer capacity:integer)
    (break-unless no-space?:boolean)
    (reply ((#\null literal)))
  }
  (result:character <- index s:string-address/deref n:integer)
  (reply result:character)
])

(function maybe-cancel-this-expression [
  ; check for ctrl-g and abort
  (default-space:space-address <- new space:literal 30:literal)
  (c:character <- next-input)
  (abort:continuation <- next-input)
  (screen:terminal-address <- next-input)
  { begin
    (interrupt?:boolean <- equal c:character ((ctrl-g literal)))
    (break-unless interrupt?:boolean)
    (print-character screen:terminal-address ((#\^ literal)))
    (print-character screen:terminal-address ((#\G literal)))
    (print-character screen:terminal-address ((#\newline literal)))
    (continue-from abort:continuation)
  }
])

(function main [
  (default-space:space-address <- new space:literal 30:literal)
  (cursor-mode)
  ($print (("connected to anarki! type in an expression, then hit enter. ctrl-d exits. ctrl-g clears the current expression." literal)))
  (print-character nil:literal/terminal ((#\newline literal)))
  ; todo: ctrl-g shouldn't clear history
  (abort:continuation <- current-continuation)
  (history:buffer-address <- init-buffer 5:literal)  ; buffer of buffers of strings, one per expression typed in
  { begin
    (s:string-address <- read-expression nil:literal/keyboard nil:literal/terminal abort:continuation history:buffer-address)
    (break-unless s:string-address)
;?     (x:integer <- length s:string-address/deref) ;? 1
;?     ($print x:integer) ;? 1
;?     ($print ((#\newline literal))) ;? 1
    (history:buffer-address <- append history:buffer-address s:string-address)
;?     (len:integer <- get history:buffer-address/deref length:offset) ;? 1
;?     ($print len:integer) ;? 1
;?     ($print ((#\newline literal))) ;? 1
    (retro-mode)  ; print errors cleanly
;?       (print-string nil:literal/terminal s:string-address) ;? 1
      (t:string-address <- $eval s:string-address)
    (cursor-mode)
    ($print (("=> " literal)))
    (print-string nil:literal/terminal t:string-address)
    (print-character nil:literal/terminal ((#\newline literal)))
    (print-character nil:literal/terminal ((#\newline literal)))  ; empty line separates each expression and result
    (loop)
  }
])