https://github.com/akkartik/mu/blob/main/baremetal/shell/eval.mu
  1 # evaluator (and parser) for the Mu shell language
  2 # inputs:
  3 #   a list of lines, each a list of words, each an editable gap-buffer
  4 #   end: a word to stop at
  5 # output:
  6 #   a stack of values to render that summarizes the result of evaluation until 'end'
  7 
  8 # Key features of the language:
  9 #   words matching '=___' create bindings
 10 #   line boundaries clear the stack (but not bindings)
 11 #   { and } for grouping words
 12 #   break and loop for control flow within groups
 13 #   -> for conditionally skipping the next word or group
 14 
 15 # Example: Pushing numbers from 1 to n on the stack
 16 #
 17 #   3 =n
 18 #   { n 1 <     -> break n n 1- =n loop }
 19 #
 20 # Stack as we evaluate each word in the second line:
 21 #     3 1 false          3 3 2  3   1
 22 #       3                  3 3      2
 23 #                                   3
 24 
 25 # Rules beyond simple postfix:
 26 #   If the final word is `->`, clear stack
 27 #   If the final word is `break`, pop top of stack
 28 #
 29 #   `{` and `}` don't affect evaluation
 30 #   If the final word is `{` or `}`, clear stack (to suppress rendering it)
 31 #
 32 #   If `->` in middle and top of stack is falsy, skip next word or group
 33 #
 34 #   If `break` in middle executes, skip to next containing `}`
 35 #     If no containing `}`, clear stack (incomplete)
 36 #
 37 #   If `loop` in middle executes, skip to previous containing `{`
 38 #     If no containing `}`, clear stack (error)
 39 
 40 fn evaluate _in: (addr line), end: (addr word), out: (addr value-stack) {
 41   clear-value-stack out
 42   var line/eax: (addr line) <- copy _in
 43   var curr-ah/eax: (addr handle word) <- get line, data
 44   var curr/eax: (addr word) <- lookup *curr-ah
 45   evaluate-sub curr, end, out, 1/top-level
 46 }
 47 
 48 fn evaluate-sub _curr: (addr word), end: (addr word), out: (addr value-stack), top-level?: boolean {
 49   var curr/ecx: (addr word) <- copy _curr
 50   var curr-stream-storage: (stream byte 0x10)
 51   var curr-stream/edi: (addr stream byte) <- address curr-stream-storage
 52   $evaluate-sub:loop: {
 53     # safety net (should never hit)
 54     compare curr, 0
 55     break-if-=
 56     # pull next word in for parsing
 57     emit-word curr, curr-stream
 58 #?     {
 59 #?       clear-screen 0/screen
 60 #?       dump-stack out
 61 #?       var foo/eax: int <- render-word 0/screen, curr, 0/x, 0/y, 0/no-cursor
 62 #?       {
 63 #?         var key/eax: byte <- read-key 0/keyboard
 64 #?         compare key, 0
 65 #?         loop-if-=
 66 #?       }
 67 #?     }
 68     $evaluate-sub:process-word: {
 69       ### if curr-stream is an operator, perform it
 70       {
 71         var is-add?/eax: boolean <- stream-data-equal? curr-stream, "+"
 72         compare is-add?, 0/false
 73         break-if-=
 74         var _b/xmm0: float <- pop-number-from-value-stack out
 75         var b/xmm1: float <- copy _b
 76         var a/xmm0: float <- pop-number-from-value-stack out
 77         a <- add b
 78         push-number-to-value-stack out, a
 79         break $evaluate-sub:process-word
 80       }
 81       {
 82         var is-sub?/eax: boolean <- stream-data-equal? curr-stream, "-"
 83         compare is-sub?, 0/false
 84         break-if-=
 85         var _b/xmm0: float <- pop-number-from-value-stack out
 86         var b/xmm1: float <- copy _b
 87         var a/xmm0: float <- pop-number-from-value-stack out
 88         a <- subtract b
 89         push-number-to-value-stack out, a
 90         break $evaluate-sub:process-word
 91       }
 92       {
 93         var is-mul?/eax: boolean <- stream-data-equal? curr-stream, "*"
 94         compare is-mul?, 0/false
 95         break-if-=
 96         var _b/xmm0: float <- pop-number-from-value-stack out
 97         var b/xmm1: float <- copy _b
 98         var a/xmm0: float <- pop-number-from-value-stack out
 99         a <- multiply b
100         push-number-to-value-stack out, a
101         break $evaluate-sub:process-word
102       }
103       {
104         var is-div?/eax: boolean <- stream-data-equal? curr-stream, "/"
105         compare is-div?, 0/false
106         break-if-=
107         var _b/xmm0: float <- pop-number-from-value-stack out
108         var b/xmm1: float <- copy _b
109         var a/xmm0: float <- pop-number-from-value-stack out
110         a <- divide b
111         push-number-to-value-stack out, a
112         break $evaluate-sub:process-word
113       }
114       {
115         var is-sqrt?/eax: boolean <- stream-data-equal? curr-stream, "sqrt"
116         compare is-sqrt?, 0/false
117         break-if-=
118         var a/xmm0: float <- pop-number-from-value-stack out
119         a <- square-root a
120         push-number-to-value-stack out, a
121         break $evaluate-sub:process-word
122       }
123       {
124         var is-lesser?/eax: boolean <- stream-data-equal? curr-stream, "<"
125         compare is-lesser?, 0/false
126         break-if-=
127         var _b/xmm0: float <- pop-number-from-value-stack out
128         var b/xmm1: float <- copy _b
129         var a/xmm0: float <- pop-number-from-value-stack out
130         compare a, b
131         {
132           break-if-float<
133           push-boolean-to-value-stack out, 0/false
134           break $evaluate-sub:process-word
135         }
136         push-boolean-to-value-stack out, 1/true
137         break $evaluate-sub:process-word
138       }
139       {
140         var is-greater?/eax: boolean <- stream-data-equal? curr-stream, ">"
141         compare is-greater?, 0/false
142         break-if-=
143         var _b/xmm0: float <- pop-number-from-value-stack out
144         var b/xmm1: float <- copy _b
145         var a/xmm0: float <- pop-number-from-value-stack out
146         compare a, b
147         {
148           break-if-float>
149           push-boolean-to-value-stack out, 0/false
150           break $evaluate-sub:process-word
151         }
152         push-boolean-to-value-stack out, 1/true
153         break $evaluate-sub:process-word
154       }
155       {
156         var is-equal?/eax: boolean <- stream-data-equal? curr-stream, "=="  # TODO support non-numbers
157         compare is-equal?, 0/false
158         break-if-=
159         var _b/xmm0: float <- pop-number-from-value-stack out
160         var b/xmm1: float <- copy _b
161         var a/xmm0: float <- pop-number-from-value-stack out
162         compare a, b
163         {
164           break-if-=
165           push-boolean-to-value-stack out, 0/false
166           break $evaluate-sub:process-word
167         }
168         push-boolean-to-value-stack out, 1/true
169         break $evaluate-sub:process-word
170       }
171       ## control flow
172       {
173         var is-conditional?/eax: boolean <- stream-data-equal? curr-stream, "->"
174         compare is-conditional?, 0/false
175         break-if-=
176         var a/eax: boolean <- pop-boolean-from-value-stack out
177         compare a, 0/false
178         {
179           break-if-!=
180           # if a is false, skip one word
181           var next-word: (handle word)
182           var next-word-ah/eax: (addr handle word) <- address next-word
183           skip-word curr, end, next-word-ah
184           var _curr/eax: (addr word) <- lookup *next-word-ah
185           curr <- copy _curr
186         }
187         break $evaluate-sub:process-word
188       }
189       {
190         var is-group-start?/eax: boolean <- stream-data-equal? curr-stream, "{"
191         compare is-group-start?, 0/false
192         break-if-=
193         # if top-level? and this is the final word, clear the stack
194         compare top-level?, 0/false
195         break-if-= $evaluate-sub:process-word
196         compare curr, end
197         break-if-!= $evaluate-sub:process-word
198         clear-value-stack out
199         break $evaluate-sub:process-word
200       }
201       {
202         var is-group-end?/eax: boolean <- stream-data-equal? curr-stream, "}"
203         compare is-group-end?, 0/false
204         break-if-=
205         # if top-level? and this is the final word, clear the stack
206         compare top-level?, 0/false
207         break-if-= $evaluate-sub:process-word
208         compare curr, end
209         break-if-!= $evaluate-sub:process-word
210         clear-value-stack out
211         break $evaluate-sub:process-word
212       }
213       {
214         var is-break?/eax: boolean <- stream-data-equal? curr-stream, "break"
215         compare is-break?, 0/false
216         break-if-=
217         # scan ahead to containing '}'
218         var next-word: (handle word)
219         var next-word-ah/eax: (addr handle word) <- address next-word
220         skip-rest-of-group curr, end, next-word-ah
221         var _curr/eax: (addr word) <- lookup *next-word-ah
222         curr <- copy _curr
223         loop $evaluate-sub:loop
224       }
225       {
226         var is-loop?/eax: boolean <- stream-data-equal? curr-stream, "loop"
227         compare is-loop?, 0/false
228         break-if-=
229         # scan back to containing '{'
230         var open-word: (handle word)
231         var open-word-ah/edx: (addr handle word) <- address open-word
232         scan-to-start-of-group curr, end, open-word-ah
233         # scan ahead to the containing '}'; record that as next word to eval at
234         var close-word: (handle word)
235         var close-word-ah/ebx: (addr handle word) <- address close-word
236         skip-rest-of-group curr, end, close-word-ah
237         var _curr/eax: (addr word) <- lookup *close-word-ah
238         curr <- copy _curr
239         # now eval until getting there
240         # TODO: can 'curr' be after 'end' at this point?
241         var open/eax: (addr word) <- lookup *open-word-ah
242         evaluate-sub open, curr, out, 0/nested
243         loop $evaluate-sub:loop
244       }
245       ## TEMPORARY HACKS; we're trying to avoid turning this into Forth
246       {
247         var is-dup?/eax: boolean <- stream-data-equal? curr-stream, "dup"
248         compare is-dup?, 0/false
249         break-if-=
250         # read src-val from out
251         var out2/esi: (addr value-stack) <- copy out
252         var top-addr/ecx: (addr int) <- get out2, top
253         compare *top-addr, 0
254         break-if-<=
255         var data-ah/eax: (addr handle array value) <- get out2, data
256         var data/eax: (addr array value) <- lookup *data-ah
257         var top/ecx: int <- copy *top-addr
258         top <- decrement
259         var offset/edx: (offset value) <- compute-offset data, top
260         var src-val/edx: (addr value) <- index data, offset
261         # push a copy of it
262         top <- increment
263         var offset/ebx: (offset value) <- compute-offset data, top
264         var target-val/ebx: (addr value) <- index data, offset
265         copy-object src-val, target-val
266         # commit
267         var top-addr/ecx: (addr int) <- get out2, top
268         increment *top-addr
269         break $evaluate-sub:process-word
270       }
271       {
272         var is-swap?/eax: boolean <- stream-data-equal? curr-stream, "swap"
273         compare is-swap?, 0/false
274         break-if-=
275         # read top-val from out
276         var out2/esi: (addr value-stack) <- copy out
277         var top-addr/ecx: (addr int) <- get out2, top
278         compare *top-addr, 0
279         break-if-<=
280         var data-ah/eax: (addr handle array value) <- get out2, data
281         var data/eax: (addr array value) <- lookup *data-ah
282         var top/ecx: int <- copy *top-addr
283         top <- decrement
284         var offset/edx: (offset value) <- compute-offset data, top
285         var top-val/edx: (addr value) <- index data, offset
286         # read next val from out
287         top <- decrement
288         var offset/ebx: (offset value) <- compute-offset data, top
289         var pen-top-val/ebx: (addr value) <- index data, offset
290         # swap
291         var tmp: value
292         var tmp-a/eax: (addr value) <- address tmp
293         copy-object top-val, tmp-a
294         copy-object pen-top-val, top-val
295         copy-object tmp-a, pen-top-val
296         break $evaluate-sub:process-word
297       }
298       ### if the word starts with a quote and ends with a quote, turn it into a string
299       {
300         rewind-stream curr-stream
301         var start/eax: byte <- stream-first curr-stream
302         compare start, 0x22/double-quote
303         break-if-!=
304         var end/eax: byte <- stream-final curr-stream
305         compare end, 0x22/double-quote
306         break-if-!=
307         var h: (handle array byte)
308         var s/eax: (addr handle array byte) <- address h
309         unquote-stream-to-array curr-stream, s  # leak
310         push-string-to-value-stack out, *s
311         break $evaluate-sub:process-word
312       }
313       ### if the word starts with a '[' and ends with a ']', turn it into an array
314       {
315         rewind-stream curr-stream
316         var start/eax: byte <- stream-first curr-stream
317         compare start, 0x5b/open-bracket
318         break-if-!=
319         var end/eax: byte <- stream-final curr-stream
320         compare end, 0x5d/close-bracket
321         break-if-!=
322         # wastefully create a new input string to strip quotes
323         var h: (handle array value)
324         var input-ah/eax: (addr handle array byte) <- address h
325         unquote-stream-to-array curr-stream, input-ah  # leak
326         # wastefully parse input into int-array
327         # TODO: support parsing arrays of other types
328         var input/eax: (addr array byte) <- lookup *input-ah
329         var h2: (handle array int)
330         var int-array-ah/esi: (addr handle array int) <- address h2
331         parse-array-of-decimal-ints input, int-array-ah  # leak
332         var _int-array/eax: (addr array int) <- lookup *int-array-ah
333         var int-array/esi: (addr array int) <- copy _int-array
334         var len/ebx: int <- length int-array
335         # push value-array of same size as int-array
336         var h3: (handle array value)
337         var value-array-ah/eax: (addr handle array value) <- address h3
338         populate value-array-ah, len
339         push-array-to-value-stack out, *value-array-ah
340         # copy int-array into value-array
341         var _value-array/eax: (addr array value) <- lookup *value-array-ah
342         var value-array/edi: (addr array value) <- copy _value-array
343         var i/eax: int <- copy 0
344         {
345           compare i, len
346           break-if->=
347           var src-addr/ecx: (addr int) <- index int-array, i
348           var src/ecx: int <- copy *src-addr
349           var src-f/xmm0: float <- convert src
350           var dest-offset/edx: (offset value) <- compute-offset value-array, i
351           var dest-val/edx: (addr value) <- index value-array, dest-offset
352           var dest/edx: (addr float) <- get dest-val, number-data
353           copy-to *dest, src-f
354           i <- increment
355           loop
356         }
357         break $evaluate-sub:process-word
358       }
359       ### otherwise assume it's a literal number and push it (can't parse floats yet)
360       {
361         var n/eax: int <- parse-decimal-int-from-stream curr-stream
362         var n-f/xmm0: float <- convert n
363         push-number-to-value-stack out, n-f
364       }
365     }
366     # termination check
367     compare curr, end
368     break-if-=
369     # update
370     var next-word-ah/edx: (addr handle word) <- get curr, next
371     var _curr/eax: (addr word) <- lookup *next-word-ah
372     curr <- copy _curr
373     #
374     loop
375   }
376 }
377 
378 fn skip-word _curr: (addr word), end: (addr word), out: (addr handle word) {
379   var curr/eax: (addr word) <- copy _curr
380   var bracket-count/ecx: int <- copy 0
381   var result-ah/esi: (addr handle word) <- get curr, next
382   {
383     var result-val/eax: (addr word) <- lookup *result-ah
384     compare result-val, end
385     break-if-=
386     {
387       var open?/eax: boolean <- word-equal? result-val, "{"
388       compare open?, 0/false
389       break-if-=
390       bracket-count <- increment
391     }
392     {
393       var close?/eax: boolean <- word-equal? result-val, "}"
394       compare close?, 0/false
395       break-if-=
396       bracket-count <- decrement
397       compare bracket-count, 0
398       {
399         break-if->=
400         abort "'->' cannot be final word in a {} group"  # TODO: error-handling
401       }
402     }
403     compare bracket-count, 0
404     break-if-=
405     result-ah <- get result-val, next
406     loop
407   }
408   copy-object result-ah, out
409 }
410 
411 fn skip-rest-of-group _curr: (addr word), end: (addr word), out: (addr handle word) {
412   var curr/eax: (addr word) <- copy _curr
413   var bracket-count/ecx: int <- copy 0
414   var result-ah/esi: (addr handle word) <- get curr, next
415   $skip-rest-of-group:loop: {
416     var result-val/eax: (addr word) <- lookup *result-ah
417     compare result-val, end
418     break-if-=
419     {
420       var open?/eax: boolean <- word-equal? result-val, "{"
421       compare open?, 0/false
422       break-if-=
423       bracket-count <- increment
424     }
425     {
426       var close?/eax: boolean <- word-equal? result-val, "}"
427       compare close?, 0/false
428       break-if-=
429       compare bracket-count, 0
430       break-if-= $skip-rest-of-group:loop
431       bracket-count <- decrement
432     }
433     result-ah <- get result-val, next
434     loop
435   }
436   copy-object result-ah, out
437 }
438 
439 fn scan-to-start-of-group _curr: (addr word), end: (addr word), out: (addr handle word) {
440   var curr/eax: (addr word) <- copy _curr
441   var bracket-count/ecx: int <- copy 0
442   var result-ah/esi: (addr handle word) <- get curr, prev
443   $scan-to-start-of-group:loop: {
444     var result-val/eax: (addr word) <- lookup *result-ah
445     compare result-val, end
446     break-if-=
447     {
448       var open?/eax: boolean <- word-equal? result-val, "{"
449       compare open?, 0/false
450       break-if-=
451       compare bracket-count, 0
452       break-if-= $scan-to-start-of-group:loop
453       bracket-count <- increment
454     }
455     {
456       var close?/eax: boolean <- word-equal? result-val, "}"
457       compare close?, 0/false
458       break-if-=
459       bracket-count <- decrement
460     }
461     result-ah <- get result-val, prev
462     loop
463   }
464   copy-object result-ah, out
465 }
466 
467 fn test-eval-arithmetic {
468   # in
469   var in-storage: line
470   var in/esi: (addr line) <- address in-storage
471   parse-line "1 1 +", in
472   # end
473   var w-ah/eax: (addr handle word) <- get in, data
474   var end-h: (handle word)
475   var end-ah/ecx: (addr handle word) <- address end-h
476   final-word w-ah, end-ah
477   var end/eax: (addr word) <- lookup *end-ah
478   # out
479   var out-storage: value-stack
480   var out/edi: (addr value-stack) <- address out-storage
481   initialize-value-stack out, 8
482   #
483   evaluate in, end, out
484   #
485   var len/eax: int <- value-stack-length out
486   check-ints-equal len, 1, "F - test-eval-arithmetic stack size"
487   var n/xmm0: float <- pop-number-from-value-stack out
488   var n2/eax: int <- convert n
489   check-ints-equal n2, 2, "F - test-eval-arithmetic result"
490 }
491 
492 fn test-eval-string {
493   # in
494   var in-storage: line
495   var in/esi: (addr line) <- address in-storage
496   parse-line "\"abc\"", in  # TODO support spaces within strings
497   # end
498   var w-ah/eax: (addr handle word) <- get in, data
499   var end-h: (handle word)
500   var end-ah/ecx: (addr handle word) <- address end-h
501   final-word w-ah, end-ah
502   var end/eax: (addr word) <- lookup *end-ah
503   # out
504   var out-storage: value-stack
505   var out/edi: (addr value-stack) <- address out-storage
506   initialize-value-stack out, 8
507   #
508   evaluate in, end, out
509   #
510   var len/eax: int <- value-stack-length out
511   check-ints-equal len, 1, "F - test-eval-string stack size"
512   var out-data-ah/eax: (addr handle array value) <- get out, data
513   var out-data/eax: (addr array value) <- lookup *out-data-ah
514   var v/eax: (addr value) <- index out-data, 0
515   var type/ecx: (addr int) <- get v, type
516   check-ints-equal *type, 1/text, "F - test-eval-string type"
517   var text-ah/eax: (addr handle array byte) <- get v, text-data
518   var text/eax: (addr array byte) <- lookup *text-ah
519   check-strings-equal text, "abc", "F - test-eval-string result"
520 }
521 
522 fn test-eval-compare-lesser {
523   # in
524   var in-storage: line
525   var in/esi: (addr line) <- address in-storage
526   parse-line "1 2 <", in
527   # end
528   var w-ah/eax: (addr handle word) <- get in, data
529   var end-h: (handle word)
530   var end-ah/ecx: (addr handle word) <- address end-h
531   final-word w-ah, end-ah
532   var end/eax: (addr word) <- lookup *end-ah
533   # out
534   var out-storage: value-stack
535   var out/edi: (addr value-stack) <- address out-storage
536   initialize-value-stack out, 8
537   #
538   evaluate in, end, out
539   #
540   var len/eax: int <- value-stack-length out
541   check-ints-equal len, 1, "F - test-eval-compare-lesser stack size"
542   var result/eax: boolean <- pop-boolean-from-value-stack out
543   check result, "F - test-eval-compare-lesser result"
544 }
545 
546 fn test-eval-compare-greater {
547   # in
548   var in-storage: line
549   var in/esi: (addr line) <- address in-storage
550   parse-line "2 1 >", in
551   # end
552   var w-ah/eax: (addr handle word) <- get in, data
553   var end-h: (handle word)
554   var end-ah/ecx: (addr handle word) <- address end-h
555   final-word w-ah, end-ah
556   var end/eax: (addr word) <- lookup *end-ah
557   # out
558   var out-storage: value-stack
559   var out/edi: (addr value-stack) <- address out-storage
560   initialize-value-stack out, 8
561   #
562   evaluate in, end, out
563   #
564   var len/eax: int <- value-stack-length out
565   check-ints-equal len, 1, "F - test-eval-compare-greater stack size"
566   var result/eax: boolean <- pop-boolean-from-value-stack out
567   check result, "F - test-eval-compare-greater result"
568 }
569 
570 fn test-eval-compare-equal-fails {
571   # in
572   var in-storage: line
573   var in/esi: (addr line) <- address in-storage
574   parse-line "1 2 ==", in
575   # end
576   var w-ah/eax: (addr handle word) <- get in, data
577   var end-h: (handle word)
578   var end-ah/ecx: (addr handle word) <- address end-h
579   final-word w-ah, end-ah
580   var end/eax: (addr word) <- lookup *end-ah
581   # out
582   var out-storage: value-stack
583   var out/edi: (addr value-stack) <- address out-storage
584   initialize-value-stack out, 8
585   #
586   evaluate in, end, out
587   #
588   var len/eax: int <- value-stack-length out
589   check-ints-equal len, 1, "F - test-eval-compare-equal-fails stack size"
590   var result/eax: boolean <- pop-boolean-from-value-stack out
591   check-not result, "F - test-eval-compare-equal-fails result"
592 }
593 
594 fn test-eval-compare-equal {
595   # in
596   var in-storage: line
597   var in/esi: (addr line) <- address in-storage
598   parse-line "2 2 ==", in
599   # end
600   var w-ah/eax: (addr handle word) <- get in, data
601   var end-h: (handle word)
602   var end-ah/ecx: (addr handle word) <- address end-h
603   final-word w-ah, end-ah
604   var end/eax: (addr word) <- lookup *end-ah
605   # out
606   var out-storage: value-stack
607   var out/edi: (addr value-stack) <- address out-storage
608   initialize-value-stack out, 8
609   #
610   evaluate in, end, out
611   #
612   var len/eax: int <- value-stack-length out
613   check-ints-equal len, 1, "F - test-eval-compare-equal stack size"
614   var result/eax: boolean <- pop-boolean-from-value-stack out
615   check result, "F - test-eval-compare-equal result"
616 }
617 
618 fn test-eval-conditional {
619   # in
620   var in-storage: line
621   var in/esi: (addr line) <- address in-storage
622   parse-line "1 2 < -> 3", in
623   # end
624   var w-ah/eax: (addr handle word) <- get in, data
625   var end-h: (handle word)
626   var end-ah/ecx: (addr handle word) <- address end-h
627   final-word w-ah, end-ah
628   var end/eax: (addr word) <- lookup *end-ah
629   # out
630   var out-storage: value-stack
631   var out/edi: (addr value-stack) <- address out-storage
632   initialize-value-stack out, 8
633   #
634   evaluate in, end, out
635   #
636   var len/eax: int <- value-stack-length out
637   check-ints-equal len, 1, "F - test-eval-conditional stack size"
638   var n/xmm0: float <- pop-number-from-value-stack out
639   var n2/eax: int <- convert n
640   check-ints-equal n2, 3, "F - test-eval-conditional result"
641 }
642 
643 # if top of stack is false, `->` skips one word
644 fn test-eval-conditional-skipped {
645   # in
646   var in-storage: line
647   var in/esi: (addr line) <- address in-storage
648   parse-line "1 2 > -> 3", in
649   # end
650   var w-ah/eax: (addr handle word) <- get in, data
651   var end-h: (handle word)
652   var end-ah/ecx: (addr handle word) <- address end-h
653   final-word w-ah, end-ah
654   var end/eax: (addr word) <- lookup *end-ah
655   # out
656   var out-storage: value-stack
657   var out/edi: (addr value-stack) <- address out-storage
658   initialize-value-stack out, 8
659   #
660   evaluate in, end, out
661   #
662   var len/eax: int <- value-stack-length out
663   check-ints-equal len, 0, "F - test-eval-conditional-skipped stack size"
664 }
665 
666 # curlies have no effect in isolation
667 fn test-eval-group {
668   # in
669   var in-storage: line
670   var in/esi: (addr line) <- address in-storage
671   parse-line "{ 1 } 1 +", in
672   # end
673   var w-ah/eax: (addr handle word) <- get in, data
674   var end-h: (handle word)
675   var end-ah/ecx: (addr handle word) <- address end-h
676   final-word w-ah, end-ah
677   var end/eax: (addr word) <- lookup *end-ah
678   # out
679   var out-storage: value-stack
680   var out/edi: (addr value-stack) <- address out-storage
681   initialize-value-stack out, 8
682   #
683   evaluate in, end, out
684   #
685   var len/eax: int <- value-stack-length out
686   check-ints-equal len, 1, "F - test-eval-group stack size"
687   var n/xmm0: float <- pop-number-from-value-stack out
688   var n2/eax: int <- convert n
689   check-ints-equal n2, 2, "F - test-eval-group result"
690 }
691 
692 fn test-eval-group-open-at-end {
693   # in
694   var in-storage: line
695   var in/esi: (addr line) <- address in-storage
696   parse-line "1 1 + {", in
697   # end
698   var w-ah/eax: (addr handle word) <- get in, data
699   var end-h: (handle word)
700   var end-ah/ecx: (addr handle word) <- address end-h
701   final-word w-ah, end-ah
702   var end/eax: (addr word) <- lookup *end-ah
703   # out
704   var out-storage: value-stack
705   var out/edi: (addr value-stack) <- address out-storage
706   initialize-value-stack out, 8
707   #
708   evaluate in, end, out
709   #
710   var len/eax: int <- value-stack-length out
711   check-ints-equal len, 0, "F - test-eval-group-open-at-end stack size"
712 }
713 
714 fn test-eval-group-close-at-end {
715   # in
716   var in-storage: line
717   var in/esi: (addr line) <- address in-storage
718   parse-line "{ 1 1 + }", in
719   # end
720   var w-ah/eax: (addr handle word) <- get in, data
721   var end-h: (handle word)
722   var end-ah/ecx: (addr handle word) <- address end-h
723   final-word w-ah, end-ah
724   var end/eax: (addr word) <- lookup *end-ah
725   # out
726   var out-storage: value-stack
727   var out/edi: (addr value-stack) <- address out-storage
728   initialize-value-stack out, 8
729   #
730   evaluate in, end, out
731   #
732   var len/eax: int <- value-stack-length out
733   check-ints-equal len, 0, "F - test-eval-group-close-at-end stack size"
734 }
735 
736 fn test-eval-conditional-skips-group {
737   # in
738   var in-storage: line
739   var in/esi: (addr line) <- address in-storage
740   parse-line "1 2 > -> { 3 } 9", in
741   # end
742   var w-ah/eax: (addr handle word) <- get in, data
743   var end-h: (handle word)
744   var end-ah/ecx: (addr handle word) <- address end-h
745   final-word w-ah, end-ah
746   var end/eax: (addr word) <- lookup *end-ah
747   # out
748   var out-storage: value-stack
749   var out/edi: (addr value-stack) <- address out-storage
750   initialize-value-stack out, 8
751   #
752   evaluate in, end, out
753   # out contains just the final sentinel '9'
754   var len/eax: int <- value-stack-length out
755   check-ints-equal len, 1, "F - test-eval-conditional-skips-group stack size"
756 }
757 
758 fn test-eval-conditional-skips-nested-group {
759   # in
760   var in-storage: line
761   var in/esi: (addr line) <- address in-storage
762   parse-line "1 2 > -> { { 3 } 4 } 9", in
763   # end
764   var w-ah/eax: (addr handle word) <- get in, data
765   var end-h: (handle word)
766   var end-ah/ecx: (addr handle word) <- address end-h
767   final-word w-ah, end-ah
768   var end/eax: (addr word) <- lookup *end-ah
769   # out
770   var out-storage: value-stack
771   var out/edi: (addr value-stack) <- address out-storage
772   initialize-value-stack out, 8
773   #
774   evaluate in, end, out
775   # out contains just the final sentinel '9'
776   var len/eax: int <- value-stack-length out
777   check-ints-equal len, 1, "F - test-eval-conditional-skips-nested-group stack size"
778 }
779 
780 # TODO: test error-handling on:
781 #   1 2 > -> }
782 
783 # break skips to next containing `}`
784 fn test-eval-break {
785   # in
786   var in-storage: line
787   var in/esi: (addr line) <- address in-storage
788   parse-line "3 { 4 break 5 } +", in
789   # end
790   var w-ah/eax: (addr handle word) <- get in, data
791   var end-h: (handle word)
792   var end-ah/ecx: (addr handle word) <- address end-h
793   final-word w-ah, end-ah
794   var end/eax: (addr word) <- lookup *end-ah
795   # out
796   var out-storage: value-stack
797   var out/edi: (addr value-stack) <- address out-storage
798   initialize-value-stack out, 8
799   #
800   evaluate in, end, out
801   # result is 3+4, not 4+5
802   var len/eax: int <- value-stack-length out
803   check-ints-equal len, 1, "F - test-eval-break stack size"
804   var n/xmm0: float <- pop-number-from-value-stack out
805   var n2/eax: int <- convert n
806   check-ints-equal n2, 7, "F - test-eval-break result"
807 }
808 
809 fn test-eval-break-nested {
810   # in
811   var in-storage: line
812   var in/esi: (addr line) <- address in-storage
813   parse-line "3 { 4 break { 5 } 6 } +", in
814   # end
815   var w-ah/eax: (addr handle word) <- get in, data
816   var end-h: (handle word)
817   var end-ah/ecx: (addr handle word) <- address end-h
818   final-word w-ah, end-ah
819   var end/eax: (addr word) <- lookup *end-ah
820   # out
821   var out-storage: value-stack
822   var out/edi: (addr value-stack) <- address out-storage
823   initialize-value-stack out, 8
824   #
825   evaluate in, end, out
826   # result is 3+4, skipping remaining numbers
827   var len/eax: int <- value-stack-length out
828   check-ints-equal len, 1, "F - test-eval-break-nested stack size"
829   var n/xmm0: float <- pop-number-from-value-stack out
830   var n2/eax: int <- convert n
831   check-ints-equal n2, 7, "F - test-eval-break-nested result"
832 }
833 
834 #? 1 2 3 4 6 5 { <       -> break loop }
835 #?  1 2 3 4 6 5   false            2
836 #?    1 2 3 4 6   4                1
837 #?      1 2 3 4   3
838 #?        1 2 3   2
839 #?          1 2   1
840 #?            1
841 
842 #? 1 2 3 4 { 3 ==     -> return loop }
843 #?  1 2 3 4   3 false            2      => 3
844 #?    1 2 3   4 3                1
845 #?      1 2   3 2
846 #?        1   2 1
847 #?            1
848 
849 # loop skips to previous containing `{` and continues evaluating until control
850 # leaves the group
851 fn test-eval-loop {
852   # in
853   var in-storage: line
854   var in/esi: (addr line) <- address in-storage
855   parse-line "1 2 4 3 { < -> break loop } 9", in
856   # end
857   var w-ah/eax: (addr handle word) <- get in, data
858   var end-h: (handle word)
859   var end-ah/ecx: (addr handle word) <- address end-h
860   final-word w-ah, end-ah
861   var end/eax: (addr word) <- lookup *end-ah
862   # out
863   var out-storage: value-stack
864   var out/edi: (addr value-stack) <- address out-storage
865   initialize-value-stack out, 8
866   #
867   evaluate in, end, out
868   # evaluation order: 1 2 4 3 { < -> loop { < -> break 9
869   # stack contents: 9
870   var len/eax: int <- value-stack-length out
871   check-ints-equal len, 1, "F - test-eval-loop stack size"
872 }
873 
874 fn test-eval-loop-2 {
875   # in
876   var in-storage: line
877   var in/esi: (addr line) <- address in-storage
878   parse-line "1 2 4 3 { 4 == -> break loop } 9", in
879   # end
880   var w-ah/eax: (addr handle word) <- get in, data
881   var end-h: (handle word)
882   var end-ah/ecx: (addr handle word) <- address end-h
883   final-word w-ah, end-ah
884   var end/eax: (addr word) <- lookup *end-ah
885   # out
886   var out-storage: value-stack
887   var out/edi: (addr value-stack) <- address out-storage
888   initialize-value-stack out, 8
889   #
890   evaluate in, end, out
891   # evaluation order: 1 2 4 3 { 4 == -> loop { 4 == -> break 9
892   # stack contents: 1 2 9
893 #?   dump-stack out
894   var len/eax: int <- value-stack-length out
895 #?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
896   check-ints-equal len, 3, "F - test-eval-loop-2 stack size"
897 }
898 
899 fn test-eval-loop-conditional {
900   # in
901   var in-storage: line
902   var in/esi: (addr line) <- address in-storage
903   parse-line "1 2 3 { 3 == -> loop } 9", in
904   # end
905   var w-ah/eax: (addr handle word) <- get in, data
906   var end-h: (handle word)
907   var end-ah/ecx: (addr handle word) <- address end-h
908   final-word w-ah, end-ah
909   var end/eax: (addr word) <- lookup *end-ah
910   # out
911   var out-storage: value-stack
912   var out/edi: (addr value-stack) <- address out-storage
913   initialize-value-stack out, 8
914   #
915   evaluate in, end, out
916   # evaluation order: 1 2 3 { 3 == -> loop { 3 == -> 9
917   # stack contents: 1 9
918 #?   dump-stack out
919   var len/eax: int <- value-stack-length out
920 #?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
921   check-ints-equal len, 2, "F - test-eval-loop-2-conditional stack size"
922 }
923 
924 fn test-eval-loop-with-words-after-in-group {
925   # in
926   var in-storage: line
927   var in/esi: (addr line) <- address in-storage
928   parse-line "1 2 3 { 3 == -> loop 37 } 9", in
929   # end
930   var w-ah/eax: (addr handle word) <- get in, data
931   var end-h: (handle word)
932   var end-ah/ecx: (addr handle word) <- address end-h
933   final-word w-ah, end-ah
934   var end/eax: (addr word) <- lookup *end-ah
935   # out
936   var out-storage: value-stack
937   var out/edi: (addr value-stack) <- address out-storage
938   initialize-value-stack out, 8
939   #
940   evaluate in, end, out
941   # evaluation order: 1 2 3 { 3 == -> loop { 3 == -> 37 } 9
942   # stack contents: 1 37 9
943 #?   dump-stack out
944   var len/eax: int <- value-stack-length out
945 #?   draw-int32-decimal-wrapping-right-then-down-from-cursor-over-full-screen 0/screen, len, 0xc/red, 0/black
946   check-ints-equal len, 3, "F - test-eval-loop-with-words-after-in-group stack size"
947 }