476 lines
12 KiB
Python
476 lines
12 KiB
Python
import sys
|
|
from pprint import pprint
|
|
import lex
|
|
from lex import TT
|
|
from dataclasses import dataclass
|
|
|
|
class ParseException(Exception):
|
|
def __init__(self, message, token):
|
|
super().__init__(message)
|
|
self.token = token
|
|
|
|
def get_lexeme(v):
|
|
if type(v) is list:
|
|
return list(get_lexeme(i) for i in v)
|
|
elif type(v) is tuple:
|
|
return tuple(get_lexeme(i) for i in v)
|
|
elif type(v) is lex.Token:
|
|
return v.lexeme
|
|
else:
|
|
return v
|
|
|
|
def lexeme_repr(self):
|
|
kws = [f"{key}={get_lexeme(value)!r}" for key, value in self.__dict__.items()]
|
|
return "{}({})".format(type(self).__name__, ", ".join(kws))
|
|
|
|
@dataclass
|
|
class FunctionCall:
|
|
name: lex.Token
|
|
args: list[lex.Token]
|
|
kwargs: list[tuple[lex.Token, lex.Token]]
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Image:
|
|
name: list[lex.Token]
|
|
path: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Define:
|
|
name: list[lex.Token]
|
|
value: 'Expression'
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Label:
|
|
name: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Play:
|
|
channel: lex.Token
|
|
path: lex.Token
|
|
fadeout: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Scene:
|
|
name: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class With:
|
|
function_call: FunctionCall
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Say:
|
|
speaker: lex.Token
|
|
text: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Voice:
|
|
path: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Show:
|
|
what: lex.Token
|
|
transform: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Menu:
|
|
entries: tuple[lex.Token, list['Statement']]
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Jump:
|
|
target: lex.Token
|
|
|
|
__repr__ = lexeme_repr
|
|
|
|
@dataclass
|
|
class Return:
|
|
pass
|
|
|
|
def parse_lhs(tokens, index):
|
|
identifier = tokens[index]
|
|
if identifier.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", identifier)
|
|
lhs = [identifier]
|
|
index += 1
|
|
while tokens[index].type == TT.DOT:
|
|
identifier = tokens[index + 1]
|
|
if identifier.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", identifier)
|
|
lhs.append(identifier)
|
|
index += 2
|
|
return index, lhs
|
|
|
|
def parse_image(tokens, index):
|
|
index, lhs = parse_lhs(tokens, index)
|
|
equal = tokens[index + 0]
|
|
if equal.type != TT.EQUAL:
|
|
raise ParseException("expected equal", equal)
|
|
string = tokens[index + 1]
|
|
if string.type != TT.STRING:
|
|
raise ParseException("expected string", string)
|
|
|
|
image = Image(
|
|
name = lhs,
|
|
path = string,
|
|
)
|
|
return index + 2, image
|
|
|
|
def parse_function_call(tokens, index):
|
|
name = tokens[index + 0]
|
|
if name.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", name)
|
|
lparen = tokens[index + 1]
|
|
if lparen.type != TT.LPAREN:
|
|
raise ParseException("expected lparen", lparen)
|
|
index += 2
|
|
|
|
# args
|
|
args = []
|
|
while tokens[index].type != TT.RPAREN:
|
|
token = tokens[index]
|
|
if token.type == TT.STRING:
|
|
args.append(token)
|
|
elif token.type == TT.NUMBER:
|
|
args.append(token)
|
|
elif token.type == TT.NEWLINE:
|
|
pass
|
|
else:
|
|
break
|
|
index += 1
|
|
if tokens[index].type != TT.COMMA:
|
|
break
|
|
index += 1
|
|
|
|
# kwargs
|
|
kwargs = []
|
|
while tokens[index].type != TT.RPAREN:
|
|
identifier = tokens[index + 0]
|
|
if identifier.type != TT.IDENTIFIER:
|
|
raise ParseException("expected function call kwargs identifier", identifier)
|
|
equal = tokens[index + 1]
|
|
if equal.type != TT.EQUAL:
|
|
raise ParseException("expected function call kwargs equal", equal)
|
|
string = tokens[index + 2]
|
|
if string.type != TT.STRING:
|
|
raise ParseException("expected function call kwargs string", string)
|
|
kwargs.append((identifier, string))
|
|
index += 3
|
|
if tokens[index].type != TT.COMMA:
|
|
break
|
|
|
|
rparen = tokens[index]
|
|
if rparen.type != TT.RPAREN:
|
|
raise ParseException("expected rparen", rparen)
|
|
index += 1
|
|
|
|
function_call = FunctionCall(
|
|
name = name,
|
|
args = args,
|
|
kwargs = kwargs
|
|
)
|
|
return index, function_call
|
|
|
|
def parse_rhs(tokens, index):
|
|
token = tokens[index]
|
|
peek = tokens[index+1]
|
|
if token.type == TT.NUMBER:
|
|
return index + 1, token
|
|
elif token.type == TT.IDENTIFIER and peek.type == TT.LPAREN:
|
|
return parse_function_call(tokens, index)
|
|
else:
|
|
raise ParseException("expected rhs expression", token)
|
|
|
|
def parse_define(tokens, index):
|
|
index, lhs = parse_lhs(tokens, index)
|
|
equal = tokens[index + 0]
|
|
if equal.type != TT.EQUAL:
|
|
raise ParseException("expected equal", equal)
|
|
index, rhs = parse_rhs(tokens, index + 1)
|
|
define = Define(
|
|
name = lhs,
|
|
value = rhs
|
|
)
|
|
return index, define
|
|
|
|
def parse_label(tokens, index):
|
|
name = tokens[index + 0]
|
|
if name.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", name)
|
|
|
|
colon = tokens[index + 1]
|
|
if colon.type != TT.COLON:
|
|
raise ParseException("expected colon", colon)
|
|
|
|
label = Label(
|
|
name = name
|
|
)
|
|
|
|
return index + 2, label
|
|
|
|
def parse_play(tokens, index):
|
|
channel = tokens[index + 0]
|
|
if channel.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", channel)
|
|
|
|
path = tokens[index + 1]
|
|
if path.type != TT.STRING:
|
|
raise ParseException("expected string", path)
|
|
|
|
index += 2
|
|
token = tokens[index]
|
|
fadeout = None
|
|
if token.type == TT.FADEOUT:
|
|
fadeout = tokens[index + 1]
|
|
if fadeout.type != TT.NUMBER:
|
|
raise ParseException("expected number", fadeout)
|
|
index += 2
|
|
|
|
play = Play(
|
|
channel = channel,
|
|
path = path,
|
|
fadeout = fadeout
|
|
)
|
|
return index, play
|
|
|
|
def parse_scene(tokens, index):
|
|
name = tokens[index + 0]
|
|
if name.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", name)
|
|
|
|
scene = Scene(
|
|
name = name,
|
|
)
|
|
return index + 1, scene
|
|
|
|
def parse_with(tokens, index):
|
|
index, function_call = parse_function_call(tokens, index)
|
|
|
|
_with = With(
|
|
function_call = function_call
|
|
)
|
|
|
|
return index, _with
|
|
|
|
def parse_say(tokens, index):
|
|
speaker = tokens[index + 0]
|
|
if speaker.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", name)
|
|
|
|
text = tokens[index + 1]
|
|
if text.type != TT.STRING:
|
|
raise ParseException("expected string", text)
|
|
|
|
say = Say(
|
|
speaker = speaker,
|
|
text = text
|
|
)
|
|
|
|
return index + 2, say
|
|
|
|
def parse_voice(tokens, index):
|
|
path = tokens[index]
|
|
if path.type != TT.STRING:
|
|
raise ParseException("expected string", path)
|
|
|
|
voice = Voice(
|
|
path = path,
|
|
)
|
|
return index + 1, voice
|
|
|
|
def parse_show(tokens, index):
|
|
what = tokens[index + 0]
|
|
if what.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", path)
|
|
|
|
at = tokens[index + 1]
|
|
if at.type != TT.AT:
|
|
raise ParseException("expected at", at)
|
|
|
|
transform = tokens[index + 2]
|
|
if transform.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", transform)
|
|
|
|
show = Show(
|
|
what = what,
|
|
transform = transform
|
|
)
|
|
return index + 3, show
|
|
|
|
def parse_menu(tokens, index):
|
|
menu = tokens[index + 0]
|
|
if menu.type != TT.MENU:
|
|
raise ParseException("expected menu", menu)
|
|
|
|
colon = tokens[index + 1]
|
|
if colon.type != TT.COLON:
|
|
raise ParseException("expected colon", colon)
|
|
|
|
index = index + 2
|
|
menu_entries = []
|
|
menu_entry_string = None
|
|
menu_entry_statements = None
|
|
while index < len(tokens):
|
|
token = tokens[index+0]
|
|
if token.type == TT.NEWLINE:
|
|
index += 1
|
|
continue
|
|
peek = tokens[index+1]
|
|
|
|
if token.position.column < menu.position.column:
|
|
raise ParseException("invalid block dedent", token)
|
|
if token.position.column == menu.position.column:
|
|
break
|
|
|
|
if token.type == TT.STRING:
|
|
if peek.type != TT.COLON:
|
|
raise ParseException("expected colon", peek)
|
|
if menu_entry_string is not None:
|
|
menu_entries.append((menu_entry_string, menu_entry_statements))
|
|
menu_entry_string = token
|
|
menu_entry_statements = []
|
|
index += 2
|
|
else:
|
|
if menu_entry_statements is None:
|
|
raise ParseException("expected menu option", token)
|
|
|
|
index, ast = parse_one(tokens, index)
|
|
if ast is not None:
|
|
menu_entry_statements.append(ast)
|
|
|
|
if menu_entry_string is not None:
|
|
menu_entries.append((menu_entry_string, menu_entry_statements))
|
|
|
|
menu = Menu(
|
|
entries = menu_entries,
|
|
)
|
|
|
|
return index, menu
|
|
|
|
def parse_jump(tokens, index):
|
|
target = tokens[index + 0]
|
|
if target.type != TT.IDENTIFIER:
|
|
raise ParseException("expected identifier", target)
|
|
|
|
jump = Jump(
|
|
target = target,
|
|
)
|
|
return index + 1, jump
|
|
|
|
def parse_init(tokens, index):
|
|
init = tokens[index + 0]
|
|
if init.type != TT.INIT:
|
|
raise ParseException("expected init", init)
|
|
|
|
colon = tokens[index + 1]
|
|
if colon.type != TT.COLON:
|
|
raise ParseException("expected identifier", colon)
|
|
|
|
index += 2
|
|
|
|
# skip all tokens inside block
|
|
while index < len(tokens):
|
|
token = tokens[index]
|
|
if token.type == TT.NEWLINE:
|
|
index += 1
|
|
continue
|
|
|
|
if token.position.column < init.position.column:
|
|
raise ParseException("invalid block dedent", token)
|
|
if token.position.column == init.position.column:
|
|
break
|
|
index += 1
|
|
|
|
return index, None
|
|
|
|
def parse_one(tokens, index):
|
|
token = tokens[index]
|
|
if token.type == TT.NEWLINE:
|
|
index = index + 1
|
|
if index < len(tokens):
|
|
return parse_one(tokens, index)
|
|
else:
|
|
return index, None
|
|
elif token.type == TT.IMAGE:
|
|
index, ast = parse_image(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.DEFINE:
|
|
index, ast = parse_define(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.LABEL:
|
|
index, ast = parse_label(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.PLAY:
|
|
index, ast = parse_play(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.SCENE:
|
|
index, ast = parse_scene(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.WITH:
|
|
index, ast = parse_with(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.IDENTIFIER:
|
|
index, ast = parse_say(tokens, index)
|
|
return index, ast
|
|
elif token.type == TT.VOICE:
|
|
index, ast = parse_voice(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.SHOW:
|
|
index, ast = parse_show(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.MENU:
|
|
index, ast = parse_menu(tokens, index)
|
|
return index, ast
|
|
elif token.type == TT.JUMP:
|
|
index, ast = parse_jump(tokens, index + 1)
|
|
return index, ast
|
|
elif token.type == TT.RETURN:
|
|
return index + 1, Return()
|
|
elif token.type == TT.INIT:
|
|
index, ast = parse_init(tokens, index)
|
|
return index, ast
|
|
else:
|
|
raise ParseException("unexpected token", token)
|
|
|
|
def parse_all(tokens):
|
|
index = 0
|
|
while index < len(tokens):
|
|
index, ast = parse_one(tokens, index)
|
|
if ast is not None:
|
|
yield ast
|
|
|
|
def main():
|
|
with open(sys.argv[1], 'rb') as f:
|
|
mem = memoryview(f.read())
|
|
|
|
tokens = list(lex.tokenize(mem))
|
|
try:
|
|
ast = parse_all(tokens)
|
|
for t in ast:
|
|
pprint(t)
|
|
except ParseException as e:
|
|
print(e, e.token)
|
|
raise
|
|
|
|
if __name__ == "__main__":
|
|
main()
|