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- -> 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 }