polish-compiler #35
256
cli/cli.go
Normal file
256
cli/cli.go
Normal 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
119
cmd/fsplc/compiler.go
Normal 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()
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user