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] ") } 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 := compileFile(inputPath, !*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 [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 { src, err := os.ReadFile(inputPath) if err != nil { fail(err) } program, err := lang.Parse(string(src)) if err != nil { fail(err) } var goSrc []byte if forceMain { goSrc, err = lang.GenerateGoMain(program) } else { goSrc, err = lang.GenerateGo(program) } if err != nil { fail(err) } if forceMain { return addBestEffortLineDirectives(string(goSrc), inputPath, string(src)) } 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] ") fmt.Fprintln(os.Stderr, " gotlinc run [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 }