Merge pull request 'polish-compiler' (#35) from polish-compiler into main

Reviewed-on: sashakoshka/fspl#35
This commit is contained in:
Sasha Koshka 2024-02-11 18:06:08 +00:00
commit 970a236eaa
4 changed files with 412 additions and 114 deletions

256
cli/cli.go Normal file
View File

@ -0,0 +1,256 @@
package cli
import "os"
import "fmt"
import "errors"
import "strings"
import "strconv"
// Cli represents a command line interface. It is not tied to os.Args, and it is
// possible to create multiple Cli's to have so many sub-commands in one
// program.
type Cli struct {
// Description describes the application.
Description string
// The syntax description of the command. It appears in Usage after the
// name of the program (os.Args[0]). If not specified, "[OPTIONS]..."
// will be used instead.
Syntax string
// Flag is a set of flags to parse.
Flags []*Flag
// Args is a list of leftover arguments that were not parsed.
Args []string
}
// New creates a new Cli from a command description and a set of flags. These
// flags should be created as variables before passing them to New(), so that
// their values can be extracted after Parse is called.
func New (description string, flags ...*Flag) *Cli {
return &Cli {
Description: description,
Flags: flags,
}
}
// Parse parses the given set of command line arguments according to the flags
// defined within the Cli. The first element of the argument slice is always
// dropped, because it is assumed that it just contains the command name.
func (this *Cli) Parse (args []string) error {
// strip out program name
if len(args) > 0 { args = args[1:] }
next := func () string {
args = args[1:]
if len(args) > 0 {
return args[0]
} else {
return ""
}
}
processFlag := func (flag *Flag, allowInput bool) error {
if flag.Validate == nil {
flag.Value = "true"
} else {
if len(args) < 1 || !allowInput {
return errors.New(
flag.String() +
" requires a value")
}
value := next()
err := flag.Validate(value)
if err != nil {
return errors.New(fmt.Sprint (
"bad value for ", flag.String(), ": ",
err))
}
flag.Value = value
}
return nil
}
// process args one by one
for ; len(args) > 0; next() {
arg := args[0]
switch {
case arg == "--":
// halt parsing
this.Args = append(this.Args, args[1:]...)
return nil
case strings.HasPrefix(arg, "--"):
// long flag
flag, ok := this.LongFlag(arg[2:])
if !ok {
return errors.New("unknown flag: " + arg)
}
err := processFlag(flag, true)
if err != nil { return err }
case strings.HasPrefix(arg, "-") && len(arg) > 1:
// one or more short flags
arg := arg[1:]
for _, part := range arg {
flag, ok := this.ShortFlag(part)
if !ok {
return errors.New (
"unknown flag: -" +
string(part))
}
err := processFlag(flag, len(arg) == 1)
if err != nil { return err }
}
default:
// not a flag
this.Args = append(this.Args, arg)
}
}
return nil
}
// ParseOrExit is like Parse, but if an error occurs while parsing the argument
// list, it prints out Usage and exits with error status 2.
func (this *Cli) ParseOrExit (args []string) {
err := this.Parse(args)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n\n", os.Args[0], err)
this.Usage()
os.Exit(2)
}
}
// ShortFlag searches for and returns the flag with the given short form. If it
// was found, it returns true. If it was not found, it returns nil, false.
func (this *Cli) ShortFlag (short rune) (*Flag, bool) {
if short == 0 { return nil, false }
for _, flag := range this.Flags {
if flag.Short == short { return flag, true }
}
return nil, false
}
// LongFlag searches for and returns the flag with the given long form. If it
// was found, it returns true. If it was not found, it returns nil, false.
func (this *Cli) LongFlag (long string) (*Flag, bool) {
if long == "" { return nil, false }
for _, flag := range this.Flags {
if flag.Long == long { return flag, true }
}
return nil, false
}
// Usage prints out usage/help information.
func (this *Cli) Usage () {
fmt.Fprintf(os.Stderr, "Usage: %s ", os.Args[0])
if this.Syntax == "" {
fmt.Fprint(os.Stderr, "[OPTION]...")
} else {
fmt.Fprint(os.Stderr, this.Syntax)
}
fmt.Fprint(os.Stderr, "\n\n")
if this.Description != "" {
fmt.Fprintf(os.Stderr, "%s\n\n", this.Description)
}
longest := 0
for _, flag := range this.Flags {
longest = max(longest, len(flag.Long))
}
format := fmt.Sprint("\t%-", longest + 8, "s%s\n")
for _, flag := range this.Flags {
shortLong := "-" + string(flag.Short)
if flag.Short != 0 && flag.Long != "" {
shortLong += ", "
}
shortLong += "--" + flag.Long
fmt.Fprintf(os.Stderr, format, shortLong, flag.Help)
}
}
// Flag is a command line option.
type Flag struct {
Short rune // The short form of the flag (-l)
Long string // Long form of the flag (--long-form)
Help string // Help text to display by the flag
// Validate is an optional function that is called when an input is
// passed to the flag. It checks whether or not the input is valid, and
// returns an error if it isn't. If this function is nil, the flag is
// assumed to have no input.
Validate func (string) error
// Value contains the input given to the flag, and is filled out when
// the flags are parsed. If this is a non-input flag (if Validate is
// nil), this will be set to true. If the flag was not specified, Value
// will be unchanged.
Value string
}
// String returns --<LongForm> if specified, and if not returns -<ShortForm>
func (this *Flag) String () string {
if this.Long == "" {
return fmt.Sprintf("-%c", this.Short)
} else {
return fmt.Sprintf("--%s", this.Long)
}
}
// NewFlag creates a new flag that does not take in a value.
func NewFlag (short rune, long string, help string) *Flag {
return &Flag {
Short: short,
Long: long,
Help: help,
}
}
// NewInputFlag creates a new flag that does take in a value. This function will
// panic if the given validation function is nil.
func NewInputFlag (short rune, long string, help string, defaul string, validate func (string) error) *Flag {
if validate == nil {
panic("validate must be non-nil for a flag to take in a value")
}
return &Flag {
Short: short,
Long: long,
Help: help,
Value: defaul,
Validate: validate,
}
}
// ValString is a validation function that always returns nil, accepting any
// string.
func ValString (value string) error {
return nil
}
// ValInt is a validation function that returns an error if the value is not an
// integer.
func ValInt (value string) error {
_, err := strconv.Atoi(value)
return err
}
// NewValSet returns a validation function that accepts a set of strings.
func NewValSet (allowed ...string) func (value string) error {
return func (value string) error {
allowedFmt := ""
for index, test := range allowed {
if test == value { return nil }
if index > 0 { allowedFmt += ", " }
allowedFmt += test
}
return errors.New (
"value must be one of (" + allowedFmt + ")")
}
}

