Exemplo n.º 1
0
def get_imported_declarations(base_path, imports, imported=None):
    """Given a list of imports and a list of paths to search for them in,
    return a list of declarations containing the imported declarations. The
    returned list of declarations can then be prepended to the the declarations
    in the input file. This function will recursively import if required.
    
    :param base_file str: the absolute path to the file we are currently
                          compiling
    :param imports [str]: a list of imports -- these are relative paths to
                          .fky files
    :return:              a list of declarations from the imported files
    """
    log.info("Finding declarations for imports '{}' relative to {}.".format(
        ", ".join(imports), base_path))

    if imported is None:
        imported = set([base_path])
    else:
        log.debug("'{}' already imported.".format(", ".join(imported)))
        imported.add(base_path)

    base_path = os.path.abspath(base_path)  # just in case...
    if not os.path.isdir(base_path):
        base_dir = os.path.dirname(base_path)
    else:
        log.debug(
            "Base path is a directory; you are probably running the REPL.")
        base_dir = base_path

    decls = []

    log.debug("Building parser to read imported source files...")
    parser = FunkyParser()
    parser.build()
    log.debug("Done building parser.")

    def do_import(base_dir, imp):
        filename = search_for_import(imp, [base_dir] + SEARCH_PATHS)
        if filename in imported: return
        imported.add(filename)

        with open(filename, "r") as f:
            source = f.read()

        parsed = parser.do_parse(source)
        for sub_imp in parsed.body.imports:
            log.debug("Found more imports, handling them.")
            new_base_dir = os.path.dirname(filename)
            do_import(new_base_dir, sub_imp)

        decls.extend(parsed.body.toplevel_declarations)

    for imp in imports:
        do_import(base_dir, imp)

    log.info("Found all imported declarations.")
    return decls
Exemplo n.º 2
0
    def test_sanity(self):
        sanity_fails = [
            """
module test with

    tau  = 2 * pi
            """,
            """
module test with
    
    f 0 0 = 0
    f x y z = 1
            """,
            """
module test with
    
    f x x = 1
            """,
            """
module test with
    
    to_int = 10
            """,
            """
module test with
    
    to_str = 10
            """,
            """
module test with
    
    to_float = 10
            """,
            """
module test with
    newtype Test = P Integer | P Float
            """,
            """
module test with
    newtype Test  = P Integer
    newtype Test2 = P Float
            """,
            """
module test with
    x = 2
    x = 3
            """,
        ]

        parser = FunkyParser()
        parser.build()
        for test in sanity_fails:
            ast = parser.do_parse(test)
            with self.assertRaises(FunkyRenamingError):
                do_rename(ast)
Exemplo n.º 3
0
    def __init__(self, lazy=False):
        start = time.time()
        super().__init__()

        self.intro   =  cgreen("\nfunkyi ({}) repl".format(__version__)) + \
                        "\nReady!\nFor help, use the ':help' command.\n"
        self.prompt  =  cyellow("funkyi> ")

        log.debug("Lazy mode {}.".format("enabled" if lazy else "disabled"))
        if lazy:
            print(cblue("Lazy evalation enabled."))
        else:
            print(cblue("Lazy evalation disabled."))

        # create the various parsers:
        log.debug("Creating required parsers...")
        self.decl_parser = FunkyParser()
        self.decl_parser.build(start="TOPLEVEL_DECLARATIONS")
        self.expr_parser = FunkyParser()
        self.expr_parser.build(start="EXP")
        self.newtype_parser = FunkyParser()
        self.newtype_parser.build(start="ADT_DECLARATION")
        self.setfix_parser = FunkyParser()
        self.setfix_parser.build(start="FIXITY_DECLARATION")
        self.import_parser = FunkyParser()
        self.import_parser.build(start="IMPORT_STATEMENT")

        if lazy:
            log.debug("Using lazy code generator for REPL.")
            self.py_generator = LazyPythonCodeGenerator()
        else:
            log.debug("Using strict code generator for REPL.")
            self.py_generator = StrictPythonCodeGenerator()

        log.debug("Done creating parsers.")

        self.reset()

        end = time.time()
        print(cblue("Startup completed ({0:.3f}s).".format(end - start)))
