From f1cc4ace407d96c00eb152641e9c7438cd72e8ba Mon Sep 17 00:00:00 2001 From: Lephe Date: Fri, 25 Oct 2019 13:38:10 +0200 Subject: [PATCH] update atomically (like a bomb) * Fix angle units for reverse trigonometric functions * Add a streaming option to capture the output in real-time as a series of bitmap images * Switch to Decimal due to the non-continuity of Ent() breaking assumptions about the fidelity of floats. Trigonometry functions are still computed as floats. * Fix the line rendering algorithm for edge cases and add a new test line-positive.txt that ensures basic patterns are all correct. * Use custom rules to generate text representation of decimal numbers as to better match the output of the fx-92. * Improve the test constants.txt to better evaluate this representation. The interpreter automatically checks that the representation matches the value and fails on error to avoid scratching heads. * Fix EQUAL not being treated as a relational symbol. --- doc/tokens.txt | 48 ++++++++++++++--------------- fx92.py | 23 ++++++++++---- fx92/ast.py | 7 +++-- fx92/drawing.py | 17 +++++++++-- fx92/interpreter.py | 65 +++++++++++++++++++++++----------------- fx92/lexer.py | 17 +++++++---- fx92/parser.py | 3 +- fx92/util.py | 65 ++++++++++++++++++++++++++++++++++++++++ tests/autovars.out | 8 ++--- tests/constants.out | 19 ++++++++---- tests/constants.txt | 7 +++++ tests/function-atom.out | 8 ++--- tests/functions.out | 2 +- tests/line-positive.png | Bin 0 -> 6343 bytes tests/line-positive.txt | 12 ++++++++ 15 files changed, 218 insertions(+), 83 deletions(-) create mode 100644 fx92/util.py create mode 100644 tests/line-positive.png create mode 100644 tests/line-positive.txt diff --git a/doc/tokens.txt b/doc/tokens.txt index 5ea64a8..96e3b1d 100644 --- a/doc/tokens.txt +++ b/doc/tokens.txt @@ -40,34 +40,34 @@ Hexadecimal Description Feature BitcodeLexer TextLexer 4A Pré-Rép - - - 4C θ Yes Yes Yes 60 ( Yes Yes Yes -68 Abs( Yes Yes - -69 Rnd( Yes Yes - -6C sinh( Yes Yes - -6D cosh( Yes Yes - -6E tanh( Yes Yes - -6F sinh⁻¹( Yes Yes - -70 cosh⁻¹( Yes Yes - -71 tanh⁻¹( Yes Yes - +68 Abs( Yes Yes Yes +69 Rnd( - Yes Yes +6C sinh( Yes Yes Yes +6D cosh( Yes Yes Yes +6E tanh( Yes Yes Yes +6F sinh⁻¹( Yes Yes asinh() +70 cosh⁻¹( Yes Yes acosh() +71 tanh⁻¹( Yes Yes atanh() 72 e^ Yes - - 73 10^ Yes - - -74 √( Yes Yes - -75 ln( Yes Yes - -76 ³√( Yes Yes - -77 sin( Yes Yes - -78 cos( Yes Yes - -79 tan( Yes Yes - -7A Arcsin( Yes Yes - -7B Arccos( Yes Yes - -7C Arctan( Yes Yes - -7D log( Yes Yes - +74 √( Yes Yes sqrt() +75 ln( Yes Yes log() +76 ³√( Yes Yes cbrt() +77 sin( Yes Yes Yes +78 cos( Yes Yes Yes +79 tan( Yes Yes Yes +7A Arcsin( Yes Yes asin() +7B Arccos( Yes Yes acos() +7C Arctan( Yes Yes atan() +7D log( - Yes log10() 7E Pol - - - 7F Rec - - - -83 Ent( Yes Yes - -84 EntEx( Yes Yes - -87 RanInt#( Yes Yes - -88 PGCD( - - - -89 PPCM( - - - -8A Arond( Yes Yes - +83 Ent( Yes Yes Yes +84 EntEx( Yes Yes Yes +87 RanInt#( - Yes RanInt() +88 PGCD( Yes Yes GCD() +89 PPCM( Yes Yes LCM() +8A Arond( - Yes Arond() A5 = Yes Yes Yes A6 + Yes Yes Yes A7 - Yes Yes Yes diff --git a/fx92.py b/fx92.py index 8c06154..d538a6b 100755 --- a/fx92.py +++ b/fx92.py @@ -1,5 +1,6 @@ #! /usr/bin/python3 +import os import sys import getopt @@ -23,9 +24,10 @@ Input mode (default is -s): -u Input file is a wes.casio.com URL ("https://...F908313200333500") Output options: - --quiet Do not show the SDL window - --save= Save a copy of the screen output in a bitmap file - --scale= Window scale up (default 4, max 16) + --quiet Do not show the SDL window + --save= Save a copy of the screen output in a bitmap file + --scale= Window scale up (default 4, max 16) + --stream= Capture rendering in real time, outputs a series of bitmaps """.format(sys.argv[0]).strip() def usage(exitcode=None): @@ -38,7 +40,7 @@ def main(): # Read command-line arguments try: opts, args = getopt.gnu_getopt(sys.argv[1:], "hus", - ["help", "quiet", "save=", "scale=", "debug="]) + ["help", "quiet", "save=", "scale=", "debug=", "stream="]) opts = dict(opts) if len(sys.argv) == 1 or "-h" in opts or "--help" in opts: @@ -62,6 +64,7 @@ def main(): quiet = "--quiet" in opts out = opts.get("--save", None) debug = opts.get("--debug", None) + stream = opts.get("--stream", False) scale = int(opts.get("--scale", "4")) if scale < 1: @@ -98,7 +101,17 @@ def main(): print_ast(ast, lang="ast") return 0 - with Window(width=192, height=47, scale=scale, quiet=quiet) as w: + # Create the folder for streaming capture + if stream: + try: + os.mkdir(stream) + except FileExistsError: + print("error: folder {} already exists, avoiding overwrite".format( + stream)) + return 1 + + with Window(width=192, height=47, scale=scale, quiet=quiet, + stream=stream) as w: ctx = Context(w) try: ctx.run(ast) diff --git a/fx92/ast.py b/fx92/ast.py index 358adda..2db8b98 100644 --- a/fx92/ast.py +++ b/fx92/ast.py @@ -1,6 +1,7 @@ # fx-92 Scientifique Collège+ language interpreter: AST definition import enum +from decimal import Decimal def auto_builder(): number = 0 @@ -95,17 +96,17 @@ class Node: arity = len(self.args) if self.type == N.MUL and arity == 0: - return Node(N.CONST, 1) + return Node(N.CONST, Decimal(1)) if self.type == N.MUL and arity == 1: return self.args[0] if self.type == N.MUL and self.constchildren(): - prod = 1 + prod = Decimal(1) for c in self.args: prod *= c.value return Node(N.CONST, prod) if self.type == N.ADD and arity == 0: - return Node(N.CONST, 0) + return Node(N.CONST, Decimal(0)) if self.type == N.ADD and arity == 1: return self.args[0] if self.type == N.ADD and self.constchildren(): diff --git a/fx92/drawing.py b/fx92/drawing.py index e2d79a2..904a314 100644 --- a/fx92/drawing.py +++ b/fx92/drawing.py @@ -2,6 +2,7 @@ from sdl2 import * import math +import os class InterruptException(Exception): pass @@ -14,11 +15,13 @@ class Window: BLACK = (0, 0, 0, 255) WHITE = (255, 255, 255, 255) - def __init__(self, width, height, scale, quiet=False): + def __init__(self, width, height, scale, quiet=False, stream=False): self.width = width self.height = height self.scale = scale self.quiet = quiet + self.stream = stream + self.streamid = 1 def __enter__(self): """ @@ -87,6 +90,7 @@ class Window: x = int(round(x)) y = int(round(y)) + SDL_RenderDrawPoint(self.r, x, y) def clear(self, color): @@ -112,7 +116,7 @@ class Window: self._point(x, y) if dx > dy: - cumul = dx // 2 + cumul = (dx+1) // 2 for i in range(dx): x += sx cumul += dy @@ -126,7 +130,7 @@ class Window: y += sy self._point(x, y) else: - cumul = dy // 2 + cumul = (dy+1) // 2 for i in range(dy): y += sy cumul += dx @@ -143,6 +147,8 @@ class Window: def linef(self, x, y, angle, dist, color): """Draw a line forward.""" + x, y, angle, dist = float(x), float(y), float(angle), float(dist) + cos = math.cos(angle * math.pi / 180) sin = math.sin(angle * math.pi / 180) @@ -192,6 +198,11 @@ class Window: def update(self): """Push window contents on-screen.""" + if self.stream: + name = "{:04d}.png".format(self.streamid) + self.save(os.path.join(self.stream, name)) + self.streamid += 1 + # Target the window with scaling SDL_SetRenderTarget(self.r, None) SDL_RenderSetScale(self.r, self.scale, self.scale) diff --git a/fx92/interpreter.py b/fx92/interpreter.py index 45a4a79..4a7c215 100644 --- a/fx92/interpreter.py +++ b/fx92/interpreter.py @@ -1,6 +1,8 @@ # fx-92 Scientifique Collège+ language interpreter: AST interpreter from fx92.ast import N, Node +from decimal import Decimal +from fx92.util import decfun, degree2decimal, decimal2degree, decimal_repr import math class Context: @@ -18,9 +20,9 @@ class Context: self.vars = { name: 0 for name in "MABCDEF" } self.w = window - self.x = float(0) - self.y = float(0) - self.theta = float(0) + self.x = Decimal(0) + self.y = Decimal(0) + self.theta = Decimal(0) self.pen = False # TODO: Also specify the style! @@ -52,8 +54,11 @@ class Context: self.w.linef(self.x, self.y, self.theta, dist, self.w.BLACK) self.w.update() - self.x += dist * math.cos(self.theta * math.pi / 180) - self.y += dist * math.sin(self.theta * math.pi / 180) + dx = dist * degree2decimal(math.cos)(self.theta) + dy = dist * degree2decimal(math.sin)(self.theta) + + self.x = round(self.x + dx, 9) + self.y = round(self.y + dy, 9) elif n.type == N.ROTATE: angle, = self.runs(n) @@ -96,8 +101,14 @@ class Context: raise Exception("Input statement not supported yet x_x") elif n.type == N.PRINT: - val, = self.runs(n) - print(val) + x, = self.runs(n) + s = decimal_repr(x) + print(s) + + diff = abs(Decimal(s) - x) + approx = Decimal(1e-9) * abs(x) + assert diff <= approx, \ + "Incorrect representation of {} as {}".format(x,s) elif n.type == N.STYLE: # TODO: Support style @@ -141,7 +152,7 @@ class Context: return x - y elif n.type == N.MUL: - prod = 1 + prod = Decimal(1) for x in self.runs(n): prod *= x return prod @@ -177,27 +188,27 @@ class Context: f = { "Abs": abs, "Rnd": None, - "sinh": math.sinh, - "cosh": math.cosh, - "tanh": math.tanh, - "asinh": math.asinh, - "acosh": math.acosh, - "atanh": math.atanh, - "sqrt": math.sqrt, - "log": math.log, + "sinh": decfun(math.sinh), + "cosh": decfun(math.cosh), + "tanh": decfun(math.tanh), + "asinh": decfun(math.asinh), + "acosh": decfun(math.acosh), + "atanh": decfun(math.atanh), + "sqrt": decfun(math.sqrt), + "log": decfun(math.log), "cbrt": lambda x: x ** (1/3), - "sin": math.sin, - "cos": math.cos, - "tan": math.tan, - "asin": math.asin, - "acos": math.acos, - "atan": math.atan, - "log10": math.log10, - "Ent": lambda x: float(int(x)), - "EntEx": lambda x: float(int(x)) - (x < 0), + "sin": degree2decimal(math.sin), + "cos": degree2decimal(math.cos), + "tan": degree2decimal(math.tan), + "asin": decimal2degree(math.asin), + "acos": decimal2degree(math.acos), + "atan": decimal2degree(math.atan), + "log10": None, + "Ent": lambda x: Decimal(int(x)), + "EntEx": lambda x: Decimal(int(x)) - (x < 0), "RanInt": None, - "GCD": lambda x, y: float(math.gcd(int(x), int(y))), - "LCM": lambda x, y: x * y / math.gcd(int(x), int(y)), + "GCD": lambda x, y: Decimal(math.gcd(int(x), int(y))), + "LCM": lambda x, y: x*y/Decimal(math.gcd(int(x),int(y))), "Arond": None, }[n.args[0]] diff --git a/fx92/lexer.py b/fx92/lexer.py index 2738749..02fabff 100644 --- a/fx92/lexer.py +++ b/fx92/lexer.py @@ -3,6 +3,7 @@ import math import re import enum +from decimal import Decimal #--- # Token description @@ -58,7 +59,6 @@ class T(enum.IntEnum): SEMI = 0x2C LPAR = 0x60 RPAR = 0xD0 - EQUAL = 0xA5 PLUS = 0xA6 MINUS = 0xA7 STAR = 0xA8 @@ -103,7 +103,7 @@ def str2float(integer, decimal, exponent, percent): m2 = ".0" if decimal == "." else (decimal or "") m3 = exponent or "" - f = float(m1 + m2 + m3) + f = Decimal(m1 + m2 + m3) if percent == "%": f /= 100 @@ -193,15 +193,19 @@ class BitcodeLexer(LexerBase): if code == 0xC0: code = 0xA7 + # Equal symbol + if h[p] == 0xA5: + return Token(T.REL, "=") + try: return Token(T(code)) except: pass if code == 0x21: - return Token(T.CONST, math.e) + return Token(T.CONST, Decimal(math.e), "[e]") if code == 0x22: - return Token(T.CONST, math.pi) + return Token(T.CONST, Decimal(math.pi), "[pi]") # Constants if code in range(0x30, 0x39+1) or code == 0x2E: @@ -383,6 +387,10 @@ class TextLexer(LexerBase): self.code = c[len(r):] self.pending_param = True return Token(T.REL, r) + if c[0] == "=": + self.code = c[1:] + self.pending_param = True + return Token(T.REL, "=") # Punctuation punct = { @@ -392,7 +400,6 @@ class TextLexer(LexerBase): "?": T.QUEST, "(": T.LPAR, ")": T.RPAR, - "=": T.EQUAL, "+": T.PLUS, "-": T.MINUS, "*": T.STAR, diff --git a/fx92/parser.py b/fx92/parser.py index ceb6e65..aa91cff 100644 --- a/fx92/parser.py +++ b/fx92/parser.py @@ -1,6 +1,7 @@ # fx-92 Scientifique Collège+ language interpreter: Syntactic analysis import re +from decimal import Decimal from fx92.lexer import T, Token, BitcodeLexer from fx92.ast import N, Node @@ -227,7 +228,7 @@ class Parser: self.expect(T.PLUS) elif self.la.type == T.MINUS: self.expect(T.MINUS) - factors.append(Node(N.CONST, -1)) + factors.append(Node(N.CONST, Decimal(-1))) while 1: optional = len(factors) > 0 diff --git a/fx92/util.py b/fx92/util.py new file mode 100644 index 0000000..04a6e5a --- /dev/null +++ b/fx92/util.py @@ -0,0 +1,65 @@ +import math +from decimal import Decimal + +def decfun(f): + def g(x): + return Decimal(f(float(x))) + return g + +def degree2decimal(f): + def g(x): + return Decimal(f(float(x) * math.pi / 180)) + return g + +def decimal2degree(f): + def g(x): + return Decimal(f(float(x)) * 180 / math.pi) + return g + +def decimal_repr(x): + # Remove trailing 0's + x = x.normalize() + + if x == 0: + return "0" + + # Keep only 10 digits for printing + i, e = int(x._int), x._exp + digits = len(str(i)) + overflow = digits - 10 + + if overflow > 0: + i = (i + (10 ** overflow) // 2) // (10 ** overflow) + e += overflow + digits = 10 + + # Handle sign + sign = "-" if x._sign else "" + + # Exponent in engineering notation (true exponent of value) + true_exp = e + digits - 1 + + # For integers up to 10 digits: print normally + if e >= 0 and true_exp <= 9: + return sign + str(i * 10 ** e) + + # For larger integers, use scientific notation + elif e >= 0: + dg = str(i) + dg = dg[0] + "." + dg[1:] + "e" + str(true_exp) + return sign + dg + + # Otherwise, if there are less than 10 digits overall: print normally + elif true_exp >= -9: + supplement = max(-true_exp, 0) + dg = "0" * supplement + str(i) + true_exp += supplement + + dg = dg[:true_exp+1] + "." + dg[true_exp+1:] + return sign + dg + + # For very small numbers, use scientific notation again + else: + dg = str(i) + dg = dg[0] + "." + dg[1:] + "e" + str(true_exp) + return sign + dg diff --git a/tests/autovars.out b/tests/autovars.out index 2aefd82..0d3bfce 100644 --- a/tests/autovars.out +++ b/tests/autovars.out @@ -1,4 +1,4 @@ -10.0 -0.0 -90.0 -160.0 +10 +0 +90 +160 diff --git a/tests/constants.out b/tests/constants.out index 5678eb2..38a0c86 100644 --- a/tests/constants.out +++ b/tests/constants.out @@ -1,11 +1,18 @@ -173.0 --374.0 +173 +838168876 +8381688765 +8.381688766e10 +8.381688766e11 +1230000000 +1.23e10 +-374 37.947 -913390.0 +913390 9.454 0.034 -1445.0 -0.0 -100000.0 +1445 +0 +100000 0.38 -0.02434 +1.3597e-16 diff --git a/tests/constants.txt b/tests/constants.txt index b3dd1c4..8218472 100644 --- a/tests/constants.txt +++ b/tests/constants.txt @@ -1,4 +1,10 @@ print 173 +print 838168876 +print 8381688765 +print 83816887657 +print 838168876572 +print 123e7 +print 123e8 print -374 print 37.947 print 913.39e3 @@ -9,4 +15,5 @@ print . print 1.e5 print 38% print -24.34e-1% +print 135.97e-18 diff --git a/tests/function-atom.out b/tests/function-atom.out index 4672473..192eecc 100644 --- a/tests/function-atom.out +++ b/tests/function-atom.out @@ -1,4 +1,4 @@ --2.0 -1.0 -1.0 --1.0 +-2 +1 +1 +-1 diff --git a/tests/functions.out b/tests/functions.out index 03b6389..98d9bcb 100644 --- a/tests/functions.out +++ b/tests/functions.out @@ -1 +1 @@ -17.0 +17 diff --git a/tests/line-positive.png b/tests/line-positive.png new file mode 100644 index 0000000000000000000000000000000000000000..16d938e7481b949475ed0c424e623159fab7c024 GIT binary patch literal 6343 zcmV;&7&zyNP)I$000*1dQ@0+Qek%> zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3*vk{!8mh5us}UILH+62NlsjPMS;e1E5!vWLxktANZrFVqe9zy9-u|KL}(+C!&;9LhZ`|ge8{7X_ zQJks7zx~z!`u*PN@7&Ek~7e>U}Vi}3q0yl_#a{JqWjYW%MJfBU`$Up>yaxz&j~ zF1~ge$x@Clm2^Uy>7|-Uk0*00Q+4bHfJF0pF*Yqs(?0GZcSzW?+%wWF@tHzkF8R~C~p zEr_0SO`c*iYblj%od!)-Zn@Z^1F2R!EYeEOPH#GM#Jzb>OW$j)+Rx8{3jL|sbE7vS zPnuOqWqw-e%+Rd4mRhUVR(lO(>;KTT^s8(3bxN;mf4as`ThoQU zOC<43RnO>{l|;vzI>11idKP;s#nd_VEDi$|)mUUzZ%&FcB&UVtCaW*~>D^zQ`?vcR zsP(_%%JbJbiCE{ZrJ@Kf4RnIYoE6$ z#4&cXmfyRZ?;Eep$JSr(c5YnjMecLfId%8t-PrcI2#Rri!=i5(WKH9p=eeGq#&v9txjhTJrR1fbP`Q>`NuqSzQ|G9rLI^Rv~+h`%*Y;z1>b3^&<31oGKr6A zuX(PsX&d(*qkHZK!qL9mLy|t7!k>2)8e^H=^~uoQqIM)+4dm=`W=5wFnwpd2tjrp|Bq|+nn7>zA?rcy1w$m;6^dQ*JON>+apaMT@|sC-;@ zVN}}2qsTT(d>6P0UQQAFJEh9@REoS{KYs;Cwu1BmpEed5g%R?Jgl zXQjDOs6J-~yt&lL4cRS=VtS#?ckoH?F!0PtybHO7q8T<+1X|1$DiROCKh7H}!{Ce3 z9&jE3TBMKczJEUK@_*o`=LuyW%4WM-I2R3I)aGEq<1Q0Gbr`G0eBg~Yp#R5wmbF2h z6~K=u85Fp4x+Q~x+tG~h;O`Cko^JIhRf1(#qKir6bT5~HSrDqaN~Jx_ajU}I1(EFz z25>WTp^tegFCIO4 zAitVTH)_rDeW15ld4|ArU}6UfV*;T>fO>~)CZZH~|GrR>r7lr(t|&Oe{CcT$l8Pgh z(0G#=+J4|4qHOi4!-&!ZU8o@Xnu53hD5HiMY>XQ1^)`=c(`Q2{n3yWJ)tAxHirH>W ze}h&owyWIjj8zO5?SM0rQ=JVZ_)X9hTS5M40YJF#O%Thx?XlKj+f~C+PQLjqmWOVp z9-#BMume~w;fNwJIxE_K`8wB-Q0}cH$OGO^iFv*?B6UjWCQF|21qka zDg2|lebQ%V)F4%c$%WdAgG5}&IHF!El#i}EQa)z~dXqt5Nm!N6fSKvSOlB0j?l`?( z?YvFYXy69;liqZuJu;&Vt+Wz|lx^4nun?6Jss?gyj8^kfq?)t6HP1*3B0|}LCLRT2 zbMky>M(ThnrrB}+z9F&7@WK#`&be(vU*r_*Geyp@%F`Dr`gp~<#RvR39@K=uq1e#U z3ryT&o2#KbbuBt36}WRnDooUQi;=dQA_Tf$&U?YE2ld2OfW_Q)LG$f~xo+ZCE6S@m zOx7WH-igH!E*^qnNW;S+R`~jRF`Z|a?Ik(%NUt$dfsSJM@|nM^2yG%@IWo@r+QLu( zP3Vqk2Q+a1=z9(N3+&*;YM2dHC#3~`bIHlTg8YUm)GjCn)meyX#fBCp=F&4ypXx$j zScx$m5@(YjV26zwfe0*95ivop7*QrUF(LQW; zLa-gvN8K4qq~C2$>@4m#g7VW+&H?2?wbpspjk;d#@>ih0LuMGK%jnOkFb^44!-Woj zf)!*63=@yEVf{LmyfM@m58j2qu%L>8fc{VjFtd)lKNv*}<&h41#yl{Uc9Ht_ofjw^ zBR6Pie1dHCCG+>$MlOS#p%0T0#(q&yxD;E&M8RC*R}35i!kn`AAXkN=Fqh;Sx}$W- z#>p-P6`N?D?ucH#?6X5_SW4z*73LSMvvWl2I?_Oyo>A`BNc*~spM+m)(rz;5D!el) zcM$NJAj-b*i82YmQ`|#XA!eZ%Bxg-YDgzB*(0I7yV)XG84d73l zQB4ISn)=&d35fKqn0L7m{KpHhsvsSt`b-R*k}vtkNIMV)_L{Ka=MgE8Ea19-P>3K=9B99E)M##3$QPX_$2b%7P1# z;Uqxb1L~Ks`egQ&I!qU)dJJfoBZ^z;uk6e3V=(L-<$+yDdnDA3?5N61z*)FS9KxPk zWVN@Z5wba9mzUtcqa=tJfd!a>N?~oO4m3|aF~j>wt?&U|!i$%}*Cje=4W^SF4^URd ztXB#_LQKrKSIkTmc!}%?;6p57lLEV7T#3C8N`&~3U~b7cqVKZ?)&(RsX=y=W?Dq78 z(RTs_E5aGali@}5;b51RB~lJC30n(x@n@1bgu?> z&pk53(+o#fUeHn#!(8y*1VRieG71AGr7}#{m4uPKzheoMr`B0E+hla`gNNw4Wd2f! zG7JnQ{u#)Lo9I63zA^L!1zm?cfUSk#GSJotDm{}>CK)l}uQCkT1M|&VeM`Wgdt+Jj zlZ3Uk7X{*6bAc?R^P6&_T*n!j{J?U)jnq%EMtB$dNgT@{o``yMtbwsQd;{I|iqV2v zpv>Jdht!MSU1Har_Yli))>%$7Ft0ewMZi*iuCxqsy8EwYK^JE?~He}#0 zr8y0zl^M!R|0)4_yQl}~nP?qa#N|L?1vZ89Usk_>Ap8bH>?0)OR9Vd?VnTrx!}Xo9 zVNBJroMkk?2Dt#h=pheDGB@DkB_m{%+h^Y1AqgcYIAs{+HG;i0Z` z7AuwHMaw4wZ_s(i13=Di;zyHQMQefZGDZLBGxo=~BEM~P8kg8*!d>NRsNO?Cm6!{R zo8QCNX1M#v0Nv<;k-x;9t+)ZH(|)#CCGVSCpK^%%*h6TaDJ0kc$T_ejk-Un#Y^ps9 z_cR?z_r z+juaF?pG>Cb?49ib~j_dB2kT{VX6Wdt~;|Z6(mqqCSPYf_&6hODQ^)FkHT<_czN){ zWg+r~lZc;4HYU(USdZumAyr8%+RQ+e z_=4&~u%nL%!1Qse$mu5K4zypv#N%hrxDn8@D>KL7Xsl$ptyjgr4xPUQIT&P%aRf)Wyq~GAaClmiD zVnT+??i0bL<+zR5;1VXE|M(vUl5#+eO(&SJ4n7^*8cA-vh1y|4m;Xd1U`zUo_}0X& z2a`I%4Q6R@|FGr3u}hhrnrIjiWo&`I$MrDm z(6D#%fJSag?;!&Eyll*-_v!FO81|Jpq7#-$7(HK~21$p2|*LN_DT{;xX z$*uY60T!J_q_L|DI)_e?cPUO#yo&nCql^fW(!T&Ct;Lj(xDXU-s7m6-w>I0bg-A-l zC;_`oLDvbz<@GX|NXG}2Acw_<8_A)GnlZnm=1AvYI1-3Gd?ucs_HxfhEbLM7Ifd`g zOICd!r?D5$h|GacHe#ocK@X|#DgWZ*n2tPTq)8LDFhnoPD>F0UkclqLaCB_I{j8uU zjy=)gcQ&SP=xk5&+(~E1_Z;B!Mra=i#YrXPyu={pAr$GBmH7L`MiMXUa1+m97q2*& zU0>H>%|&}1@B6CbL@vo$V?-1MfjVF~f>JakgVvuOw-U&UTH6V5g=`6q5u+*}^vxQ+ z3%OnL3|N;Sj=snjsiTilpGKJpA@?v)rjt_16)tk>IRF7k&JkY04RFuCQRWo+7WO-O zO2@`r$p=t)*E!wIuphn>t*J?wHEi{HUi zObAe?4t}2oJ%fdF4w}z!ub3DfoTS{K4yDKfft2V100W0d1@l$s889dy8>c{zcnbJH zQ4rQBa-~U`4qLv^TRb7)^AeCw){nlUzhp^93r{4u0G(ICRtHO%L^QN6J>Z+EpiX>Y zzqJ|)-!V%h$e*VbPW1V@K->ic&_*B86xz|(g78I?81;G(k0wWg7e^lvA$WKZYV?D# zdQtG&L{|(Q0Ta`MJe`91$6su!1Eu0E@xF1q{qb@||9<*IVh+5Ve2Rs#&-OZh!9}Kf z_Z0b}$cO$Ug3@Or9xtdc*h&}Wr>v4$N5=!Vn-j(+SBYv_2-T%QuCs5b5q8nx6C4F; z>yUmBX0=}LOkWfyw&^ts@u!{3w}LA>BWmHrxXg;zlZkR!AM zYs5MVhzxpOz`U@I$7UJ4hnb-Fj!-I;Vd0m2MijRBN0J(#dpcK(`1FIpAZ#$%Fx`s` zpd|^B(qfH?>GSl+gLN@fAOkm)lzVsuF@pH%g+5aCv;YqpZ~{gZ%y)Rq>e2H?0A4Dw z1G`+e)TirsZ5(VPz~M!_7{sZGC5mG0Pyp6?R7iwSv!ns2wB7pTsq& zt{Yx-+blDn2?KVni8r8j2~U}m8?A#Z#~u(E?sB3A$Q<)jCFWFaY^6Uar|9>60XN)q zkPlPfCim*;i@^wpa*t;9>Uauj9n%4ojMeTWKwJ&hALC;b;?6+4JTwFC^n+W8M~7^9 zHkdksT0Ox4yLe18O>B&CVnU8i4hz|3+;hnp}W+00*xC@kU@vRobiM40SR)#1kD8vG9lMfj5{Vj3qX; z=+hrqH9dibv?Psap=0@7hc~X{(Sz9~>w>PVkEr0xZQ@8#bP!C+-jh@aw~h$eW_745 z48NCk`>#{%@O~d|NQz?!b=FWeeNcnwk@dN-fQchOfgXVzV_C*yPCFYVKww+fm+ruI zy5D&Lj)cI+up=E>1Fpa8b4g)J>6Pcle6P#E;-1G{b87`R61npG9#9hE4mRB6T!eH3EFwnyvw zU2k3OtN8DFr}F_%{aZS5pSW-kw|a<+F$1uKXM(ej51F6V8vcv!BhOLKdHjjbtAbtE zXT2yzZtV5~elQlu+6@Q68!*k}wqBI!wH6X|C95}mzC!fy&sQ3>RLBzwZ;U%{)T3O& zgp}1@qN}un8RQr5l`k-=b8L96KHhs6CN@f{u>z&`Pvs>Yl#2=010qNS#tmY zE+YT{E+YYWr9XB6000McNliru;|drFEEU)!5SIV|0dPq~K~#9!?V8&X!XOMpjl=){ za((bJj=dOYLK8UWdCMrQmEDOf%W?x~<<1rW27)jK5)u8wh*OJ26tT2@`SR9%#q`2@flno2K_0h{&Bq*^uOk@jwa#3co zqgjg&25x@D$D=vl;=e;YAF*cK=P6bW25o#nVm$B{1{er(@&IjCL}&Cw-cJAk002ov JPDHLkV1gDmHZ=eM literal 0 HcmV?d00001 diff --git a/tests/line-positive.txt b/tests/line-positive.txt new file mode 100644 index 0000000..3835185 --- /dev/null +++ b/tests/line-positive.txt @@ -0,0 +1,12 @@ +setvar 0, A +while A <= 5 + setvar 0, B + while B <= 12 + penup + goto 14*B-90, 7*A-20 + pendown + goto 15*B-90, 8*A-20 + setvar B+1, B + while_end + setvar A+1, A +while_end