119
cmd/fsplc/compiler.go Normal file
View File

@ -0,0 +1,119 @@
package main
import "os"
import "fmt"
import "errors"
import "strings"
import "os/exec"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/fspl/llvm"
import "git.tebibyte.media/sashakoshka/fspl/lexer"
import "git.tebibyte.media/sashakoshka/fspl/parser"
import "git.tebibyte.media/sashakoshka/fspl/analyzer"
import "git.tebibyte.media/sashakoshka/fspl/generator/native"
type Compiler struct {
Output string
Optimization string
Format string
}
func (this *Compiler) Compile (inputs []string) error {
if len(inputs) == 0 {
return errors.New("no input files specified")
}
var syntaxTree parser.Tree
for _, name := range inputs {
lx, err := lexer.LexFile(name)
if err != nil { return err }
err = syntaxTree.Parse(lx)
if err != nil { return err }
}
var semanticTree analyzer.Tree
err := semanticTree.Analyze(syntaxTree)
if err != nil { return err }
module, 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 this.Output == "" {
this.Output = strings.TrimSuffix (
inputs[0],
filepath.Ext(inputs[0])) + this.Format
}
// do something based on the output extension
switch this.Format {
case ".s":
return this.CompileModule(module, "asm")
case ".o":
return this.CompileModule(module, "obj")
case ".ll":
file, err := os.Create(this.Output)
if err != nil { return err }
defer file.Close()
_, err = module.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) CompileModule (module *llvm.Module, filetype string) error {
var commandName string
var args []string
_, err := exec.LookPath("llc")
if err == nil {
commandName = "llc"
args = []string {
"-",
fmt.Sprintf("-filetype=%s", filetype),
"-o", this.Output,
fmt.Sprintf("-O=%s", this.Optimization),
}
} else {
commandName = "clang"
if filetype != "obj" {
return errors.New("need 'llc' to compile to " + filetype)
}
args = []string {
"-c",
"-x", "ir",
"-o", this.Output,
fmt.Sprintf("-O%d", this.Optimization),
"-",
}
}
command := exec.Command(commandName, args...)
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()
}

View File

@ -2,125 +2,48 @@ package main
import "os"
import "fmt"
import "flag"
import "errors"
import "strings"
import "os/exec"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/fspl/llvm"
import "git.tebibyte.media/sashakoshka/fspl/lexer"
import "git.tebibyte.media/sashakoshka/fspl/parser"
import "git.tebibyte.media/sashakoshka/fspl/analyzer"
import "git.tebibyte.media/sashakoshka/fspl/cli"
import ferrors "git.tebibyte.media/sashakoshka/fspl/errors"
import "git.tebibyte.media/sashakoshka/fspl/generator/native"
type Compiler struct {
Output string
Optimization int
}
func main () {
compiler := new(Compiler)
help := cli.NewFlag (
'h', "help",
"Display usage information and exit")
format := cli.NewInputFlag (
'm', "format",
"Output format (.s, .o, .ll)", "",
cli.NewValSet(".s", ".o", ".ll"))
output := cli.NewInputFlag (
'o', "output",
"Output filename", "",
cli.ValString)
optimization := cli.NewInputFlag (
'O', "optimization",
"Optimization level (0-3)", "0",
cli.NewValSet("0", "1", "2", "3"))
flag.StringVar(&compiler.Output, "o", "", "Output filename")
flag.IntVar(&compiler.Optimization, "O", 0, "Optimization level (0-3)")
flag.Parse()
if compiler.Optimization < 0 || compiler.Optimization > 3 {
flag.Usage()
application := cli.New (
"Compile FSPL source files",
help,
format,
output,
optimization)
application.Syntax = "[OPTION]... [FILE]..."
application.ParseOrExit(os.Args)
if help.Value != "" {
application.Usage()
os.Exit(2)
}
compiler := new(Compiler)
compiler.Output = output.Value
compiler.Optimization = optimization.Value
compiler.Format = format.Value
err := compiler.Compile(flag.Args())
err := compiler.Compile(application.Args)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], ferrors.Format(err))
os.Exit(1)
}
}
func (this *Compiler) Compile (inputs []string) error {
if len(inputs) == 0 {
return errors.New("no input files specified")
}
var syntaxTree parser.Tree
for _, name := range inputs {
lx, err := lexer.LexFile(name)
if err != nil { return err }
err = syntaxTree.Parse(lx)
if err != nil { return err }
}
var semanticTree analyzer.Tree
err := semanticTree.Analyze(syntaxTree)
if err != nil { return err }
module, err := native.NativeTarget().Generate(semanticTree)
if err != nil {
return errors.New(fmt.Sprintf("internal errror: %v", err))
}
if this.Output == "" {
this.Output = strings.TrimSuffix (
inputs[0],
filepath.Ext(inputs[0])) + ".o"
}
extension := filepath.Ext(this.Output)
switch extension {
case ".s":
return this.CompileModule(module, "asm")
case ".o":
return this.CompileModule(module, "obj")
case ".ll":
file, err := os.Create(this.Output)
if err != nil { return err }
defer file.Close()
_, err = module.WriteTo(file)
return err
default:
return errors.New(fmt.Sprintf (
"unknown output type %s", extension))
}
}
func (this *Compiler) CompileModule (module *llvm.Module, filetype string) error {
var commandName string
var args []string
_, err := exec.LookPath("llc")
if err == nil {
commandName = "llc"
args = []string {
"-",
fmt.Sprintf("-filetype=%s", filetype),
"-o", this.Output,
fmt.Sprintf("-O=%d", this.Optimization),
}
} else {
commandName = "clang"
if filetype != "obj" {
return errors.New("need 'llc' to compile to " + filetype)
}
args = []string {
"-c",
"-x", "ir",
"-o", this.Output,
fmt.Sprintf("-O%d", this.Optimization),
"-",
}
}
command := exec.Command(commandName, args...)
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()
}

View File

@ -50,12 +50,12 @@ func Errorf (position Position, format string, variables ...any) Error {
// because normal error messages do not produce trailing line breaks, neither
// does this function.
func Format (err error) string {
if err, ok := err.(Error); ok {
if e, ok := err.(Error); ok {
return fmt.Sprintf (
"%v: %v\n%v",
err.Position(),
err.Error(),
err.Position().Format())
e.Position(),
e.Error(),
e.Position().Format())
} else {
return err.Error()
}