""" SPI - Simple Pascal Interpreter """ ############################################################################### # # # LEXER # # # ############################################################################### # Token types # # EOF (end-of-file) token is used to indicate that # there is no more input left for lexical analysis from iamai import Plugin from HydroRoll.utils import HydroDice INTEGER, PLUS, MINUS, MUL, DIV, LPAREN, RPAREN, EOF = ( "INTEGER", "PLUS", "MINUS", "MUL", "DIV", "(", ")", "EOF", ) DICE = "DICE" class Token(object): """A single token from the lexer.""" def __init__(self, _type, _value): self.type = _type self.value = _value def __str__(self): """String representation of the class instance. Examples: Token(INTEGER, 3) Token(PLUS, '+') Token(MUL, '*') """ return f"Token({self.type}, {repr(self.value)})" def __repr__(self): return self.__str__() class Lexer(object): """A lexer for the Psi language.""" def __init__(self, text): # client string input, e.g. "4 + 2 * 3 - 6 / 2" self.text = text # self.pos is an index into self.text self.pos = 0 self.current_char = self.text[self.pos] def error(self): """Raise an exception at the current position.""" raise ValueError("Invalid character") def advance(self): """Advance the `pos` pointer and set the `current_char` variable.""" self.pos += 1 if self.pos > len(self.text) - 1: self.current_char = None # Indicates end of input else: self.current_char = self.text[self.pos] def skip_whitespace(self): while self.current_char is not None and self.current_char.isspace(): self.advance() def integer(self): """Return a (multidigit) integer consumed from the input.""" result = "" while self.current_char is not None and self.current_char.isdigit(): result += self.current_char self.advance() return int(result) def get_next_token(self): """Lexical analyzer (also known as scanner or tokenizer)""" while self.current_char is not None: if self.current_char.isspace(): self.skip_whitespace() continue token_type = { "+": PLUS, "-": MINUS, "d": DICE, "*": MUL, "/": DIV, "(": LPAREN, ")": RPAREN, }.get(self.current_char) if token_type: self.advance() return Token(token_type, self.current_char) if self.current_char.isdigit(): return Token(INTEGER, self.integer()) self.error() return Token(EOF, None) ############################################################################### # # # PARSER # # # ############################################################################### class AST(object): pass class BinOp(AST): def __init__(self, left, op, right): self.left = left self.token = self.op = op self.right = right class Num(AST): def __init__(self, token): self.token = token self.value = token.value class UnaryOp(AST): def __init__(self, op, expr): self.token = self.op = op self.expr = expr class Parser(object): def __init__(self, lexer): self.lexer = lexer # set current token to the first token taken from the input self.current_token = self.lexer.get_next_token() def error(self): raise Exception("Invalid syntax") def eat(self, token_type): # compare the current token type with the passed token # type and if they match then "eat" the current token # and assign the next token to the self.current_token, # otherwise raise an exception. if self.current_token.type == token_type: self.current_token = self.lexer.get_next_token() else: self.error() def factor(self): """factor : (PLUS | MINUS | DICE) factor | INTEGER | LPAREN expr RPAREN""" token = self.current_token if token.type == PLUS: self.eat(PLUS) node = UnaryOp(token, self.factor()) return node elif token.type == MINUS: self.eat(MINUS) node = UnaryOp(token, self.factor()) return node elif token.type == DICE: self.eat(DICE) left = Num(Token(INTEGER, 1)) # 默认骰子个数为1 right = self.factor() node = BinOp(left, token, right) return node elif token.type == INTEGER: self.eat(INTEGER) return Num(token) elif token.type == LPAREN: self.eat(LPAREN) node = self.expr() self.eat(RPAREN) return node def term(self): """term : factor ((MUL | DIV) factor)*""" node = self.factor() while self.current_token.type in (MUL, DIV): token = self.current_token if token.type == MUL: self.eat(MUL) elif token.type == DIV: self.eat(DIV) node = BinOp(left=node, op=token, right=self.factor()) return node def expr(self): """ expr : term ((PLUS | MINUS) term)* term : factor ((MUL | DIV) factor)* factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN """ node = self.term() while self.current_token.type in (PLUS, MINUS): token = self.current_token if token.type == PLUS: self.eat(PLUS) elif token.type == MINUS: self.eat(MINUS) node = BinOp(left=node, op=token, right=self.term()) return node def parse(self): node = self.expr() if self.current_token.type != EOF: self.error() return node ############################################################################### # # # INTERPRETER # # # ############################################################################### class NodeVisitor(object): def visit(self, node): method_name = "visit_" + type(node).__name__ visitor = getattr(self, method_name, self.generic_visit) return visitor(node) def generic_visit(self, node): raise Exception("No visit_{} method".format(type(node).__name__)) class Interpreter(NodeVisitor): def __init__(self, parser): self.parser = parser def visit_BinOp(self, node): if node.op.type == PLUS: return self.visit(node.left) + self.visit(node.right) elif node.op.type == MINUS: return self.visit(node.left) - self.visit(node.right) elif node.op.type == DICE: return int( HydroDice(1).roll_dice( _counts=self.visit(node.left), _sides=self.visit(node.right), streamline=True, ) ) elif node.op.type == MUL: return self.visit(node.left) * self.visit(node.right) elif node.op.type == DIV: return self.visit(node.left) // self.visit(node.right) def visit_Num(self, node): return node.value def visit_UnaryOp(self, node): op = node.op.type if op == PLUS: return +self.visit(node.expr) elif op == MINUS: return -self.visit(node.expr) def interpret(self): tree = self.parser.parse() if tree is None: return "" return self.visit(tree) class Psi(Plugin): async def handle(self) -> None: lexer = Lexer(self.event.message.get_plain_text()[4:]) parser = Parser(lexer) interpreter = Interpreter(parser) result = interpreter.interpret() await self.event.reply(str(result)) async def rule(self) -> bool: return self.event.type == "message" and self.event.message.startswith(".psi")