add relations, and most functions

This change adds a [cond] grammar symbol corresponding to binary
relations in IF, IFELSE and WHILE conditions. It also adds support for
unary functions with parenthesis syntax. Other functions will need
specific rules depending on their operator precedence level.

Also adds the Window.save() function that implements the --save option
to save the output of the program into a bitmap file. This will be used
to perform automated unit tests.
This commit is contained in:
Lephe 2019-10-04 12:41:42 +02:00
parent 0e9a2ee944
commit a983faedeb
Signed by: Lephenixnoir
GPG Key ID: 1BBA026E13FC0495
7 changed files with 151 additions and 22 deletions

4
ast.py
View File

@ -28,7 +28,7 @@ class N(enum.IntEnum):
# Flow control
REPEAT = enum.auto()
WHILE = enum.auto()
IFELSE = enum.auto()
IF = enum.auto()
# Expressions
ADD = enum.auto()
@ -39,6 +39,8 @@ class N(enum.IntEnum):
EXP = enum.auto()
VAR = enum.auto()
CONST = enum.auto()
FUN = enum.auto()
REL = enum.auto()
#---
# AST nodes

View File

@ -126,6 +126,19 @@ class Window:
if event.type == SDL_QUIT:
break
def save(self, out):
"""Save the output into a BMP file."""
SDL_SetRenderTarget(self.r, self.t)
surface = SDL_CreateRGBSurface(0, self.width, self.height, 32,0,0,0,0)
fmt = surface.contents.format.contents.format
pixels = surface.contents.pixels
pitch = surface.contents.pitch
SDL_RenderReadPixels(self.r, None, fmt, pixels, pitch);
SDL_SaveBMP(surface, out.encode())
SDL_FreeSurface(surface)
def update(self):
"""Push window contents on-screen."""

View File

@ -99,6 +99,8 @@ def main():
ctx = Context(w)
ctx.run(ast)
if out is not None:
w.save(out)
w.wait()
return 0

View File

@ -137,7 +137,7 @@ class Context:
while self.run(n.args[0]):
self.run(n.args[1])
elif n.type == N.IFELSE:
elif n.type == N.IF:
if self.run(n.args[0]):
self.run(n.args[1])
elif n.args[2] is not None:
@ -181,6 +181,57 @@ class Context:
elif n.type == N.CONST:
return n.value
elif n.type == N.FUN:
arg = self.run(n.args[1])
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,
"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),
"RanInt": None,
"GCD": None,
"LCM": None,
"Arond": None,
}[n.args[0]]
if f is None:
raise Exception(f"Function {n.args[0]} not there yet x_x")
return f(arg)
elif n.type == N.REL:
left = self.run(n.args[0])
right = self.run(n.args[2])
return {
">": left > right,
"<": left < right,
">=": left >= right,
"<=": left <= right,
"=": left == right,
"!=": left != right
}[n.args[1]]
else:
t = N(n.type).name
raise Exception(f"Unhandled node of type {t}")
def runs(self, n):
"""Evaluate all the arguments of a node."""
return [self.run(e) for e in n.args]

View File

@ -68,6 +68,7 @@ class T(enum.IntEnum):
CONST = -1
VAR = -2
REL = -3
FUN = -4
class Token:
def __init__(self, type, *args):
@ -104,7 +105,7 @@ class LexerBase:
while not self.at_end():
x = self.lex()
print(x)
print("{:5d}: {}".format(self.position, x))
#---
# Bitcode lexer
@ -123,11 +124,13 @@ class BitcodeLexer(LexerBase):
def rewind(self):
"""Restart lexing the same input."""
self.pos = 0
self.position = 0
self.errors = 0
def lex(self):
"""Return the next token in the stream."""
h, p = self.hex, self.pos
self.position += 1
if self.at_end():
return Token(T.END)
@ -205,10 +208,44 @@ class BitcodeLexer(LexerBase):
if code == 0x49:
return Token(T.VAR, "y")
# Functions
fun = {
0x68: "Abs",
0x69: "Rnd",
0x6C: "sinh",
0x6D: "cosh",
0x6E: "tanh",
0x6F: "asinh",
0x70: "acosh",
0x71: "atanh",
# 0x72: "exp",
# 0x73: "exp10",
0x74: "sqrt",
0x75: "log",
0x76: "cbrt",
0x77: "sin",
0x78: "cos",
0x79: "tan",
0x7A: "asin",
0x7B: "acos",
0x7C: "atan",
0x7D: "log10",
0x83: "Ent",
0x84: "EntEx",
0x87: "RanInt",
# 0x88: "GCD",
# 0x89: "LCM",
0x8A: "Arond",
}
if code in fun:
return Token(T.FUN, fun[code])
print(f"[lexer] Unknown opcode {hex(code)}")
self.errors += 1
# Try to read another token
# Try to read another token after skipping one byte
self.position -= 1
return self.lex()
def at_end(self):
@ -279,11 +316,13 @@ class TextLexer(LexerBase):
"""Restart lexing the same input."""
self.code = self.base_code
self.position = 0
self.errors = 0
self.pending_param = False
def lex(self):
"""Return the next token in the stream."""
self.position += 1
c = self.code.lstrip(" \t")

