package main
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/marzeq/qk/codegen/llvm"
"github.com/marzeq/qk/ir"
"github.com/marzeq/qk/loader"
"github.com/marzeq/qk/parser"
"github.com/marzeq/qk/sema"
"github.com/marzeq/qk/shared"
"github.com/marzeq/qk/tokeniser"
"github.com/marzeq/qk/types"
)
func main() {
args, err := parseArgs()
check(err)
searchPaths := buildSearchPaths(args.baseDir)
files, err := collectSourceFiles(searchPaths, args.excludeDirs)
check(err)
if len(files) == 0 {
fatal("no source files found")
}
var partials []*loader.PartialModuleInfo
for _, file := range files {
ast, err := parseFile(file)
check(err)
info, err := loader.CollectModuleInfo(ast)
check(err)
partials = append(partials, info)
}
if args.verbose {
fmt.Println("parsed and collected modules")
}
modules, err := loader.BuildModules(partials)
check(err)
analyser := sema.NewAnalyser()
order, errs := loader.ComputeModuleOrder(modules)
checkErrs(errs)
errs = loader.RunSemanticPipeline(modules, analyser, order, args.verbose, args.debug)
checkErrs(errs)
if args.verbose {
fmt.Println("semantic analysis completed successfully")
}
irModules, errs := loader.GenerateIRModules(modules, args.mainModule, order, args.verbose)
checkErrs(errs)
llvmOutputs := buildLLVMModules(irModules, args.mainModule, order, args.outputType == OutputExecutable)
if args.dumpIR {
dumpIRModules(irModules)
}
if args.dumpLLVM {
dumpLLVMModules(llvmOutputs, order)
}
if _, ok := modules[args.mainModule]; !ok {
fatal("main module (%s) not found", args.mainModule)
}
foundMain := false
mainModule := modules[args.mainModule]
for _, root := range mainModule.Roots {
for _, stmt := range root.Body {
switch fn := stmt.(type) {
case *parser.FunctionDefNode:
if fn.Name == "main" {
if len(fn.Args) != 0 {
fatal("%v", shared.NewError(fn.Loc, "main function must not have arguments"))
}
if fn.Body == nil {
fatal("%v", shared.NewError(fn.Loc, "main function must have a body"))
}
if fn.Symbol.Signature.ReturnType != types.PrimitiveVoid {
fatal("%v", shared.NewError(fn.Loc, "main function must return void"))
}
foundMain = true
break
}
}
}
}
if !foundMain && args.outputType == OutputExecutable {
fatal("main function not found in main module (%s)", args.mainModule)
}
buildDir, err := emitLLVMFiles(llvmOutputs, order)
check(err)
if !args.keepBuildDir {
defer os.RemoveAll(buildDir)
}
if args.verbose {
fmt.Printf("emitted LLVM files to %s\n", buildDir)
}
objFiles, err := compileLLVMModules(buildDir, order, args.optLevel, args.verbose, args.target, args.sysroot, args.clangArgs)
check(err)
stat, err := os.Stat(args.output)
switch {
case os.IsNotExist(err):
// output file doesn't exist -> good
case err != nil:
fatal("failed to stat %q: %v", args.output, err)
case stat.IsDir():
fatal("output file %s is an existing directory", args.output)
default:
if err := os.Remove(args.output); err != nil {
fatal("failed to remove existing output file %q: %v", args.output, err)
}
}
os.Remove(args.output)
err = linkObjects(objFiles, args.output, args.outputType, args.static, args.verbose, args.target, args.sysroot, args.linkArgs)
check(err)
if args.keepBuildDir {
fmt.Printf("kept build directory: %s\n", buildDir)
}
}
func parseFile(path string) (*parser.RootNode, error) {
t, err := tokeniser.NewTokeniserFromFile(path)
if err != nil {
return nil, err
}
toks, err := t.Tokenise()
if err != nil {
return nil, err
}
p := parser.NewParser(toks)
return p.Parse()
}
func collectSourceFiles(paths []string, exclude []string) ([]string, error) {
seen := map[string]struct{}{}
excluded := map[string]struct{}{}
for _, e := range exclude {
abs, _ := filepath.Abs(e)
excluded[abs] = struct{}{}
}
var files []string
for _, root := range paths {
info, err := os.Stat(root)
if err != nil || !info.IsDir() {
continue
}
filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
abs, _ := filepath.Abs(path)
for ex := range excluded {
if strings.HasPrefix(abs, ex) {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}
}
if !d.IsDir() && filepath.Ext(path) == ".qk" {
if _, ok := seen[abs]; !ok {
seen[abs] = struct{}{}
files = append(files, abs)
}
}
return nil
})
}
return files, nil
}
func buildSearchPaths(baseDir string) []string {
var paths []string
paths = append(paths, baseDir)
home, err := os.UserHomeDir()
if err == nil {
paths = append(paths,
filepath.Join(home, ".local", "share", "qk"),
)
}
paths = append(paths, filepath.Join("/usr", "local", "lib", "qk"))
paths = append(paths, filepath.Join("/usr", "lib", "qk"))
return paths
}
func check(err error) {
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func checkErrs(errs []error) {
if len(errs) > 0 {
for _, err := range errs {
fmt.Println(err)
}
os.Exit(1)
}
}
func fatal(format string, args ...any) {
fmt.Printf(format+"\n", args...)
os.Exit(1)
}
func buildLLVMModules(mods map[string]*ir.Module, mainModule string, order []string, isExecutable bool) map[string]string {
outputs := make(map[string]string, len(mods))
for _, name := range order {
mod := mods[name]
if mod == nil {
continue
}
emitter := &llvm.Emitter{ModuleName: name, Executable: isExecutable, MainModule: mainModule}
var output strings.Builder
emitter.EmitModule(&output, mod)
outputs[name] = output.String()
}
return outputs
}
func dumpLLVMModules(mods map[string]string, order []string) {
for i, name := range order {
output, ok := mods[name]
if !ok {
continue
}
if i > 0 {
fmt.Println()
}
fmt.Print(output)
if !strings.HasSuffix(output, "\n") {
fmt.Println()
}
}
}
func emitLLVMFiles(mods map[string]string, order []string) (string, error) {
buildDir, err := os.MkdirTemp("/tmp", "qk-build-")
if err != nil {
return "", err
}
for _, name := range order {
output, ok := mods[name]
if !ok {
continue
}
llPath := filepath.Join(buildDir, safeModuleFileName(name)+".ll")
if err := os.WriteFile(llPath, []byte(output), 0o644); err != nil {
return "", err
}
}
return buildDir, nil
}
func compileLLVMModules(buildDir string, order []string, optLevel OptimisationLevel, verbose bool, target string, sysroot string, extraClangArgs []string) ([]string, error) {
objFiles := make([]string, 0, len(order))
for _, name := range order {
llPath := filepath.Join(buildDir, safeModuleFileName(name)+".ll")
if _, err := os.Stat(llPath); err != nil {
if os.IsNotExist(err) {
continue
}
return nil, err
}
objPath := filepath.Join(buildDir, safeModuleFileName(name)+".o")
clangArgs := []string{"-c", llPath, "-o", objPath, fmt.Sprintf("-O%s", optLevel)}
if target != "" {
clangArgs = append([]string{"-target", target}, clangArgs...)
}
if sysroot != "" {
clangArgs = append(clangArgs, "--sysroot="+sysroot)
}
if len(extraClangArgs) > 0 {
clangArgs = append(clangArgs, extraClangArgs...)
}
if verbose {
fmt.Printf("> clang %s\n", strings.Join(clangArgs, " "))
}
cmd := exec.Command("clang", clangArgs...)
out, err := cmd.CombinedOutput()
if err != nil {
return nil, fmt.Errorf("clang failed for module %q: %w\n%s", name, err, string(out))
}
objFiles = append(objFiles, objPath)
}
if len(objFiles) == 0 {
return nil, fmt.Errorf("no object files were produced")
}
return objFiles, nil
}
func linkObjects(objFiles []string, output string, outputType OutputType, static, verbose bool, target string, sysroot string, extraLinkArgs []string) error {
args, err := buildLinkArgs(objFiles, output, outputType, static, target, sysroot, extraLinkArgs)
if err != nil {
return err
}
if verbose {
fmt.Printf("> clang %s\n", strings.Join(args, " "))
}
cmd := exec.Command("clang", args...)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("linking failed: %w\n%s", err, string(out))
}
return nil
}
func buildLinkArgs(objFiles []string, output string, outputType OutputType, static bool, target string, sysroot string, extraLinkArgs []string) ([]string, error) {
args := append([]string{}, objFiles...)
switch outputType {
case OutputExecutable:
case OutputObject:
args = append(args, "-r")
case OutputSharedLib:
if static {
return nil, fmt.Errorf("cannot use --static with shared lib output")
}
args = append(args, "-shared", "-fPIC")
default:
return nil, fmt.Errorf("unknown output type")
}
if static {
args = append(args, "-static")
}
if sysroot != "" {
args = append([]string{"--sysroot=" + sysroot}, args...)
}
if target != "" {
args = append([]string{"-target", target}, args...)
}
if len(extraLinkArgs) > 0 {
args = append(args, extraLinkArgs...)
}
args = append(args, "-o", output)
args = append(args, "-fuse-ld=lld")
return args, nil
}
func safeModuleFileName(name string) string {
if name == "" {
return "module"
}
var b strings.Builder
b.Grow(len(name))
for i, r := range name {
valid := (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || (r >= '0' && r <= '9')
if !valid {
b.WriteByte('_')
continue
}
if i == 0 && r >= '0' && r <= '9' {
b.WriteByte('_')
}
b.WriteRune(r)
}
if b.Len() == 0 {
return "module"
}
return b.String()
}