385 lines
		
	
	
		
			9.9 KiB
		
	
	
	
		
			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
 | |
| }
 |