gotlin/cmd/gotlinc/main.go

272 lines
6.1 KiB
Go

package main
import (
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"gotlin/internal/lang"
)
func main() {
if len(os.Args) < 2 {
usage()
}
switch os.Args[1] {
case "build":
runBuild(os.Args[2:])
case "run":
runRun(os.Args[2:])
default:
runBuild(os.Args[1:])
}
}
func runBuild(args []string) {
fs := flag.NewFlagSet("build", flag.ExitOnError)
srcOnly := fs.Bool("src", false, "emit Go source instead of building an executable")
outPath := fs.String("o", "", "output file path")
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: gotlinc build [-src] [-o output] <input.gt>")
}
normalizedArgs, err := normalizeBuildArgs(args)
if err != nil {
fail(err)
}
if err := fs.Parse(normalizedArgs); err != nil {
fail(err)
}
if fs.NArg() < 1 {
fs.Usage()
os.Exit(2)
}
inputPath := fs.Arg(0)
goSrc := compileFiles(fs.Args(), !*srcOnly)
if *srcOnly {
if *outPath == "" {
_, _ = os.Stdout.WriteString(goSrc)
return
}
if err := os.WriteFile(*outPath, []byte(goSrc), 0o644); err != nil {
fail(err)
}
return
}
outputPath := *outPath
if outputPath == "" {
outputPath = defaultExecutablePath(inputPath)
}
if err := buildExecutable(goSrc, outputPath); err != nil {
fail(err)
}
}
func runRun(args []string) {
fs := flag.NewFlagSet("run", flag.ExitOnError)
fs.Usage = func() {
fmt.Fprintln(os.Stderr, "usage: gotlinc run <input.gt> [program args...]")
}
if err := fs.Parse(args); err != nil {
fail(err)
}
if fs.NArg() < 1 {
fs.Usage()
os.Exit(2)
}
inputPath := fs.Arg(0)
goSrc := compileFile(inputPath, true)
tmpFile, err := os.CreateTemp("", "gotlinc-*.go")
if err != nil {
fail(err)
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err := tmpFile.WriteString(goSrc); err != nil {
_ = tmpFile.Close()
fail(err)
}
if err := tmpFile.Close(); err != nil {
fail(err)
}
cmdArgs := append([]string{"run", tmpPath}, fs.Args()[1:]...)
cmd := exec.Command("go", cmdArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Run(); err != nil {
fail(err)
}
}
func compileFile(inputPath string, forceMain bool) string {
return compileFiles([]string{inputPath}, forceMain)
}
func compileFiles(inputPaths []string, forceMain bool) string {
program := &lang.Program{}
var firstSource string
for _, inputPath := range inputPaths {
src, err := os.ReadFile(inputPath)
if err != nil {
fail(err)
}
parsed, err := lang.Parse(string(src))
if err != nil {
fail(err)
}
if program.PackagePath == "" {
program.PackagePath = parsed.PackagePath
}
if parsed.PackagePath != program.PackagePath {
fail(fmt.Errorf("Gotlin files must use the same package"))
}
program.Imports = append(program.Imports, parsed.Imports...)
program.Interfaces = append(program.Interfaces, parsed.Interfaces...)
program.Enums = append(program.Enums, parsed.Enums...)
program.Classes = append(program.Classes, parsed.Classes...)
program.Workers = append(program.Workers, parsed.Workers...)
program.Functions = append(program.Functions, parsed.Functions...)
program.Embeds = append(program.Embeds, parsed.Embeds...)
if firstSource == "" {
firstSource = string(src)
}
}
var goSrc []byte
var err error
if forceMain {
goSrc, err = lang.GenerateGoMain(program)
} else {
goSrc, err = lang.GenerateGo(program)
}
if err != nil {
fail(err)
}
if forceMain {
return addBestEffortLineDirectives(string(goSrc), inputPaths[0], firstSource)
}
return string(goSrc)
}
func buildExecutable(goSrc string, outputPath string) error {
tmpFile, err := os.CreateTemp("", "gotlinc-build-*.go")
if err != nil {
return err
}
tmpPath := tmpFile.Name()
defer os.Remove(tmpPath)
if _, err := tmpFile.WriteString(goSrc); err != nil {
_ = tmpFile.Close()
return err
}
if err := tmpFile.Close(); err != nil {
return err
}
cmd := exec.Command("go", "build", "-o", outputPath, tmpPath)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
return cmd.Run()
}
func defaultExecutablePath(inputPath string) string {
base := strings.TrimSuffix(filepath.Base(inputPath), filepath.Ext(inputPath))
if runtime.GOOS == "windows" && filepath.Ext(base) != ".exe" {
base += ".exe"
}
return base
}
func normalizeBuildArgs(args []string) ([]string, error) {
var flags []string
var positional []string
for i := 0; i < len(args); i++ {
arg := args[i]
switch {
case arg == "-src":
flags = append(flags, arg)
case arg == "-o":
if i+1 >= len(args) {
return nil, fmt.Errorf("missing value for -o")
}
flags = append(flags, arg, args[i+1])
i++
case strings.HasPrefix(arg, "-o="):
flags = append(flags, arg)
case strings.HasPrefix(arg, "-"):
flags = append(flags, arg)
default:
positional = append(positional, arg)
}
}
return append(flags, positional...), nil
}
func usage() {
fmt.Fprintln(os.Stderr, "usage:")
fmt.Fprintln(os.Stderr, " gotlinc build [-src] [-o output] <input.gt>")
fmt.Fprintln(os.Stderr, " gotlinc run <input.gt> [program args...]")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "default command: build")
os.Exit(2)
}
func fail(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
func addBestEffortLineDirectives(goSrc string, inputPath string, gtSrc string) string {
fnLines := sourceFunctionLines(gtSrc)
if len(fnLines) == 0 {
return goSrc
}
pattern := regexp.MustCompile(`^\s*func\s+(?:\(\s*[^)]*\)\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
lines := strings.Split(goSrc, "\n")
out := make([]string, 0, len(lines)+len(fnLines))
for _, line := range lines {
m := pattern.FindStringSubmatch(line)
if len(m) == 2 {
if gtLine, ok := fnLines[m[1]]; ok {
out = append(out, fmt.Sprintf("//line %s:%d", inputPath, gtLine))
}
}
out = append(out, line)
}
return strings.Join(out, "\n")
}
func sourceFunctionLines(src string) map[string]int {
out := map[string]int{}
pattern := regexp.MustCompile(`^\s*fun\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(`)
lines := strings.Split(src, "\n")
for i, line := range lines {
m := pattern.FindStringSubmatch(line)
if len(m) == 2 {
if _, exists := out[m[1]]; !exists {
out[m[1]] = i + 1
}
}
}
return out
}