Exemplo n.º 4
0
    def test_invalid_parse(self):
        invalid_programs = [
            """
module invalid with

    main = f x + + 2
            """,

            """
module invalid some rubbish with 
    nothing
            """,

            """
module invalid with

    n = * 5 2 # '*' needs to be '(*)' for this to be valid!
            """,

            """
module invalid with
    10 + 10"""
            ]

        parser = FunkyParser()
        parser.build()
        for invalid_program in invalid_programs:
            with self.assertRaises(FunkySyntaxError):
                parser.do_parse(invalid_program)
Exemplo n.º 5
0
def compiler_lex_and_parse(source, dump_pretty, dump_lexed, dump_parsed):
    """Lexes and parses the source code to get the syntax tree.
    
    :param source str:       the Funky source code
    :param dump_pretty bool: if true, dump the syntax-highlighted, prettified
                             code to stdout
    :param dump_lexed bool:  if true, dump the lexed code to stdout
    :param dump_parsed bool: if true, dump the syntax tree to stdout
    """
    # lex and parse code
    parser = FunkyParser()
    parser.build(dump_pretty=dump_pretty, dump_lexed=dump_lexed)
    # lexing is done in the same step as parsing -- so we have to tell the
    # parser whether we want the lexer's output to be displayed
    parsed = parser.do_parse(source)
    if dump_parsed:
        print(cblue("## DUMPED PARSE TREE"))
        print(parsed)
        print("")

    log.info("Parsing source code completed.")
    return parsed
