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 0000000..16d938e Binary files /dev/null and b/tests/line-positive.png differ 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