main.py 5.38 KB
import argparse, sys, os, os.path, subprocess

import ply.yacc as yacc
from . import cparse, cvisitors, cx86, cttc, cpp

ARCH = {"x86" : cx86,
        "ttc" : cttc}

ARCHDOC = """supported target architectures:
  x86    Intel 80x86 assembly (in GNU Assemble format)
  ttc    The Tiny Computer assembly
"""

class CompileError (Exception) :
    "Exception raised when there's been a compilation error."
    pass

class Compiler (object) :
    """This object encapsulates the front-end for the compiler and
    serves as a facade interface to the 'meat' of the compiler
    underneath."""
    def __init__ (self, arch, path, lmap, test) :
        self.arch = arch
        self.path = path
        self.lmap = lmap
        self.test = test
        self.total_errors = 0
        self.total_warnings = 0
    def _parse (self) :
        "Parses the source code."
        cparse.inputfile = self.path
        cparse.linenomap = self.lmap
        self.ast = yacc.parse(self.code)
    def _compile_phase (self, visitor) :
        "Applies a visitor to the abstract syntax tree."
        visitor.visit(self.ast)
        self.total_errors += visitor.errors
        self.total_warnings += visitor.warnings
        if visitor.has_errors():
            raise CompileError()
    def _do_compile (self, ast_file) :
        """Compiles the code to the given file object.  Enabling
        show_ast prints out the abstract syntax tree."""
        self._parse()
        self._compile_phase(cvisitors.SymtabVisitor())
        self._compile_phase(cvisitors.TypeCheckVisitor())
        self._compile_phase(cvisitors.FlowControlVisitor())
        comp = self.arch.CodeGenVisitor(self.path, self.test)
        self._compile_phase(comp)
        if ast_file is not None:
            self._compile_phase(cvisitors.ASTPrinterVisitor(ast_file))
        return comp
    def _print_stats (self) :
        "Prints the total number of errors/warnings from compilation."
        if self.total_errors :
            print("%d errors" % self.total_errors)
        if self.total_warnings :
            print("%s warnings" % self.total_warnings)
    def compile (self, code, show_ast) :
        "Compiles the given code string to the given file object."
        self.code = code
        try:
            chunk = self._do_compile(show_ast)
        except cparse.ParseError:
            print("... errors encountered, bailing")
            return
        except CompileError:
            self._print_stats()
            print("... errors encountered, bailing")
            return
        self._print_stats()
        print("... compile successful")
        return chunk

def main (args=None) :
    parser = argparse.ArgumentParser(prog="cct",
                                     epilog=ARCHDOC,
                                     formatter_class=argparse.RawDescriptionHelpFormatter)
    parser.add_argument("-o", action="store", metavar="PATH",
                        type=argparse.FileType('w'), default=sys.stdout,
                        help="write output to PATH")
    parser.add_argument("--ast", action="store_true", default=False,
                        help="dump AST for each C file")
    parser.add_argument("--cpp", "-p", action="store_true", default=False,
                        help="process input file through cpp")
    parser.add_argument("--arch", action="store", choices=list(ARCH), default="ttc",
                        help="output assembly for specified architecture"
                        " (default 'ttc')")
    parser.add_argument("--compile", "-c", action="store_true", default=False,
                        help="compile generated ASM")
    parser.add_argument("--test", "-t", action="store_true", default=False,
                        help="test compiler")
    parser.add_argument("source", nargs="+", metavar="PATH",
                        help="C source files(s) to compile")
    args = parser.parse_args(args)
    chunks = []
    if args.test :
        print("Generating tests")
        print("..$ gcc -o cct-test -D CC %s" % " ".join(args.source))
        subprocess.run(["gcc", "-o", "cct-test", "-D", "CC"] + args.source)
        print("..$ cct-test")
        testdata = subprocess.check_output("cct-test").decode()
        args.test = dict(line.rsplit(" => ", 1) for line in testdata.splitlines())
    for src in args.source :
        if args.ast :
            ast_path = os.path.splitext(src)[0] + ".ast"
            print("Dumping AST to %r" % ast_path)
            ast_file = open(ast_path, "w")
        else :
            ast_file = None
        if args.cpp :
            print("Preprocessing %r" % src)
            cppargs = ARCH[args.arch].cpp(args)
            print("..$ cpp %s %s" % (" ".join(cppargs), src))
            code, lmap = cpp.cpp(src, args.arch, cppargs)
            print("Compiling preprocessed %r" % src)
        else :
            code = "\n".join(l.rstrip() for l in open(src).readlines())
            print("Compiling %r" % src)
        ret = Compiler(ARCH[args.arch], src, lmap, args.test).compile(code, ast_file)
        if ast_file is not None :
            ast_file.close()
        if ret is None :
            sys.exit(1)
        chunks.append(ret)
    ARCH[args.arch].CodeGenVisitor.concat(args.o, chunks)
    args.o.flush()
    if args.compile :
        tgt, cmd = ARCH[args.arch].compile(args.o.name)
        print("Building %r" % tgt)
        print(".. $ %s" % " ".join(cmd))
        ret = subprocess.run(cmd)
        print("... exited with status %s" % ret.returncode)