Exemplo n.º 6
0
class FunkyShell(CustomCmd):

    def __init__(self, lazy=False):
        start = time.time()
        super().__init__()

        self.intro   =  cgreen("\nfunkyi ({}) repl".format(__version__)) + \
                        "\nReady!\nFor help, use the ':help' command.\n"
        self.prompt  =  cyellow("funkyi> ")

        log.debug("Lazy mode {}.".format("enabled" if lazy else "disabled"))
        if lazy:
            print(cblue("Lazy evalation enabled."))
        else:
            print(cblue("Lazy evalation disabled."))

        # create the various parsers:
        log.debug("Creating required parsers...")
        self.decl_parser = FunkyParser()
        self.decl_parser.build(start="TOPLEVEL_DECLARATIONS")
        self.expr_parser = FunkyParser()
        self.expr_parser.build(start="EXP")
        self.newtype_parser = FunkyParser()
        self.newtype_parser.build(start="ADT_DECLARATION")
        self.setfix_parser = FunkyParser()
        self.setfix_parser.build(start="FIXITY_DECLARATION")
        self.import_parser = FunkyParser()
        self.import_parser.build(start="IMPORT_STATEMENT")

        if lazy:
            log.debug("Using lazy code generator for REPL.")
            self.py_generator = LazyPythonCodeGenerator()
        else:
            log.debug("Using strict code generator for REPL.")
            self.py_generator = StrictPythonCodeGenerator()

        log.debug("Done creating parsers.")

        self.reset()

        end = time.time()
        print(cblue("Startup completed ({0:.3f}s).".format(end - start)))

    @report_errors
    @atomic
    def do_begin_block(self, arg):
        """Start a block of definitions."""
        block_prompt = cyellow("block>  ")
        end_block = ":end_block" # <- type this to end the block
        lines = []
        try:
            while True:
                inp = input(block_prompt)
                if inp:
                    if inp == end_block:
                        break
                    lines.append(" " + inp)
        except KeyboardInterrupt:
            print("^C\n{}".format(cred("Cancelled block.")))
            return
        self.parse_and_add_declarations(lines)

    @report_errors
    @atomic
    @needs_argument
    def do_type(self, arg):
        """Show the type of an expression. E.g.: :type 5"""
        expr = self.get_core(arg)
        self.global_let.expr = expr
        do_type_inference(self.global_let, self.global_types)
        print("{} :: {}".format(arg, self.global_let.inferred_type))

    @needs_argument
    def do_lazy(self, arg):
        """Use ':lazy on' to use lazy evaluation and ':lazy off' to use strict evaluation."""
        SWAPPED = "Swapped to {} code generator.".format
        ALREADY = "Already using {} code generator; ignoring.".format
        if arg.lower() == "on":
            log.debug("Now using lazy code generator for REPL.")
            if isinstance(self.py_generator, StrictPythonCodeGenerator):
                self.py_generator = LazyPythonCodeGenerator()
                print(cgreen(SWAPPED("lazy")))
            else:
                print(ALREADY("lazy"))
        elif arg.lower() == "off":
            log.debug("Now using strict code generator for REPL.")
            if isinstance(self.py_generator, LazyPythonCodeGenerator):
                self.py_generator = StrictPythonCodeGenerator()
                print(cgreen(SWAPPED("strict")))
            else:
                print(ALREADY("strict"))
        else:
            print(cred("Invalid option '{}'. Please specify 'on' or "
                       "'off'.".format(arg)))

    @needs_argument
    def do_color(self, arg):
        """Use ':color on' to enable colors and ':color off' to disable them."""
        SWAPPED = "Turned colors {}.".format
        ALREADY = "Colors are already {}.".format
        
        if arg.lower() == "on":
            log.debug("Enabling colors.")
            if not funky.globals.USE_COLORS:
                funky.globals.USE_COLORS = True
                print(cgreen(SWAPPED("on")))
            else:
                print(ALREADY("on"))
        elif arg.lower() == "off":
            log.debug("Disabling colors.")
            if funky.globals.USE_COLORS:
                funky.globals.USE_COLORS = False
                print(cgreen(SWAPPED("off")))
            else:
                print(ALREADY("off"))
        else:
            print(cred("Invalid option '{}'. Please specify 'on' or "
                       "'off'.".format(arg)))

    @needs_argument
    def do_unicode(self, arg):
        """Use ':unicode on' to enable unicode characters and ':unicode off' to disable them."""
        SWAPPED = "Unicode printing is now {}.".format
        ALREADY = "Unicode printing is already {}.".format
        
        if arg.lower() == "on":
            log.debug("Enabling unicode printing.")
            if not funky.globals.USE_UNICODE:
                funky.globals.USE_UNICODE = True
                print(cgreen(SWAPPED("on")))
            else:
                print(ALREADY("on"))
        elif arg.lower() == "off":
            log.debug("Disabling unicode printing.")
            if funky.globals.USE_UNICODE:
                funky.globals.USE_UNICODE = False
                print(cgreen(SWAPPED("off")))
            else:
                print(ALREADY("off"))
        else:
            print(cred("Invalid option '{}'. Please specify 'on' or "
                       "'off'.".format(arg)))

    def do_list(self, arg):
        """List the current bindings in desuguared intermediate code."""
        if not (self.global_types or self.global_let.binds):
            print(cgreen("Nothing currently registered."))
            return

        print(cgreen("Currently registered bindings:"))
        if self.global_types:
            print("\n".join(str(b) for b in self.global_types))
        if self.global_let.binds:
            print("\n".join(str(b) for b in self.global_let.binds))

    def do_binds(self, arg):
        """List the available bindings."""
        if not self.scope.local:
            print("No bindings.")
            return

        print(cgreen("Available bindings:"))
        self.scope.pprint_local_binds()

    @report_errors
    @atomic
    @needs_argument
    def do_newtype(self, arg):
        """Create an ADT. E.g.: :newtype List = Cons Integer List | Nil"""
        parsed = self.newtype_parser.do_parse("newtype {}".format(arg))
        self.add_typedefs([parsed])

    @report_errors
    @atomic
    @needs_argument
    def do_show(self, arg):
        """Show the compiled code for an expression. E.g.: :show 1 + 1"""
        code = self.get_compiled(arg)
        print(code)

    @report_errors
    @needs_argument
    def do_setfix(self, arg):
        """Change the fixity of an operator. E.g.: :setfix leftassoc 8 **"""
        self.setfix_parser.do_parse("setfix {}".format(arg))

    @report_errors
    @atomic
    @needs_argument
    def do_import(self, arg):
        """Import a .fky file into the REPL. E.g.: :import "stdlib.fky"."""
        start = time.time()
        try:
            import_stmt = self.import_parser.do_parse("import {}".format(arg))

            cwd = os.path.abspath(os.getcwd())
            imports_source = get_imported_declarations(cwd, [import_stmt],
                                                       imported=self.imported)

            typedefs, code = split_typedefs_and_code(imports_source)

            self.add_typedefs(typedefs)
            self.add_declarations(code)

            end = time.time()
            print(cgreen("Successfully imported {0} ({1:.3f}s).".format(arg,
                                                                        end - start)))
        except FunkyError as e:
            # if an error occurs, extend the error message to tell the user
            # that the error occurred while importing *this* file.
            new_msg = "{} (while importing {})".format(e.args[0], arg)
            e.args = (new_msg,) + e.args[1:]
            raise

    @needs_argument
    def do_typeclass(self, arg):
        """Prints a quick summary of a typeclass."""
        try:
            typeclass = TYPECLASSES[arg]
            print(arg, typeclass.constraints_str())
        except KeyError:
            print(cred("Typeclass '{}' does not exist.".format(arg)))

    def do_reset(self, arg):
        """Reset the environment (clear the current list of bindings)."""
        self.reset()
        print(cgreen("All bindings reset."))

    def reset(self):
        """Resets the scope and bindings."""
        self.imported = set([])
        self.scope = Scope()

        # global_types is the collection of user-defined type declarations.
        self.global_types = []
        # global_let is a core let whose bindings are just the bindings the
        # user has introduced, and whose expression is 'dynamic' -- it is
        # changed each time the user asks for an expression to be evaluated and
        # recompiled as a new program to give the new result.
        self.global_let = CoreLet([], CoreLiteral(0))

    def get_state(self):
        return (
            copy.copy(self.imported),
            copy.deepcopy(self.scope),
            copy.deepcopy(self.global_types),
            copy.deepcopy(self.global_let)
        )

    def revert_state(self, state):
        self.imported, self.scope, self.global_types, self.global_let = state

    def get_core(self, source):
        """Converts a string of Funky code into the intermediate language.

        :param source: the source code to convert to the intermediate language
        :return:       the core code
        """
        parsed = self.expr_parser.do_parse(source)

        rename(parsed, self.scope)
        check_scope_for_errors(self.scope)
        core_tree, _ = do_desugar(parsed)
        return core_tree

    def get_compiled(self, source):
        """Converts a string of funky code into the target source language.

        :param source: the source code to convert to the target source language
        :return:       the compiled code in the target source language
        """
        core_expr = self.get_core(source)
        self.global_let.expr = core_expr
        do_type_inference(self.global_let, self.global_types)

        target_source = self.py_generator.do_generate_code(self.global_let,
                                                           self.global_types)
        return target_source

    def parse_and_add_declarations(self, lines):
        """Add a new block of declarations to global_let.

        :param lines: the Funky source code lines in the new block of
                      declarations
        """
        # shoehorn the lines into a 'fake' with clause so that they can be
        # parsed correctly.
        parsed = self.decl_parser.do_parse("func = 0 with\n{}".format(
                                        "\n".join(lines)))

        # extract the parsed declarations back out from our 'fake' with clause.
        declarations = parsed[0].expression.declarations

        self.add_declarations(declarations)

    def add_typedefs(self, typedefs):
        for typedef in typedefs:
            rename(typedef, self.scope)
            typedef = desugar(typedef)
            self.global_types.append(typedef)

    def add_declarations(self, declarations):
        self.global_let.expr = CoreLiteral(0)

        # rename and desugar each declaration one-by-one, and append each to
        # the (new_) global_let binds
        for decl in declarations:
            rename(decl, self.scope)
            core_tree, _ = do_desugar(decl)
            self.global_let.binds.append(core_tree)

        check_scope_for_errors(self.scope)
        self.global_let.binds = condense_function_binds(self.global_let.binds)
        self.global_let.create_dependency_graph()

        # type infer the new global let to check for inconsistencies
        do_type_inference(self.global_let, self.global_types)

    def do_EOF(self, line):
        """Exit safely."""
        print("^D\nEOF, exiting.")
        exit(0)

    @report_errors
    @atomic
    def default(self, arg):
        """This is called when the user does not type in any command in
        particular. Since this is a REPL, we should interpret the given
        text as an expression or a declaration
        """

        # if there are comments, drop them
        try:
            arg = arg[:arg.index("#")]
            if not arg:
                return
        except ValueError:
            pass

        try:
            # try treating as an expression...
            code = self.get_compiled(arg)
            print(cblue("= "), end="")
            try:
                exec(code, {"__name__" : "__main__"})
            except Exception as e:
                print(cred(str(e)))
        except FunkyParsingError:
            # if that didn't work, try treating as a declaration
            self.parse_and_add_declarations([arg])

    def emptyline(self):
        """Empty lines in the REPL do nothing."""
        pass
