fspl/compiler/compiler.go

385 lines
9.9 KiB
Go

package compiler
import "io"
import "os"
import "fmt"
import "sort"
import "errors"
import "os/exec"
import "path/filepath"
import "github.com/google/uuid"
import "git.tebibyte.media/fspl/fspl/llvm"
import "git.tebibyte.media/fspl/fspl/lexer"
import "git.tebibyte.media/fspl/fspl/entity"
import "git.tebibyte.media/fspl/fspl/analyzer"
import "git.tebibyte.media/fspl/fspl/parser/fspl"
import "git.tebibyte.media/fspl/fspl/parser/meta"
import ferrors "git.tebibyte.media/fspl/fspl/errors"
import "git.tebibyte.media/fspl/fspl/generator/native"
type Compiler struct {
Resolver
Output string
Optimization string
Format string
Stderr io.Writer
Debug bool
Quiet bool
}
func (this *Compiler) CompileUnit (address entity.Address) error {
this.Debugln("using search path", this.Resolver.Path)
path, err := this.ResolveCwd(address)
if err != nil { return err }
this.Debugln("compiling unit", path)
var semanticTree analyzer.Tree
_, err = this.AnalyzeUnit(&semanticTree, path, false)
if err != nil { return err }
irModule, err := native.NativeTarget().Generate(semanticTree)
if err != nil {
return errors.New(fmt.Sprintf("internal errror: %v", err))
}
// if the format isn't specified, try to get it from the filename
// extension of the input. if that isn't specified, default to .o
if this.Format == "" {
this.Format = filepath.Ext(this.Output)
}
if this.Format == "" {
this.Format = ".o"
}
// if the output file is unspecified, generate a nickname from the
// input address. if that doesn't work, default to "output"
if this.Output == "" {
nickname, ok := address.Nickname()
if !ok { nickname = "output" }
this.Output = nickname + this.Format
}
// do something based on the output extension
// TODO: add .so
switch this.Format {
case ".s":
return this.CompileIRModule(irModule, "asm")
case ".o":
return this.CompileIRModule(irModule, "obj")
case ".ll":
file, err := os.Create(this.Output)
if err != nil { return err }
defer file.Close()
_, err = irModule.WriteTo(file)
return err
case "":
return errors.New(fmt.Sprint (
"output file has no extension, ",
"could not determine output type"))
default:
return errors.New(fmt.Sprintf (
"unknown output type %s", this.Format))
}
}
func (this *Compiler) AnalyzeUnit (
semanticTree *analyzer.Tree,
path string,
skim bool,
) (
uuid.UUID,
error,
) {
this.Debugln("analyzing unit", path)
filePath, isFile := entity.Address(path).SourceFile()
modulePath, isModule := entity.Address(path).Module()
if !isFile && !isModule {
return uuid.UUID { }, errors.New(fmt.Sprintf (
"%v is not a module, nor a source file",
path))
}
if isModule {
return this.AnalyzeModule(semanticTree, modulePath, skim)
} else {
return this.AnalyzeSourceFile(semanticTree, filePath, skim)
}
}
func (this *Compiler) AnalyzeModule (
semanticTree *analyzer.Tree,
path string,
skim bool,
) (
uuid.UUID,
error,
) {
this.Debugln("analyzing module", path)
// parse module metadata file
var metaTree metaParser.Tree
lx, err := lexer.LexFile(filepath.Join(path, "fspl.mod"))
if err != nil { return uuid.UUID { }, err }
err = metaTree.Parse(lx)
if err != nil { return uuid.UUID { }, err }
// ensure metadata is well formed
dependencies := make(map[string] *entity.Dependency)
for _, dependency := range metaTree.Dependencies {
nickname := dependency.Nickname
if nickname == "" {
newNickname, ok := dependency.Address.Nickname()
if !ok {
return uuid.UUID { }, ferrors.Errorf (
dependency.Position,
"cannot generate nickname for %v, " +
"please add one after the address",
dependency.Address)
}
nickname = newNickname
}
if previous, exists := dependencies[nickname]; exists {
return uuid.UUID { }, ferrors.Errorf (
dependency.Position,
"unit with nickname %v already listed at %v",
nickname, previous.Position)
}
dependencies[nickname] = dependency
}
// analyze dependency units, building a nickname translation table
nicknames := make(map[string] uuid.UUID)
dependencyKeys := sortMapKeys(dependencies)
for _, nickname := range dependencyKeys {
dependency := dependencies[nickname]
resolved, err := this.Resolve(path, dependency.Address)
if err != nil { return uuid.UUID { }, err }
dependencyUUID, err := this.AnalyzeUnit(semanticTree, resolved, true)
if err != nil { return uuid.UUID { }, err }
nicknames[nickname] = dependencyUUID
}
// parse this unit
var syntaxTree fsplParser.Tree
err = this.ParseUnit(&syntaxTree, path, skim)
if err != nil { return uuid.UUID { }, err}
// analyze this unit
err = semanticTree.Analyze(metaTree.UUID, nicknames, syntaxTree)
if err != nil { return uuid.UUID { }, err}
return metaTree.UUID, nil
}
func (this *Compiler) AnalyzeSourceFile (
semanticTree *analyzer.Tree,
path string,
skim bool,
) (
uuid.UUID,
error,
) {
this.Debugln("analyzing source file", path)
// parse this unit
var syntaxTree fsplParser.Tree
err := this.ParseUnit(&syntaxTree, path, skim)
if err != nil { return uuid.UUID { }, err}
// analyze this unit
unitId := entity.Address(path).UUID()
err = semanticTree.Analyze(unitId, nil, syntaxTree)
if err != nil { return uuid.UUID { }, err}
return unitId, nil
}
func (this *Compiler) ParseUnit (
syntaxTree *fsplParser.Tree,
path string,
skim bool,
) (
error,
) {
filePath, isFile := entity.Address(path).SourceFile()
modulePath, isModule := entity.Address(path).Module()
if !isFile && !isModule {
return errors.New(fmt.Sprintf (
"%v is not a module, nor a source file",
path))
}
if isModule {
return this.ParseModule(syntaxTree, modulePath, skim)
} else {
return this.ParseSourceFile(syntaxTree, filePath, skim)
}
}
func (this *Compiler) ParseModule (
syntaxTree *fsplParser.Tree,
path string,
skim bool,
) (
error,
) {
this.Debugln("parsing module", path)
// parse all files in the module
entries, err := os.ReadDir(path)
if err != nil { return err }
for _, entry := range entries {
if filepath.Ext(entry.Name()) != ".fspl" { continue }
lx, err := lexer.LexFile(filepath.Join(path, entry.Name()))
if err != nil { return err }
if skim {
err = syntaxTree.Skim(lx)
} else {
err = syntaxTree.Parse(lx)
}
if err != nil { return err }
}
return nil
}
func (this *Compiler) ParseSourceFile (
syntaxTree *fsplParser.Tree,
path string,
skim bool,
) (
error,
) {
this.Debugln("parsing source file", path)
lx, err := lexer.LexFile(path)
if err != nil { return err }
if skim {
err = syntaxTree.Skim(lx)
} else {
err = syntaxTree.Parse(lx)
}
if err != nil { return err }
return nil
}
func (this *Compiler) CompileIRModule (module *llvm.Module, filetype string) error {
this.Debugln("compiling ir module to filetype", filetype)
commandName, args, err := this.FindBackend(filetype)
if err != nil { return err }
command := exec.Command(commandName, args...)
this.Debugln("compiling ir using:", command)
command.Stdout = os.Stdout
command.Stderr = os.Stderr
pipe, err := command.StdinPipe()
if err != nil { return err }
err = command.Start()
if err != nil { return err }
_, err = module.WriteTo(pipe)
if err != nil { return err }
pipe.Close()
return command.Wait()
}
// FindBackend returns the name of an LLVM backend command, and a list of
// arguments to pass to it. It tries commands in this order:
// - llc
// - llc-<latest> -> llc-14
// - clang
// If none were found, it returns an error.
func (this *Compiler) FindBackend (filetype string) (string, []string, error) {
llcArgs := []string {
"-",
fmt.Sprintf("-filetype=%s", filetype),
"-o", this.Output,
fmt.Sprintf("-O=%s", this.Optimization),
}
// attempt to use llc command
commandName := "llc"
_, err := exec.LookPath(commandName)
this.Debugln("trying", commandName)
if err == nil {
return commandName, llcArgs, nil
}
// attempt to find a versioned llc command, counting down from the
// latest known version
llcVersion := 17 // TODO change this number to the latest llc version in
// the future
for err != nil && llcVersion >= 14 {
commandName := fmt.Sprintf("llc-%d", llcVersion)
this.Debugln("trying", commandName)
_, err = exec.LookPath(commandName)
if err == nil {
if llcVersion == 14 {
// -opaque-pointers is needed in version 14
llcArgs = append(llcArgs, "-opaque-pointers")
this.Warnln (
"using llvm llc version 14, which",
"does not have proper support for",
"opaque pointers. expect bugs")
}
return commandName, llcArgs, nil
}
llcVersion --
}
// attempt to use clang
commandName = "clang"
_, err = exec.LookPath(commandName)
this.Debugln("trying", commandName)
if err == nil {
if filetype != "obj" {
return "", nil, errors.New("need 'llc' to compile to " + filetype)
}
this.Warnln("falling back to clang to compile llvm ir. expect bugs")
return commandName, []string {
"-c",
"-x", "ir",
"-o", this.Output,
fmt.Sprintf("-O%s", this.Optimization),
"-",
}, nil
}
return "", nil, errors.New (
"no suitable backends found: please make sure either 'llc' " +
"or 'clang' are accessable from your PATH")
}
func (this *Compiler) Errorln (v ...any) {
if this.Quiet { return }
if this.Stderr == nil { return }
fmt.Fprintf(this.Stderr, "%s: %s", os.Args[0], fmt.Sprintln(v...))
}
func (this *Compiler) Warnln (v ...any) {
if this.Quiet { return }
if this.Stderr == nil { return }
fmt.Fprintf(this.Stderr, "%s: warning: %s", os.Args[0], fmt.Sprintln(v...))
}
func (this *Compiler) Debugln (v ...any) {
if this.Quiet { return }
if !this.Debug { return }
if this.Stderr == nil { return }
fmt.Fprintf(this.Stderr, "%s: debug: %s", os.Args[0], fmt.Sprintln(v...))
}
func sortMapKeys[T any] (unsorted map[string] T) []string {
keys := make([]string, len(unsorted))
index := 0
for key := range unsorted {
keys[index] = key
index ++
}
sort.Strings(keys)
return keys
}