diff options
Diffstat (limited to 'examples/plugins/nivis.❄')
| -rw-r--r-- | examples/plugins/nivis.❄ | 295 |
1 files changed, 295 insertions, 0 deletions
diff --git a/examples/plugins/nivis.❄ b/examples/plugins/nivis.❄ new file mode 100644 index 0000000..783fdb1 --- /dev/null +++ b/examples/plugins/nivis.❄ @@ -0,0 +1,295 @@ +""" 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") |
