package main
import (
"fmt"
"io"
"os"
"os/exec"
"path"
"strings"
"github.com/marzeq/quokka/codegen"
"github.com/marzeq/quokka/modules"
"github.com/marzeq/quokka/parser"
"github.com/marzeq/quokka/shared"
"github.com/marzeq/quokka/tokeniser"
"github.com/marzeq/quokka/typechecker"
)
func _check(err error) {
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
func usage() {
prog := path.Base(os.Args[0])
fmt.Printf("Usage: %s -o <output file> [-lib] [-lf ...] <src files>\n", prog)
}
func runCmd(args ...string) error {
if len(args) == 0 {
return nil
}
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer func() {
_ = out.Close()
}()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Sync()
}
func parseArgs() (
outfile string,
linkerFlags []string,
sources []string,
irOutput string,
asmOutput string,
buildDir string,
) {
args := os.Args[1:]
for i := 0; i < len(args); i++ {
arg := args[i]
if len(arg) > 2 && arg[0] == '-' && arg[1] == '-' {
arg = arg[1:]
}
switch arg {
case "-o", "-output", "-out":
if i+1 >= len(args) {
fmt.Println("missing argument for output file")
os.Exit(1)
}
outfile = args[i+1]
i++
case "-lf", "-lflags", "-linkerflags":
if i+1 >= len(args) {
fmt.Println("missing argument for linker flags")
}
linkerFlags = append(linkerFlags, args[i+1])
i++
case "-h", "-help":
usage()
os.Exit(0)
// flags for debugging, do not document
case "-iro", "-irout", "-iroutput":
if i+1 >= len(args) {
fmt.Println("missing argument for ir output file")
}
irOutput = args[i+1]
i++
case "-asmo", "-asmout", "-asmoutput":
if i+1 >= len(args) {
fmt.Println("missing argument for asm output file")
}
asmOutput = args[i+1]
i++
case "-bd", "-builddir":
if i+1 >= len(args) {
fmt.Println("missing argument for build dir")
}
buildDir = args[i+1]
i++
default:
if len(arg) > 0 && arg[0] == '-' {
fmt.Printf("unknown argument: %s\n", arg)
os.Exit(1)
}
sources = append(sources, args[i])
}
}
if len(sources) == 0 {
usage()
os.Exit(1)
}
return
}
func main() {
outFile, linkerFlags, sources, irOutput, asmOutput, buildDir := parseArgs()
if len(sources) != 1 {
fmt.Println("for now, only one source file is supported")
os.Exit(1)
}
srcFile := sources[0]
outBaseName := strings.TrimSuffix(path.Base(outFile), path.Ext(outFile))
pathExt := path.Ext(outFile)
var tmpDir string
if buildDir == "" {
td, err := os.MkdirTemp("", "quokka_build_*")
_check(err)
tmpDir = td
defer os.RemoveAll(tmpDir)
} else {
tmpDir = buildDir
os.MkdirAll(tmpDir, 0755)
}
t, err := tokeniser.NewTokeniserFromFile(srcFile)
_check(err)
toks, err := t.Tokenise()
_check(err)
p := parser.NewParser(toks)
ast, err := p.Parse()
_check(err)
dg, err := modules.NewDepGraph(srcFile, ast)
_check(err)
files, err := dg.TopoSort()
_check(err)
ms := make(typechecker.ModulesSignatures)
filesToMods := make(map[string]string)
for _, f := range files {
mod, err := ms.CollectSignaturesFromRootNode(dg.ASTs[f])
_check(err)
filesToMods[f] = mod
}
for f, mod := range filesToMods {
tc := typechecker.NewTypeChecker(mod, ms)
ast, err := tc.TypeCheck(dg.ASTs[f])
_check(err)
dg.ASTs[f] = ast
}
ir := ""
for f, mod := range filesToMods {
cg := codegen.NewCodeGen(dg.ASTs[f], mod, ms)
fileIr, err := cg.EmitIR()
_check(err)
ir += fileIr + "\n"
}
switch pathExt {
case "":
mainSig, ok := ms.LookupFunction("", "main", "")
if !ok {
fmt.Println("no main function found in the default module")
os.Exit(1)
}
if len(mainSig.ArgTypes) != 0 {
fmt.Println("main function must take no arguments")
os.Exit(1)
}
if !mainSig.RetType.Compare(shared.PRIMITIVE_VOID) {
fmt.Println("main function must return void")
os.Exit(1)
}
ir += "\nexport function w $main() {\n"
ir += "@start\n"
ir += " call $_main()\n"
ir += " ret 0\n"
ir += "}\n"
}
ssaPath := path.Join(tmpDir, outBaseName+".ssa")
_check(os.WriteFile(ssaPath, []byte(ir), 0644))
if irOutput != "" {
_check(copyFile(ssaPath, irOutput))
}
sPath := path.Join(tmpDir, outBaseName+".s")
_check(runCmd("qbe", "-o", sPath, ssaPath))
if asmOutput != "" {
_check(copyFile(sPath, asmOutput))
}
oPath := path.Join(tmpDir, outBaseName+".o")
_check(runCmd("cc", "-c", "-o", oPath, sPath))
switch pathExt {
case ".o":
_check(copyFile(oPath, outFile))
case ".a":
_check(runCmd("ar", "rcs", outFile, oPath))
case ".so":
args := append([]string{"cc", "-shared", "-o", outFile, oPath}, linkerFlags...)
_check(runCmd(args...))
case "":
if outFile == "" {
outFile = "a.out"
}
args := append([]string{"cc", "-o", outFile, oPath}, linkerFlags...)
_check(runCmd(args...))
default:
fmt.Printf("unknown output file extension: %s\n", pathExt)
}
}