365 lines
9.3 KiB
Go
365 lines
9.3 KiB
Go
package compiler
|
|
|
|
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/cli"
|
|
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
|
|
cli.Logger
|
|
|
|
Output string
|
|
Optimization string
|
|
Format string
|
|
}
|
|
|
|
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 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
|
|
}
|