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.
This commit is contained in:
Lephe 2019-10-25 13:38:10 +02:00
parent c52e7ccaa3
commit f1cc4ace40
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
15 changed files with 218 additions and 83 deletions

View File

@ -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

23
fx92.py
View File

@ -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=<file> Save a copy of the screen output in a bitmap file
--scale=<n> Window scale up (default 4, max 16)
--quiet Do not show the SDL window
--save=<file> Save a copy of the screen output in a bitmap file
--scale=<n> Window scale up (default 4, max 16)
--stream=<dir> 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)

View File

@ -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():

View File

@ -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)

View File

@ -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]]

View File

@ -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,

View File

@ -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

65
fx92/util.py Normal file
View File

@ -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

View File

@ -1,4 +1,4 @@
10.0
0.0
90.0
160.0
10
0
90
160

View File

@ -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

View File

@ -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

View File

@ -1,4 +1,4 @@
-2.0
1.0
1.0
-1.0
-2
1
1
-1

View File

@ -1 +1 @@
17.0
17

BIN
tests/line-positive.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

12
tests/line-positive.txt Normal file
View File

@ -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