Source

maynard / examples / perth.py

Full commit
#
# Perth.py
# Copyright 2013 by Larry Hastings
#
# Implements a toy FORTH-like language on top
# of the CPython VM.  Uses Maynard for assembly.
#
# todo:
#  if - else - then
#  make sure recursive fn call works
#  fib!

try:
    import maynard
except ImportError:
    import sys
    sys.path.insert(0, "..")
    import __init__ as maynard

def print_one(o):
    print(o, end=' ')

def cr():
    print()

class Perth:

    assembly_map = {
        "+" : "binary_add",
        "-" : "binary_subtract",
        "<" : "compare_op " + str(maynard.Py_LT),
        "<=" : "compare_op " + str(maynard.Py_LE),
        "dup" : "dup_top",
        "2dup" : "dup_top_two",
        "drop" : "pop_top",
        "." : ("load_global print_one", "rot_two", "call_function 1"),
        "cr" : ("load_global cr", "call_function 0"),
    }

    def __init__(self, name):
        self.name = name

        self.immediate_words = {}

        prefix = "immediate_"
        for name in Perth.__dict__:
            if name.startswith(prefix):
                self.immediate_words[name[len(prefix):]] = getattr(self, name)

        # fixups
        fixups = ": colon ; semicolon { left_curly".split()
        while fixups:
            new = fixups.pop(0)
            old = fixups.pop(0)
            self.immediate_words[new] = self.immediate_words[old]
            del self.immediate_words[old]

    @property
    def a(self):
        return self.assembler.assembler or self.assembler

    def al(self, s):
        return self.assembler.assemble_line(s, 1)

    def immediate_colon(self):
        assert not self.compiling
        name = next(self.i)
        self.compiled_words[name] = 0
        self.a.varnames.append(name)
        self.al("def " + name + ":")
        self.compiling = name

    def immediate_semicolon(self):
        assert self.compiling
        self.al("return_value")
        self.al("end")
        self.locals = set()
        self.compiling = None

    def immediate_left_curly(self):
        # syntax for locals:
        # { arg arg2 ... -- output1 output2 ... | local1 local2 ... }
        # the "output" is a comment, it's ignored
        # locals are uninitialised
        # the rightmost arg is at the top of the stack
        assert self.compiling
        keyword = 'arg'
        args = []

        def add_local(name):
            if not keyword:
                return
            if keyword == "arg":
                self.compiled_words[self.compiling] += 1
            self.al(keyword + " " + name)
            self.locals.add(name)

        def add_reversed_args():
            for name in reversed(args):
                add_local(name)

        for word in self.i:
            if word == '}':
                add_reversed_args()
                return
            if word == '|':
                assert keyword in ('arg', '')
                add_reversed_args()
                keyword = 'local'
                continue
            elif word == '--':
                assert keyword == 'arg'
                add_reversed_args()
                keyword = ''
                continue
            if keyword == "arg":
                args.append(word)
                continue

            add_local(word)

    def immediate_store(self):
        name = next(self.i)
        self.al("store " + name)

    def recalculate_jump_offset(self, opcode, offset):
        if opcode == "jump_forward":
            return self.a.bytecode_offset - (offset + 3)
        return self.a.bytecode_offset

    def immediate_if(self):
        opcode = "pop_jump_if_false"
        offset = self.a.bytecode_offset
        self.al(opcode + " 0")
        self.control_stack.append((opcode, offset))

    def immediate_else(self):
        old_top = self.control_stack.pop()

        opcode = "jump_forward"
        offset = self.a.bytecode_offset

        self.al(opcode + " 0")
        self.control_stack.append((opcode, offset))

        opcode, offset = old_top
        delta = self.recalculate_jump_offset(opcode, offset)
        self.a.set_opcode(offset, maynard.base_opcodes[opcode], delta)

    def immediate_then(self):
        opcode, offset = self.control_stack.pop()
        delta = self.recalculate_jump_offset(opcode, offset)
        self.a.set_opcode(offset, maynard.base_opcodes[opcode], delta)


    def immediate_quote(self, word):
        quote = word[0]
        word = word[1:]
        assert quote in ('"', "'")

        stop = False
        text = []
        while True:
            if word.endswith(quote):
                stop = True
                word = word[:-1]
            text.append(word)
            if stop:
                break
            word = next(self.i)
        const = " ".join(text)
        index = self.a.constants.ensure(None, const)
        self.al("load_const " + str(index))

    def execute(self, i):
        """
        i should be an iterator yielding individual tokens,
        separated by whitespace.
        """
        self.i = i

        self.assembler = maynard.Assembler(self.name, 'module')
        self.compiled_words = {}
        self.compiling = None
        self.locals = set()
        self.label_counter = 0

        self.control_stack = []

        def compile(line):
            self.al(line)

        for word in i:
            if word in self.compiled_words:
                argcount = self.compiled_words[word]
                if argcount > 1:
                    compile("build_tuple " + str(argcount))
                # if it's compiled, but not in the child,
                # it's a recursive call, add our own name
                # to the child's global table
                child = self.assembler.assembler
                if child:
                    if child.names.find(word) == -1:
                        child.names.append(word)
                compile("load " + word)
                if argcount > 0:
                    compile("rot_two")
                    if argcount > 1:
                        compile("unpack_sequence " + str(argcount))
                compile("call_function " + str(argcount))
                continue

            assembly = self.assembly_map.get(word, None)
            if assembly:
                if isinstance(assembly, str):
                    compile(assembly)
                else:
                    for line in assembly:
                        compile(line)
                continue

            if word in self.a.names or word in self.a.varnames or word in self.locals:
                compile("load " + word)
                continue

            fn = self.immediate_words.get(word, None)
            if fn:
                fn()
                continue

            try:
                value = int(word)
                name = "const_int_" + str(value)
            except ValueError:
                value = None

            if value == None:
                try:
                    value = float(word)
                    name = "const_float_" + str(value).replace('.', '_')
                except ValueError:
                    value = None

            if word.startswith(("'", '"')):
                self.immediate_quote(word)
                continue

            if value is not None:
                a = self.assembler.assembler or self.assembler
                name = "const_" + str(value)
                index = a.constants.ensure(name, value)
                line = "load_const " + name
                compile(line)
                continue

            if word == 'None':
                name = "const_None"
                index = a.constants.ensure(name, value)
                line = "load_const " + name
                compile(line)
                continue

            sys.exit("unhandled word: " + repr(word))

        self.assembler.ensure_return_none()
        callable = self.assembler.make_callable(globals())
        # maynard.disassemble(callable)
        callable()

if __name__ == "__main__":
    p = Perth('test')
    # program = " : addfour { i } i 4 + ; 5.5 addfour . cr ".split()
    # p.execute(iter(program))

    program = " : yesno { i } i if 'yes' else 'no' then ; 1 yesno . cr 0 yesno . cr ".split()
    p.execute(iter(program))

    program = " : recur { i }  i . i if i 1 - recur then ; 5 recur cr ".split()
    p.execute(iter(program))

    program = " : fib { n } n 1 <= if 1 else n 1 - fib n 2 - fib + then ; : testfib { n }  n dup if 1 - dup testfib drop fib . cr else drop then None ; 10 testfib ".split()
    p.execute(iter(program))