From d57586f5cca8a1829658a7d77d454cad3bbff8a8 Mon Sep 17 00:00:00 2001 From: m-kus Date: Tue, 11 Feb 2020 15:05:47 +0300 Subject: [PATCH] annotations and error handling --- pytezos/repl/arithmetic.py | 169 +++++++++++++++------------------ pytezos/repl/control.py | 182 ++++++++++++++++++++++++------------ pytezos/repl/interpreter.py | 13 --- pytezos/repl/kernel.py | 73 +++++++++++++++ pytezos/repl/parser.py | 49 +++++++--- pytezos/repl/stack.py | 39 +++++--- pytezos/repl/structures.py | 163 ++++++++++++++++---------------- pytezos/repl/types.py | 171 ++++++++++++++++++++++----------- 8 files changed, 537 insertions(+), 322 deletions(-) delete mode 100644 pytezos/repl/interpreter.py create mode 100644 pytezos/repl/kernel.py diff --git a/pytezos/repl/arithmetic.py b/pytezos/repl/arithmetic.py index a92cbb7ad..6fa6b28b7 100644 --- a/pytezos/repl/arithmetic.py +++ b/pytezos/repl/arithmetic.py @@ -3,31 +3,20 @@ from pytezos.crypto import blake2b_32, Key from pytezos.repl.control import instruction from pytezos.repl.stack import Stack -from pytezos.repl.types import assert_type, Int, Nat, Timestamp, Mutez, Option, Pair, Bool, Bytes, Key, Signature, \ - KeyHash -from pytezos.repl.parser import assert_comparable, assert_expr_equal - - -def dispatch_type_map(a, b, mapping): - assert (type(a), type(b)) in mapping, \ - f'unsupported argument types {type(a).__name__} and {type(b).__name__}' - return mapping[(type(a), type(b))] - - -def assert_equal_types(a, b): - assert type(a) == type(b), f'different types {type(a).__name__} and {type(b).__name__}' +from pytezos.repl.types import assert_stack_type, Int, Nat, Timestamp, Mutez, Option, Pair, Bool, Bytes, Key, \ + Signature, KeyHash, dispatch_type_map @instruction('ABS') -def do_abs(stack: Stack, prim, args): +def do_abs(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Int) - res = Nat(abs(top.value)) - stack.ins(res) + assert_stack_type(top, Int) + res = Nat(abs(int(top))) + return stack.ins(res, annots=annots) @instruction('ADD') -def do_add(stack: Stack, prim, args): +def do_add(stack: Stack, prim, args, annots): a, b = stack.pop2() res_type = dispatch_type_map(a, b, { (Nat, Nat): Nat, @@ -38,26 +27,19 @@ def do_add(stack: Stack, prim, args): (Int, Timestamp): Timestamp, (Mutez, Mutez): Mutez }) - res = res_type(a.value + b.value) - stack.ins(res) + res = res_type(int(a) + int(b)) + return stack.ins(res, annots=annots) @instruction('COMPARE') -def do_compare(stack: Stack, prim, args): +def do_compare(stack: Stack, prim, args, annots): a, b = stack.pop2() - assert_expr_equal(a.type_expr, b.type_expr) - assert_comparable(a.type_expr) - if a.value > b.value: - res = Int(1) - elif a.value < b.value: - res = Int(-1) - else: - res = Int(0) - stack.ins(res) + res = Int(a.__cmp__(b)) + return stack.ins(res, annots=annots) @instruction('EDIV') -def do_ediv(stack: Stack, prim, args): +def do_ediv(stack: Stack, prim, args, annots): a, b = stack.pop2() q_type, r_type = dispatch_type_map(a, b, { (Nat, Nat): (Nat, Nat), @@ -67,21 +49,21 @@ def do_ediv(stack: Stack, prim, args): (Mutez, Nat): (Mutez, Mutez), (Mutez, Mutez): (Nat, Mutez) }) - if b.value == 0: + if int(b) == 0: res = Option.none(Pair.new(q_type(), r_type()).type_expr) else: - q, r = divmod(a.value, b.value) + q, r = divmod(int(a), int(b)) if r < 0: - r += abs(b.value) + r += abs(int(b)) q += 1 res = Option.some(Pair.new(q_type(q), r_type(r))) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction(['EQ', 'GE', 'GT', 'LE', 'LT', 'NEQ']) -def do_eq(stack: Stack, prim, args): +def do_eq(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Int) + assert_stack_type(top, Int) handlers = { 'EQ': lambda x: x == 0, 'GE': lambda x: x >= 0, @@ -90,44 +72,44 @@ def do_eq(stack: Stack, prim, args): 'LT': lambda x: x < 0, 'NEQ': lambda x: x != 0 } - res = Bool(handlers[prim](top.value)) - stack.ins(res) + res = Bool(handlers[prim](int(top))) + return stack.ins(res, annots=annots) @instruction('INT') -def do_int(stack: Stack, prim, args): +def do_int(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Nat) - res = Int(top.value) - stack.ins(res) + assert_stack_type(top, Nat) + res = Int(int(top)) + return stack.ins(res, annots=annots) @instruction('ISNAT') -def do_is_nat(stack: Stack, prim, args): +def do_is_nat(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Int) - if top.value >= 0: - res = Option.some(Nat(top.value)) + assert_stack_type(top, Int) + if int(top) >= 0: + res = Option.some(Nat(int(top))) else: res = Option.none(Nat().type_expr) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction(['LSL', 'LSR']) -def do_lsl(stack: Stack, prim, args): +def do_lsl(stack: Stack, prim, args, annots): a, b = stack.pop2() - assert_type(a, Nat) - assert_type(b, Nat) + assert_stack_type(a, Nat) + assert_stack_type(b, Nat) handlers = { 'LSL': lambda x: x[0] << x[1], 'LSR': lambda x: x[0] >> x[1] } - res = Nat(handlers[prim]((a.value, b.value))) - stack.ins(res) + res = Nat(handlers[prim]((int(a), int(b)))) + return stack.ins(res, annots=annots) @instruction('MUL') -def do_mul(stack: Stack, prim, args): +def do_mul(stack: Stack, prim, args, annots): a, b = stack.pop2() res_type = dispatch_type_map(a, b, { (Nat, Nat): Nat, @@ -137,20 +119,20 @@ def do_mul(stack: Stack, prim, args): (Mutez, Nat): Mutez, (Nat, Mutez): Mutez }) - res = res_type(a.value * b.value) - stack.ins(res) + res = res_type(int(a) * int(b)) + return stack.ins(res, annots=annots) @instruction('NEG') -def do_neg(stack: Stack, prim, args): +def do_neg(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, [Int, Nat]) - res = Int(-top.value) - stack.ins(res) + assert_stack_type(top, [Int, Nat]) + res = Int(-int(top)) + return stack.ins(res, annots=annots) @instruction('SUB') -def do_sub(stack: Stack, prim, args): +def do_sub(stack: Stack, prim, args, annots): a, b = stack.pop2() res_type = dispatch_type_map(a, b, { (Nat, Nat): Int, @@ -161,78 +143,79 @@ def do_sub(stack: Stack, prim, args): (Timestamp, Timestamp): Int, (Mutez, Mutez): Mutez }) - res = res_type(a.value - b.value) - stack.ins(res) + res = res_type(int(a) - int(b)) + return stack.ins(res, annots=annots) @instruction(['AND', 'OR', 'XOR']) -def do_and(stack: Stack, prim, args): +def do_and(stack: Stack, prim, args, annots): a, b = stack.pop2() - assert_type(a, [Bool, Nat]) - assert_equal_types(a, b) + val_type = dispatch_type_map(a, b, { + (Bool, Bool): bool, + (Nat, Nat): int + }) handlers = { 'AND': lambda x: x[0] & x[1], 'OR': lambda x: x[0] | x[1], 'XOR': lambda x: x[0] ^ x[1] } - res_type = type(a) - res = res_type(handlers[prim]((a.value, b.value))) - stack.ins(res) + res = type(a)(handlers[prim]((val_type(a), val_type(b)))) + return stack.ins(res, annots=annots) @instruction('NOT') -def do_not(stack: Stack, prim, args): +def do_not(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, [Nat, Int, Bool]) + assert_stack_type(top, [Nat, Int, Bool]) if type(top) in [Nat, Int]: - res = Int(~top.value) + res = Int(~int(top)) elif type(top) == Bool: - res = Bool(not top.value) + res = Bool(not bool(top)) else: assert False - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('BLAKE2B') -def do_blake2b(stack: Stack, prim, args): +def do_blake2b(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Bytes) - res = Bytes(blake2b_32(top.value).digest()) - stack.ins(res) + assert_stack_type(top, Bytes) + res = Bytes(blake2b_32(bytes(top)).digest()) + return stack.ins(res, annots=annots) @instruction('CHECK_SIGNATURE') -def do_check_sig(stack: Stack, prim, args): +def do_check_sig(stack: Stack, prim, args, annots): pk, sig, msg = stack.pop3() - assert_type(pk, Key) - assert_type(sig, Signature) - assert_type(msg, Bytes) - key = Key.from_encoded_key(pk.value) + assert_stack_type(pk, Key) + assert_stack_type(sig, Signature) + assert_stack_type(msg, Bytes) + key = Key.from_encoded_key(str(pk)) try: - key.verify(signature=sig.value, message=msg.value) + key.verify(signature=str(sig), message=bytes(msg)) except: res = Bool(False) else: res = Bool(True) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('HASH_KEY') -def do_hash_key(stack: Stack, prim, args): +def do_hash_key(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Key) - key = Key.from_encoded_key(top.value) + assert_stack_type(top, Key) + key = Key.from_encoded_key(str(top)) res = KeyHash(key.public_key_hash()) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction(['SHA256', 'SHA512']) -def do_sha(stack: Stack, prim, args): +def do_sha(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Bytes) + assert_stack_type(top, Bytes) handlers = { 'SHA256': lambda x: sha256(x).digest(), 'SHA512': lambda x: sha512(x).digest(), } - res = Bytes(handlers[prim](top.value)) - stack.ins(res) + res = Bytes(handlers[prim](bytes(top))) + return stack.ins(res, annots=annots) diff --git a/pytezos/repl/control.py b/pytezos/repl/control.py index 41475304c..5ae18e38e 100644 --- a/pytezos/repl/control.py +++ b/pytezos/repl/control.py @@ -2,12 +2,17 @@ from copy import deepcopy from pytezos.repl.stack import Stack -from pytezos.repl.types import StackItem, assert_type, Option, Lambda, Bool, List, Or, Pair, Map -from pytezos.repl.parser import get_int, assert_pushable +from pytezos.repl.types import StackItem, assert_stack_type, assert_expr_equal, Option, Lambda, Bool, List, Or, Pair, \ + Map +from pytezos.repl.parser import get_int, MichelsonRuntimeError instructions = {} +def assert_no_annots(prim, annots): + assert not annots, f'{prim}: unexpected annotations {annots}' + + def instruction(prim, args_len=0): def register_instruction(func): if isinstance(prim, list): @@ -21,6 +26,18 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper return register_instruction + + +def parse_instruction(code_expr): + prim = code_expr.get('prim') + if prim not in instructions: + raise MichelsonRuntimeError.init('unknown instruction', prim) from None + args_len, handler = instructions[prim] + args = code_expr.get('args', []) + if len(args) != args_len: + raise MichelsonRuntimeError.init(f'expected {args_len} arg(s), got {len(args)}', prim) from None + annots = code_expr.get('annots', []) + return prim, args, annots, handler def do_interpret(stack: Stack, code_expr): @@ -28,107 +45,117 @@ def do_interpret(stack: Stack, code_expr): for item in code_expr: do_interpret(stack, item) elif isinstance(code_expr, dict): - prim = code_expr.get('prim') - assert prim in instructions, f'{prim}: unknown instruction' - args_len, handler = instructions[prim] - args = code_expr.get('args', []) - assert len(args) == args_len, f'{prim}: expected {args_len} arg(s), got {len(args)}' - return handler(stack, prim, args) + prim, args, annots, handler = parse_instruction(code_expr) + try: + res = handler(stack, prim, args, annots) + except AssertionError as e: + raise MichelsonRuntimeError.init(e.args, prim) from None + except MichelsonRuntimeError as e: + raise MichelsonRuntimeError.wrap(e, prim) from None + else: + return res else: assert False, f'unexpected code expression: {code_expr}' @instruction('PUSH', args_len=2) -def do_push(stack: Stack, prim, args): - assert_pushable(args[0]) +def do_push(stack: Stack, prim, args, annots): item = StackItem.parse(val_expr=args[1], type_expr=args[0]) - stack.ins(item) + return stack.ins(item, annots=annots) @instruction('DROP', args_len=1) -def do_drop(stack: Stack, prim, args): +def do_drop(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) count = get_int(args[0]) _ = stack.pop_many(count=count, index=0) @instruction('DUP') -def do_dup(stack: Stack, prim, args): +def do_dup(stack: Stack, prim, args, annots): top = stack.peek() - stack.ins(deepcopy(top)) + return stack.ins(deepcopy(top), annots=annots) @instruction('SWAP') -def do_swap(stack: Stack, prim, args): +def do_swap(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) second = stack.pop(index=1) - stack.ins(second) + return stack.ins(second) @instruction('DIG', args_len=1) -def do_dig(stack: Stack, prim, args): +def do_dig(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) index = get_int(args[0]) - value = stack.pop(index=index) - stack.ins(value) + res = stack.pop(index=index) + return stack.ins(res) @instruction('DUG', args_len=1) -def do_dug(stack: Stack, prim, args): +def do_dug(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) index = get_int(args[0]) - value = stack.pop() - stack.ins(value, index=index - 1) + res = stack.pop() + return stack.ins(res, index=index-1) @instruction('DIP', args_len=2) -def do_dip(stack: Stack, prim, args): +def do_dip(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) count = get_int(args[0]) - protected = stack.pop_many(count=count, index=0) + protected = stack.pop_many(count=count) do_interpret(stack, args[1]) stack.ins_many(protected) @instruction('LAMBDA', args_len=3) -def do_lambda(stack: Stack, prim, args): +def do_lambda(stack: Stack, prim, args, annots): res = Lambda.new(p_type_expr=args[0], r_type_expr=args[1], code=args[2]) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('EXEC') -def do_exec(stack: Stack, prim, args): - param, lmbd = stack.pop2() - assert_type(lmbd, Lambda) - lmbd.assert_param_type(param) - lmbd_stack = Stack([param]) - do_interpret(lmbd_stack, lmbd.value) - ret = lmbd_stack.pop() - lmbd.assert_ret_type(ret) - assert len(lmbd_stack) == 0, 'Lambda stack is not empty' - stack.ins(ret) +def do_exec(stack: Stack, prim, args, annots): + param, lmbda = stack.pop2() + assert_stack_type(lmbda, Lambda) + lmbda.assert_param_type(param) + lmbda_stack = Stack([param]) + do_interpret(lmbda_stack, lmbda.code) + ret = lmbda_stack.pop() + lmbda.assert_ret_type(ret) + assert len(lmbda_stack) == 0, f'lambda stack is not empty: {lmbda_stack}' + return stack.ins(ret, annots=annots) @instruction('APPLY') -def do_apply(stack: Stack, prim, args): +def do_apply(stack: Stack, prim, args, annots): param, lmbd = stack.pop2() - assert_type(lmbd, Lambda) + assert_stack_type(lmbd, Lambda) # TODO: @instruction('FAILWITH') -def do_failwith(stack: Stack, prim, args): +def do_failwith(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) top = stack.pop() raise ValueError(top) @instruction('IF', args_len=2) -def do_if(stack: Stack, prim, args): +def do_if(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) cond = stack.pop() - assert_type(cond, Bool) - do_interpret(stack, args[0 if cond.value else 1]) + assert_stack_type(cond, Bool) + do_interpret(stack, args[0 if bool(cond) else 1]) @instruction('IF_CONS', args_len=2) -def do_if_cons(stack: Stack, prim, args): +def do_if_cons(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) top = stack.pop() - assert_type(top, List) - if len(top.value) > 0: + assert_stack_type(top, List) + if len(top) > 0: stack.ins(top) do_interpret(stack, args[0]) else: @@ -136,33 +163,55 @@ def do_if_cons(stack: Stack, prim, args): @instruction('IF_LEFT', args_len=2) -def do_if_left(stack: Stack, prim, args): - top = stack.pop() - assert_type(top, Or) - stack.ins(top.value) +def do_if_left(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) + top = stack.pop() # type: Or + assert_stack_type(top, Or) + stack.ins(next(iter(top))) do_interpret(stack, args[0 if top.is_left() else 1]) @instruction('IF_NONE', args_len=2) -def do_if_left(stack: Stack, prim, args): +def do_if_left(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) top = stack.pop() - assert_type(top, Option) - if top.value is None: + assert_stack_type(top, Option) + if top.is_none(): do_interpret(stack, args[0]) else: - stack.ins(top.value) + stack.ins(next(iter(top))) do_interpret(stack, args[1]) @instruction('LOOP', args_len=1) -def do_loop(stack: Stack, prim, args): - pass # TODO +def do_loop(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) + while True: + top = stack.pop() + assert_stack_type(top, Bool) + if bool(top): + do_interpret(stack, args[0]) + else: + break + + +@instruction('LOOP_LEFT', args_len=1) +def do_loop_left(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) + while True: + top = stack.pop() + assert_stack_type(top, Or) + stack.ins(next(iter(top))) + if top.is_left(): + do_interpret(stack, args[0]) + else: + break @instruction('MAP', args_len=1) -def do_map(stack: Stack, prim, args): +def do_map(stack: Stack, prim, args, annots): container = stack.pop() - assert_type(container, [List, Map]) + assert_stack_type(container, [List, Map]) if type(container) == List: items = list() @@ -184,11 +233,12 @@ def do_map(stack: Stack, prim, args): assert False res = type(container).new(items) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('ITER', args_len=1) -def do_map(stack: Stack, prim, args): +def do_map(stack: Stack, prim, args, annots): + assert_no_annots(prim, annots) container = stack.pop() if type(container) == List: for item in container: @@ -200,3 +250,17 @@ def do_map(stack: Stack, prim, args): do_interpret(stack, args[0]) else: assert False, f'Unexpected type: {type(container)}' + + +@instruction('CAST', args_len=1) +def do_cast(stack: Stack, prim, args, annots): + top = stack.pop() + assert_expr_equal(args[0], top.type_expr) + top.type_expr = args[0] + return stack.ins(top, annots=annots) + + +@instruction('RENAME') +def do_rename(stack: Stack, prim, args, annots): + top = stack.pop() + return stack.ins(top, annots=annots) diff --git a/pytezos/repl/interpreter.py b/pytezos/repl/interpreter.py deleted file mode 100644 index eb0b62f15..000000000 --- a/pytezos/repl/interpreter.py +++ /dev/null @@ -1,13 +0,0 @@ -from pytezos.michelson.converter import michelson_to_micheline -from pytezos.repl.stack import Stack -from pytezos.repl.control import do_interpret - - -class Interpreter: - - def __init__(self): - self.stack = Stack([]) - - def run(self, command): - expr = michelson_to_micheline(command) - return do_interpret(self.stack, expr) diff --git a/pytezos/repl/kernel.py b/pytezos/repl/kernel.py new file mode 100644 index 000000000..98b886af3 --- /dev/null +++ b/pytezos/repl/kernel.py @@ -0,0 +1,73 @@ +from ipykernel.kernelbase import Kernel + +from pytezos.michelson.grammar import MichelsonParserError +from pytezos.michelson.converter import michelson_to_micheline, micheline_to_michelson +from pytezos.repl.stack import Stack, StackItem +from pytezos.repl.control import do_interpret, MichelsonRuntimeError + + +class MichelsonInterpreter: + + def __init__(self): + self.stack = Stack([]) + + def run(self, code): + try: + expr = michelson_to_micheline(code) + except MichelsonParserError as e: + return + + backup = self.stack.copy() + + try: + res = do_interpret(self.stack, expr) + except MichelsonRuntimeError as e: + self.stack = backup + return + + if isinstance(res, StackItem): + return micheline_to_michelson(res.val_expr) + + +class MichelsonKernel(Kernel): + implementation = 'IMichelson' + implementation_version = '0.1.0' + language_info = { + 'mimetype': 'text/x-michelson', + 'file_extension': 'tz', + 'codemirror_mode': 'michelson' + } + banner = 'Tezos VM language' + help_links = [ + 'https://michelson.nomadic-labs.com/', + 'https://tezos.gitlab.io/whitedoc/michelson.html' + ] + + def __init__(self, **kwargs): + super(MichelsonKernel, self).__init__(**kwargs) + self.interpreter = MichelsonInterpreter() + + def do_execute(self, code, silent, store_history=True, user_expressions=None, allow_stdin=False): + try: + self.interpreter.run(code) + except MichelsonRuntimeError: + pass + except MichelsonParserError: + pass + else: + pass + + if not silent: + stream_content = {'name': 'stdout', 'text': code} + self.send_response(self.iopub_socket, 'stream', stream_content) + + return {'status': 'ok', + 'execution_count': self.execution_count, + 'payload': [], + 'user_expressions': {}} + + def do_complete(self, code, cursor_pos): + pass + + def do_inspect(self, code, cursor_pos, detail_level=0): + pass diff --git a/pytezos/repl/parser.py b/pytezos/repl/parser.py index fa4463774..f08b3925a 100644 --- a/pytezos/repl/parser.py +++ b/pytezos/repl/parser.py @@ -9,6 +9,22 @@ parsers = {} +class MichelsonRuntimeError(ValueError): + + def __init__(self, message, trace): + super(MichelsonRuntimeError, self).__init__(message) + self.message = message + self.trace = trace + + @staticmethod + def init(message, prim) -> 'MichelsonRuntimeError': + return MichelsonRuntimeError(message, trace=[prim]) + + @staticmethod + def wrap(error: 'MichelsonRuntimeError', prim) -> 'MichelsonRuntimeError': + return MichelsonRuntimeError(error.message, trace=[prim] + error.trace) + + def primitive(prim, args_len=0): def register_primitive(func): parsers[prim] = (args_len, func) @@ -20,13 +36,13 @@ def wrapper(*args, **kwargs): return register_primitive -def assert_list(val_expr): - assert isinstance(val_expr, list), f'expected list, got {type(val_expr)}' +def assert_type(value, exp_type): + assert isinstance(value, exp_type), f'expected {exp_type}, got {type(value)}' def parse_prim_expr(expr) -> tuple: assert isinstance(expr, dict), f'expected dict, got {type(expr)}' - assert 'prim' in expr, f'primitive is absent' + assert 'prim' in expr, f'prim field is absent' return expr['prim'], expr.get('args', []) @@ -129,7 +145,9 @@ def assert_comparable(type_expr): def assert_pushable(type_expr): - pass + prim, _ = parse_prim_expr(type_expr) + assert prim not in ['big_map', 'contract', 'operation'], \ + f'type is not pushable: {micheline_to_michelson(type_expr)}' def is_big_map_val(type_expr): @@ -145,15 +163,24 @@ def assert_big_map_val(type_expr): def parse_type(type_expr) -> Tuple[str, list, Callable]: prim, args = parse_prim_expr(type_expr) - assert prim in parsers, f'{prim}: unknown primitive ' + if prim not in parsers: + raise MichelsonRuntimeError.init('unknown primitive', prim) from None args_len, func = parsers[prim] - assert len(args) == args_len, f'{prim}: expected {args_len} arg(s), got {len(args)}' + if len(args) != args_len: + raise MichelsonRuntimeError.init('expected {args_len} arg(s), got {len(args)}', prim) from None return prim, args, func def parse_value(val_expr, type_expr): - _, args, func = parse_type(type_expr) - return func(val_expr, args) + prim, args, func = parse_type(type_expr) + try: + res = func(val_expr, args) + except AssertionError as e: + raise MichelsonRuntimeError(e.args, prim) from None + except MichelsonRuntimeError as e: + raise MichelsonRuntimeError.wrap(e, prim) from None + else: + return res @primitive('string') @@ -193,7 +220,7 @@ def parse_unit(val_expr, type_args): @primitive('list', args_len=1) def parse_list(val_expr, type_args): - assert_list(val_expr) + assert_type(val_expr, list) return [parse_value(item, type_args[0]) for item in val_expr] @@ -221,7 +248,7 @@ def parse_or(val_expr, type_args): @primitive('set', args_len=1) def parse_set(val_expr, type_args): - assert_list(val_expr) + assert_type(val_expr, list) assert_comparable(type_args[0]) value = {parse_value(item, type_args[0]) for item in val_expr} assert len(value) == len(val_expr), f'found duplicate elements' @@ -235,7 +262,7 @@ def parse_elt(val_expr, type_args) -> tuple: @primitive('map', args_len=2) def parse_map(val_expr, type_args): - assert_list(val_expr) + assert_type(val_expr, list) assert_comparable(type_args[0]) return dict(parse_elt(item, type_args) for item in val_expr) diff --git a/pytezos/repl/stack.py b/pytezos/repl/stack.py index 97fae0147..908b581d8 100644 --- a/pytezos/repl/stack.py +++ b/pytezos/repl/stack.py @@ -1,5 +1,9 @@ from copy import deepcopy from pprint import pformat +from typing import List + +from pytezos.repl.types import StackItem, assert_stack_item +from pytezos.repl.parser import assert_pushable class Stack: @@ -8,33 +12,44 @@ def __init__(self, items: list): self.items = items def ins_many(self, items: list, index: int = 0): - assert len(self.items) >= index, f'Got {len(self.items)} items, wanted to insert before {index}th' + assert len(self.items) >= index, f'got {len(self.items)} items, wanted to insert before {index}th' + for item in items: + assert_stack_item(item) + assert_pushable(item.type_expr) self.items[index:index] = items - def ins(self, item, index: int = 0): - self.ins_many([item], index=index) # TODO: use insert + def ins(self, item: StackItem, index: int = 0, annots=None): + assert_stack_item(item) + assert_pushable(item.type_expr) + item.val_expr['annots'] = annots or [] + self.items.insert(index, item) + if index == 0: + return self.items[0] def peek(self): - assert len(self.items) > 0, 'Stack is empty' + assert len(self.items) > 0, 'stack is empty' return self.items[0] - def pop_many(self, count: int, index: int = 0) -> list: - assert len(self.items) - index >= count, f'Got {len(self.items)} items, requested {count} from {index}th' + def pop_many(self, count: int, index: int = 0) -> List[StackItem]: + assert len(self.items) - index >= count, f'got {len(self.items)} items, requested {count} from {index}th' return [self.items.pop(index) for _ in range(count)] def pop(self, index: int = 0): - return self.pop_many(count=1, index=index)[0] + res = self.pop_many(count=1, index=index) + return res[0] - def pop2(self) -> list: - return self.pop_many(count=2) + def pop2(self): + res = self.pop_many(count=2) + return tuple(*res) - def pop3(self) -> list: - return self.pop_many(count=3) + def pop3(self): + res = self.pop_many(count=3) + return tuple(*res) def copy(self) -> 'Stack': return Stack(deepcopy(self.items)) - def __len__(self): + def __len__(self) -> int: return len(self.items) def __repr__(self): diff --git a/pytezos/repl/structures.py b/pytezos/repl/structures.py index d3b439bff..04dc44c86 100644 --- a/pytezos/repl/structures.py +++ b/pytezos/repl/structures.py @@ -1,182 +1,181 @@ from pytezos.michelson.forge import forge_micheline from pytezos.repl.control import instruction from pytezos.repl.stack import Stack -from pytezos.repl.types import assert_type, Option, Pair, String, Bytes, List, BigMap, Map, Set, Or, Bool, Nat, \ - Unit, StackItem +from pytezos.repl.types import assert_stack_type, Option, Pair, String, Bytes, List, BigMap, Map, Set, Or, Bool, Nat, \ + Unit, StackItem, dispatch_type_map @instruction(['CAR', 'CDR']) -def do_car(stack: Stack, prim, args): +def do_car(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Pair) + assert_stack_type(top, Pair) handlers = { 'CAR': lambda x: x[0], 'CDR': lambda x: x[1] } - res = handlers[prim](top.value) - stack.ins(res) + res = handlers[prim](list(iter(top))) + return stack.ins(res, annots=annots) @instruction('CONCAT') -def do_concat(stack: Stack, prim, args): +def do_concat(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, [String, Bytes, List]) + assert_stack_type(top, [String, Bytes, List]) if type(top) in [String, Bytes]: second = stack.pop() - assert_type(second, type(top)) - res_type = type(top) - res = res_type(top.value + second.value) + val_type = dispatch_type_map(top, second, { + (String, String): str, + (Bytes, Bytes): bytes + }) + res = type(top)(val_type(top) + val_type(second)) elif type(top) == List: - val_type = top.val_type() - delimiter = val_type() - assert_type(delimiter, [String, Bytes]) - res = val_type(delimiter.join(top.value)) + res_type = top.val_type() + val_type, sep = {String: (str, ''), Bytes: (bytes, b'')}[res_type] + res = res_type(sep.join(map(val_type, top))) else: assert False - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('CONS') -def do_cons(stack: Stack, prim, args): - elt, lst = stack.pop2() - assert_type(lst, List) - lst.assert_val_type(elt) - res = elt + lst - stack.ins(res) +def do_cons(stack: Stack, prim, args, annots): + val, container = stack.pop2() + assert_stack_type(container, List) + res = container.prepend(val) + return stack.ins(res, annots=annots) @instruction('EMPTY_BIG_MAP', args_len=2) -def do_empty_big_map(stack: Stack, prim, args): +def do_empty_big_map(stack: Stack, prim, args, annots): res = BigMap.empty(k_type_expr=args[0], v_type_expr=args[1]) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('EMPTY_MAP', args_len=2) -def do_empty_map(stack: Stack, prim, args): +def do_empty_map(stack: Stack, prim, args, annots): res = Map.empty(k_type_expr=args[0], v_type_expr=args[1]) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('EMPTY_SET', args_len=1) -def do_empty_set(stack: Stack, prim, args): +def do_empty_set(stack: Stack, prim, args, annots): res = Set.empty(k_type_expr=args[0]) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('GET') -def do_get(stack: Stack, prim, args): - key, bmp = stack.pop2() - assert_type(bmp, [Map, BigMap]) - res = bmp[key] - stack.ins(res) +def do_get(stack: Stack, prim, args, annots): + key, container = stack.pop2() + assert_stack_type(container, [Map, BigMap]) + if key in container: + val = next(v for k, v in container if k == key) + res = Option.some(val) + else: + res = Option.none(container.val_type_expr()) + return stack.ins(res, annots=annots) @instruction(['LEFT', 'RIGHT'], args_len=1) -def do_left(stack: Stack, prim, args): +def do_left(stack: Stack, prim, args, annots): top = stack.pop() if prim == 'LEFT': - res = Or.left(type_expr=args[0], item=top) + res = Or.left(r_type_expr=args[0], item=top) else: - res = Or.right(type_expr=args[0], item=top) - stack.ins(res) + res = Or.right(l_type_expr=args[0], item=top) + return stack.ins(res, annots=annots) @instruction('MEM') -def do_mem(stack: Stack, prim, args): +def do_mem(stack: Stack, prim, args, annots): key, container = stack.pop2() - assert_type(container, [Set, Map, BigMap]) - container.assert_key_type(key) - res = Bool(key.value in container.value) - stack.ins(res) + assert_stack_type(container, [Set, Map, BigMap]) + res = Bool(key in container) + return stack.ins(res, annots=annots) @instruction('NIL', args_len=1) -def do_nil(stack: Stack, prim, args): +def do_nil(stack: Stack, prim, args, annots): nil = List.empty(args[0]) - stack.ins(nil) + return stack.ins(nil, annots=annots) @instruction('NONE', args_len=1) -def do_none(stack: Stack, prim, args): +def do_none(stack: Stack, prim, args, annots): none = Option.none(args[0]) - stack.ins(none) + return stack.ins(none, annots=annots) @instruction('PACK') -def do_pack(stack: Stack, prim, args): +def do_pack(stack: Stack, prim, args, annots): top = stack.pop() res = Bytes(forge_micheline(top.val_node)) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('PAIR') -def do_pair(stack: Stack, prim, args): +def do_pair(stack: Stack, prim, args, annots): left, right = stack.pop2() res = Pair.new(left, right) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('SIZE') -def do_size(stack: Stack, prim, args): +def do_size(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, [String, Bytes, List, Set, Map]) - res = Nat(len(top.value)) - stack.ins(res) + assert_stack_type(top, [String, Bytes, List, Set, Map]) + res = Nat(len(top)) + return stack.ins(res, annots=annots) @instruction('SLICE') -def do_slice(stack: Stack, prim, args): +def do_slice(stack: Stack, prim, args, annots): offset, length, s = stack.pop3() - assert_type(s, [String, Bytes]) - res_type = type(s) - if offset + length < len(s.value): - sls = s.value[offset:offset + length] - res = Option.some(res_type(sls)) + assert_stack_type(s, [String, Bytes]) + if offset + length < len(s): + sls = str(s)[offset:offset + length] + res = Option.some(type(s)(sls)) else: - res = Option.none(res_type().type_expr) - stack.ins(res) + res = Option.none(type(s)().type_expr) + return stack.ins(res, annots=annots) @instruction('SOME') -def do_some(stack: Stack, prim, args): +def do_some(stack: Stack, prim, args, annots): top = stack.pop() res = Option.some(top) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('UNIT') -def do_unit(stack: Stack, prim, args): - stack.ins(Unit()) +def do_unit(stack: Stack, prim, args, annots): + return stack.ins(Unit(), annots=annots) @instruction('UNPACK', args_len=1) -def do_unpack(stack: Stack, prim, args): +def do_unpack(stack: Stack, prim, args, annots): top = stack.pop() - assert_type(top, Bytes) + assert_stack_type(top, Bytes) # TODO: parse micheline res = StackItem.parse(type_expr=args[0], val_expr=None) - stack.ins(res) + return stack.ins(res, annots=annots) @instruction('UPDATE') -def do_update(stack: Stack, prim, args): +def do_update(stack: Stack, prim, args, annots): key, val, container = stack.pop3() - assert_type(container, [Set, Map, BigMap]) - container.assert_key_type(key) + assert_stack_type(container, [Set, Map, BigMap]) if type(container) == Set: - assert_type(val, Bool) + assert_stack_type(val, Bool) if val: - container.value.add(key) # TODO + res = container.add(key) else: - container.value.remove(key) # TODO + res = Set.new(list(filter(lambda x: x != key, container))) else: - assert_type(val, Option) - if val.value is None: - if key in container.value: - del container.value[key] # TODO + assert_stack_type(val, Option) + if val.is_none(): + res = type(container).new(list(filter(lambda x: x[0] != key, container))) else: - container.assert_val_type(val.value) - container.value[key] = val.value # TODO + res = container.add(key, next(iter(val))) - stack.ins(container) + return stack.ins(res, annots=annots) diff --git a/pytezos/repl/types.py b/pytezos/repl/types.py index 234918e5f..4764c72ea 100644 --- a/pytezos/repl/types.py +++ b/pytezos/repl/types.py @@ -1,22 +1,31 @@ from pprint import pformat from pytezos.encoding import is_pkh -from pytezos.michelson.converter import micheline_to_michelson from pytezos.repl.parser import parse_value, parse_prim_expr, assert_expr_equal, assert_comparable, \ - assert_big_map_val, expr_equal + assert_big_map_val, expr_equal, assert_type def assert_stack_item(item: 'StackItem'): assert isinstance(item, StackItem), f'expected StackItem, got {type(item).__name__}' -def assert_type(item: 'StackItem', item_type): +def assert_stack_type(item: 'StackItem', item_type): if not isinstance(item_type, list): item_type = [item_type] expected = ' or '.join(map(lambda x: x.__name__, item_type)) assert type(item) in item_type, f'expected {expected}, got {type(item).__name__}: {item}' +def dispatch_type_map(a, b, mapping): + assert (type(a), type(b)) in mapping, \ + f'unsupported argument types {type(a).__name__} and {type(b).__name__}' + return mapping[(type(a), type(b))] + + +def assert_equal_types(a, b): + assert type(a) == type(b), f'different types {type(a).__name__} and {type(b).__name__}' + + class StackItem: prim = '' __cls__ = {} @@ -24,7 +33,7 @@ class StackItem: def __init__(self, *args, val_expr, type_expr): self.val_expr = val_expr self.type_expr = type_expr - self.value = parse_value(val_expr, type_expr) + self._val = parse_value(val_expr, type_expr) @staticmethod def _get_type(type_expr): @@ -44,23 +53,45 @@ def __init_subclass__(cls, prim='', args_len=0, **kwargs): cls.__cls__[prim] = cls, args_len cls.prim = prim - def __repr__(self): - return pformat(self.value) + def __cmp__(self, other: 'StackItem') -> int: + assert_stack_item(other) + assert_expr_equal(self.type_expr, other.type_expr) + if self._val > other._val: + return 1 + elif self._val < other._val: + return -1 + else: + return 0 - @property - def val(self): - return micheline_to_michelson(self.val_expr) + def __eq__(self, other: 'StackItem'): + assert_stack_item(other) + assert_expr_equal(self.type_expr, other.type_expr) + return expr_equal(self.val_expr, other.val_expr) - @property - def type(self): - return micheline_to_michelson(self.type_expr) + def __int__(self): + assert_type(self._val, int) + return self._val + + def __str__(self): + assert_type(self._val, str) + return self._val + + def __bytes__(self): + assert_type(self._val, bytes) + return self.__bytes__() - # - # def _arg_type(self, arg_idx: int): - # return StackItem.dispatch(self.type_expr['args'][arg_idx]) - # - # def _arg_type_expr(self, arg_idx): - # return self.type_expr['args'][arg_idx] + def __bool__(self): + assert_type(self._val, bool) + return self._val + + def __len__(self): + return len(self._val) + + def __repr__(self): + return pformat(self._val) + + def arg_types(self): + return [self._get_type(x) for x in self.type_expr['args']] class String(StackItem, prim='string'): @@ -75,6 +106,7 @@ def __init__(self, value='', val_expr=None, type_expr=None): class Int(StackItem, prim='int'): def __init__(self, value=0, val_expr=None, type_expr=None): + assert_type(value, int) assert isinstance(value, int) super(Int, self).__init__( val_expr=val_expr or {'int': str(value)}, @@ -84,7 +116,7 @@ def __init__(self, value=0, val_expr=None, type_expr=None): class Bytes(StackItem, prim='bytes'): def __init__(self, value=b'', type_expr=None, val_expr=None): - assert isinstance(value, bytes) + assert_type(value, bytes) super(Bytes, self).__init__( val_expr=val_expr or {'bytes': value.hex()}, type_expr=type_expr or {'prim': self.prim}) @@ -93,15 +125,15 @@ def __init__(self, value=b'', type_expr=None, val_expr=None): class Nat(Int, prim='nat'): def __init__(self, value=0, val_expr=None, type_expr=None): - assert isinstance(value, int) - assert value >= 0 + assert_type(value, int) + assert value >= 0, 'expected non-negative value' super(Nat, self).__init__(value, val_expr=val_expr, type_expr=type_expr) class Bool(StackItem, prim='bool'): def __init__(self, value=False, val_expr=None, type_expr=None): - assert isinstance(value, bool) + assert_type(value, bool) super(Bool, self).__init__( val_expr=val_expr or {'int': str(value)}, type_expr=type_expr or {'prim': self.prim}) @@ -118,11 +150,11 @@ def __init__(self, val_expr=None, type_expr=None): class List(StackItem, prim='list', args_len=1): def __iter__(self): - val_type = self.val_type() + val_type, = self.arg_types() for item in self.val_expr: yield val_type(val_expr=item, type_expr=self.type_expr['args'][0]) - def __radd__(self, item: StackItem) -> 'List': + def prepend(self, item: StackItem) -> 'List': self.assert_val_type(item) return self.parse(val_expr=[item.val_expr] + self.val_expr, type_expr=self.type_expr) @@ -136,9 +168,6 @@ def new(cls, items: list): val_expr = [assert_stack_item(x) or x.val_expr for x in items] return cls(val_expr=val_expr, type_expr={'prim': cls.prim, 'args': [items[0].type_expr]}) - def val_type(self): - return self._get_type(self.type_expr['args'][0]) - def assert_val_type(self, item: StackItem): assert_stack_item(item) assert_expr_equal(self.type_expr['args'][0], item.type_expr) @@ -154,6 +183,10 @@ def new(cls, left: StackItem, right: StackItem): type_expr={'prim': cls.prim, 'args': [left.type_expr, right.type_expr]}, val_expr={'prim': 'Pair', 'args': [left.val_expr, right.val_expr]}) + def __iter__(self): + for i in [0, 1]: + yield self.parse(val_expr=self.val_expr['args'][i], type_expr=self.type_expr['args'][i]) + class Option(StackItem, prim='option', args_len=1): @@ -170,24 +203,35 @@ def none(cls, type_expr): type_expr={'prim': cls.prim, 'args': [type_expr]}, val_expr={'prim': 'None'}) + def __iter__(self): + if not self.is_none(): + yield self.parse(val_expr=self.val_expr['args'][0], type_expr=self.type_expr['args'][0]) + + def is_none(self): + return self._val is None + class Or(StackItem, prim='or', args_len=2): @classmethod - def left(cls, type_expr, item: StackItem): + def left(cls, r_type_expr, item: StackItem): assert_stack_item(item) - assert_expr_equal(type_expr['args'][0], item.type_expr) - return cls(type_expr=type_expr, val_expr={'prim': 'Left', 'args': [item.val_expr]}) + return cls(type_expr={'prim': cls.prim, 'args': [item.type_expr, r_type_expr]}, + val_expr={'prim': 'Left', 'args': [item.val_expr]}) @classmethod - def right(cls, type_expr, item: StackItem): + def right(cls, l_type_expr, item: StackItem): assert_stack_item(item) - assert_expr_equal(type_expr['args'][1], item.type_expr) - return cls(type_expr=type_expr, val_expr={'prim': 'Right', 'args': [item.val_expr]}) + return cls(type_expr={'prim': cls.prim, 'args': [l_type_expr, item.type_expr]}, + val_expr={'prim': 'Right', 'args': [item.val_expr]}) + + def __iter__(self): + idx = 0 if self.is_left() else 1 + yield self.parse(val_expr=self.val_expr['args'][0], type_expr=self.type_expr['args'][idx]) - # def is_left(self): - # prim, _ = parse_prim_expr(self.val_expr) - # return prim == 'Left' + def is_left(self): + prim, _ = parse_prim_expr(self.val_expr) + return prim == 'Left' class Set(StackItem, prim='set', args_len=1): @@ -197,12 +241,31 @@ def empty(cls, k_type_expr): assert_comparable(k_type_expr) return cls(type_expr={'prim': cls.prim, 'args': [k_type_expr]}, val_expr=[]) + @classmethod + def new(cls, items: list): + assert isinstance(items, list) and len(items) > 0, f'expected non-empty list' + assert len(set(items)) == len(items), f'duplicate keys found' + val_expr = [assert_stack_item(x) or x.val_expr for x in items] + return cls(val_expr=val_expr, type_expr={'prim': cls.prim, 'args': [items[0].type_expr]}) + + def __contains__(self, item: StackItem): + self.assert_key_type(item) + return item._val in self._val + + def add(self, item: StackItem) -> 'Set': + self.assert_key_type(item) + if item._val in self._val: + return self + else: + return self.parse(val_expr=[item.val_expr] + self.val_expr, type_expr=self.type_expr) # TODO: sort + def __iter__(self): - key_type = self._get_type(self.type_expr['args'][0]) + key_type, = self.arg_types() for item in self.val_expr: yield key_type(val_expr=item, type_expr=self.type_expr['args'][0]) def assert_key_type(self, item: StackItem): + assert_stack_item(item) assert_expr_equal(self.type_expr['args'][0], item.type_expr) @@ -217,29 +280,26 @@ def empty(cls, k_type_expr, v_type_expr): def new(cls, items: list): assert isinstance(items, list) and len(items) > 0, f'expected non-empty list' _ = [assert_stack_item(x) for kv in items for x in kv] - val_expr = [(k.val_expr, v.val_expr) for k, v in items] + val_expr = [{'prim': 'Elt', 'args': [k.val_expr, v.val_expr]} for k, v in items] k0, v0 = items[0] return cls(val_expr=val_expr, type_expr={'prim': cls.prim, 'args': [k0.type_expr, v0.type_expr]}) def __iter__(self): - kv_types = [self._get_type(self.type_expr['args'][i]) for i in [0, 1]] + arg_types = self.arg_types() for elt in self.val_expr: - yield tuple( - kv_types[i](val_expr=elt['args'][i], type_expr=self.type_expr['args'][i]) - for i in [0, 1]) + yield tuple(arg_types[i](val_expr=elt['args'][i], type_expr=self.type_expr['args'][i]) for i in [0, 1]) - def __getitem__(self, item: StackItem): + def __contains__(self, item: StackItem): self.assert_key_type(item) - if item.value in self.value: - val_expr = next( - elt['args'][1] - for elt in self.val_expr - if expr_equal(elt['args'][0], item.val_expr)) - type_expr = self.type_expr['args'][1] - val_type = self._get_type(type_expr) - return Option.some(val_type(type_expr=type_expr, val_expr=val_expr)) + return item._val in self._val + + def add(self, key: StackItem, val: StackItem): + self.assert_key_type(key) + self.assert_val_type(val) + if key._val in self._val: + return self else: - return Option.none(self.type_expr['args'][1]) + return self.parse(val_expr=self.val_expr + [val.val_expr], type_expr=self.type_expr) # TODO: sort def assert_key_type(self, item: StackItem): assert_stack_item(item) @@ -249,6 +309,9 @@ def assert_val_type(self, item: StackItem): assert_stack_item(item) assert_expr_equal(self.type_expr['args'][1], item.type_expr) + def val_type_expr(self): + return self.type_expr['args'][1] + class BigMap(Map, prim='big_map', args_len=1): @@ -316,6 +379,10 @@ def assert_ret_type(self, item: StackItem): assert_stack_item(item) assert_expr_equal(self.type_expr['args'][1], item.type_expr) + @property + def code(self): + return self._val + # def apply(self, item: StackItem): # p_type_expr = self.type_expr['args'][0] # assert_prim(p_type_expr, prim='pair', args_len=2)