Exemplo n.º 7
0
    def test_valid_parse(self):
        valid_programs = ["""
module test with

    main = 10 + 10
        """,

        """
module test with

    factorial 0 = 1
    factorial n = n * factorial (n - 1)
    main = factorial 5""",
    
    """
module test with

    area r = pi * r ** 2.0
             with pi = 3.141
    """,
    
    """
# Treesort algorithm for integer in Funky.
module treesort with

    import "stdlib.fky"
    import "intlist.fky"

    newtype Tree = Branch Tree Integer Tree | Empty

    insert e Empty                          = Branch Empty e Empty
    insert e (Branch l v r) given e <= v    = Branch (insert e l) v r
                            given otherwise = Branch l v (insert e r)
    
    inorder Empty = Nil
    inorder (Branch l v r) = inorder l ~concatenate~
                             unit v    ~concatenate~
                             inorder r

    treesort list = inorder (foldr insert Empty list)

    main = treesort my_list
           with my_list = Cons 5 (Cons 1 (Cons 7 (Cons 7 (Cons 3 Nil))))
    """,
    
    """
# Representing and evaluating expression trees in Funky.
module expressiontree with

    newtype Expression = Const Float
                       | BinOp Expression
                               (Expression -> Expression -> Float)
                               Expression
                       | UnOp  (Expression -> Float)
                               Expression

    evaluate (Const x)     = x
    evaluate (BinOp a f b) = f a b
    evaluate (UnOp f x)    = f x

    add  exp1  exp2  =  (evaluate  exp1)  +  (evaluate  exp2)
    sub  exp1  exp2  =  (evaluate  exp1)  -  (evaluate  exp2)
    mul  exp1  exp2  =  (evaluate  exp1)  *  (evaluate  exp2)
    div  exp1  exp2  =  (evaluate  exp1)  /  (evaluate  exp2)

    test_exp = (BinOp (Const 5.0) add (BinOp (Const 3.0) div (Const 2.0)))

    main = evaluate test_exp
    """,
    
    """
# Demonstrating the use of Funky's random library to generate a random float.
module randomfloat with

    import "random.fky"

    seed = 51780

    main = randfloat seed
    """]

        parser = FunkyParser()
        parser.build()
        for valid_program in valid_programs:
            ast = parser.do_parse(valid_program)
            self.assertTrue(isinstance(ast, ASTNode))
Exemplo n.º 8
0
from unittest import TestCase

import funky.globals

from funky.parse.funky_parser import FunkyParser
from funky.rename.rename import do_rename
from funky.desugar.desugar import do_desugar
from funky.infer.infer import do_type_inference
from funky.infer import FunkyTypeError

parser = FunkyParser()
parser.build()


def get_type_str(source):
    ast = parser.do_parse(source)
    # NOTE: we do not include imports for simplicity :)
    do_rename(ast)
    core_tree, typedefs = do_desugar(ast)
    do_type_inference(core_tree, typedefs)

    return str(core_tree.inferred_type)


class TestBasicInference(TestCase):
    def setUp(self):
        funky.globals.USE_UNICODE = False
        funky.globals.USE_COLOR = False

    def test_basic_type_inference(self):
        tests = {