View File

@ -19,15 +19,17 @@ class Parser:
PENDOWN | PENUP | SETVAR arg argvar | INPUT argvar |
MESSAGE arg | PRINT arg | STYLE style | WAIT arg |
REPEAT arg program REPEAT_END |
WHILE arg program WHILE_END |
IF arg program IF_END |
IFELSE arg program ELSE program IFELSE_END
WHILE cond PARAM program WHILE_END |
IF cond PARAM program IF_END |
IFELSE cond PARAM program ELSE program IFELSE_END
arg -> expr PARAM
argvar -> var PARAM
cond -> expr REL expr
expr -> factor | factor + expr | factor - expr
factor -> atom | atom * factor | atom / factor
atom -> const (var | "(" expr ")")* | (var | "(" expr ")")+
atom -> const (var | "(" expr ")" | FUN expr ")")* |
(var | "(" expr ")" | FUN expr ")")+
const -> (+|-)? CONST
var -> VAR
@ -71,7 +73,8 @@ class Parser:
expected = [T(t).name for t in types]
got = T(self.la.type).name
err = f"Expected one of {expected}, got {got}"
pos = self.lexer.position
err = f"Expected one of {expected}, got {got} (at token {pos})"
print("[urlparser] " + err)
raise Exception("Syntax error: " + err)
@ -145,27 +148,30 @@ class Parser:
return Node(N.REPEAT, arg, prg)
if op.type == T.WHILE:
arg = self.arg()
cond = self.cond()
self.expect(T.PARAM)
self.expect(T.EOL, optional=True)
prg = self.program()
self.expect(T.WHILE_END)
return Node(N.WHILE, arg, prg)
return Node(N.WHILE, cond, prg)
if op.type == T.IF:
arg = self.arg()
cond = self.cond()
self.expect(T.PARAM)
self.expect(T.EOL, optional=True)
prg = self.program()
self.expect(T.IF_END)
return Node(N.IF, arg, prg, None)
return Node(N.IF, cond, prg, None)
if op.type == T.IFELSE:
arg = self.arg()
cond = self.cond()
self.expect(T.PARAM)
self.expect(T.EOL, optional=True)
p1 = self.program()
self.expect(T.ELSE)
p2 = self.program()
self.expect(T.IFELSE_END)
return Node(N.IF, arg, p1, p2)
return Node(N.IF, cond, p1, p2)
# arg -> expr PARAM
def arg(self):
@ -173,6 +179,13 @@ class Parser:
self.expect(T.PARAM)
return e
# cond -> expr REL expr
def cond(self):
e1 = self.expr()
rel = self.expect(T.REL).args[0]
e2 = self.expr()
return Node(N.REL, e1, rel, e2)
# expr -> factor | factor + expr | factor - expr
def expr(self):
f = [self.factor()]
@ -216,7 +229,20 @@ class Parser:
elif lat == T.LPAR:
self.expect(T.LPAR)
factors.append(self.expr())
self.expect(T.RPAR)
# Allow a parenthesis to be removed at the end of a parameter
optional = (self.la.type == T.PARAM)
self.expect(T.RPAR, optional=optional)
elif lat == T.FUN:
name = self.expect(T.FUN).args[0]
e = self.expr()
# Allow a parenthesis to be removed at the end of a parameter
optional = (self.la.type == T.PARAM)
self.expect(T.RPAR, optional=optional)
factors.append(Node(N.FUN, name, e))
else:
break

View File

@ -34,7 +34,7 @@ def print_ast(n, lang="en", indent=0):
if lang == "en": lang = MessageEnglish
if lang == "ast": lang = MessageAST
if n.type == N.PROGRAM:
if isinstance(n, Node) and n.type == N.PROGRAM:
for arg in n.args:
print_ast(arg, lang=lang, indent=indent)
return
@ -45,14 +45,10 @@ def print_ast(n, lang="en", indent=0):
print(f"{type(n)}({n})")
return
if n.type == N.CONST:
if n.type in [N.CONST, N.VAR, N.REL]:
print(n.args[0])
return
if n.type == N.VAR:
print(f"{n.args[0]}")
return
id = n.type.name.lower()
if hasattr(lang, id):