repos / gbc

GBC - Go B Compiler
git clone https://github.com/xplshn/gbc.git

gbc / cmd / gbc
xplshn  ·  2025-09-14

main.go

Go
  1package main
  2
  3import (
  4	"fmt"
  5	"os"
  6	"os/exec"
  7	"path/filepath"
  8	"runtime"
  9	"strings"
 10
 11	"github.com/xplshn/gbc/pkg/ast"
 12	"github.com/xplshn/gbc/pkg/cli"
 13	"github.com/xplshn/gbc/pkg/codegen"
 14	"github.com/xplshn/gbc/pkg/config"
 15	"github.com/xplshn/gbc/pkg/lexer"
 16	"github.com/xplshn/gbc/pkg/parser"
 17	"github.com/xplshn/gbc/pkg/token"
 18	"github.com/xplshn/gbc/pkg/typeChecker"
 19	"github.com/xplshn/gbc/pkg/util"
 20)
 21
 22func main() {
 23	app := cli.NewApp("gbc")
 24	app.Synopsis = "[options] <input.b> ..."
 25	app.Description = "A compiler for the B programming language with modern extensions. Like stepping into a time machine, but with better error messages."
 26	app.Authors = []string{"xplshn"}
 27	app.Repository = "<https://github.com/xplshn/gbc>"
 28	app.Since = 2025
 29
 30	var (
 31		outFile          string
 32		std              string
 33		target           string
 34		linkerArgs       []string
 35		compilerArgs     []string
 36		userIncludePaths []string
 37		libRequests      []string
 38		pedantic         bool
 39		dumpIR           bool
 40	)
 41
 42	fs := app.FlagSet
 43	fs.String(&outFile, "output", "o", "a.out", "Place the output into <file>.", "file")
 44	fs.String(&target, "target", "t", "qbe", "Set the backend and target ABI.", "backend/target")
 45	fs.Bool(&dumpIR, "dump-ir", "d", false, "Dump the intermediate representation and exit.")
 46	fs.List(&userIncludePaths, "include", "I", []string{}, "Add a directory to the include path.", "path")
 47	fs.List(&linkerArgs, "linker-arg", "L", []string{}, "Pass an argument to the linker.", "arg")
 48	fs.List(&compilerArgs, "compiler-arg", "C", []string{}, "Pass a compiler-specific argument (e.g., -C linker_args='-s').", "arg")
 49	fs.Special(&libRequests, "l", "Link with a library (e.g., -lb for 'b')", "lib")
 50	fs.String(&std, "std", "", "Bx", "Specify language standard (B, Bx)", "std")
 51	fs.Bool(&pedantic, "pedantic", "", false, "Issue all warnings demanded by the current B std.")
 52
 53	cfg := config.NewConfig()
 54	warningFlags, featureFlags := cfg.SetupFlagGroups(fs)
 55
 56	// Main compilation pipeline
 57	app.Action = func(inputFiles []string) error {
 58		// Pedantic flag affects everything else
 59		if pedantic {
 60			cfg.SetWarning(config.WarnPedantic, true)
 61		}
 62
 63		// Apply language standard first
 64		if err := cfg.ApplyStd(std); err != nil {
 65			util.Error(token.Token{}, err.Error())
 66		}
 67
 68		// Apply warning flags (override standard settings)
 69		for i, entry := range warningFlags {
 70			if entry.Enabled != nil && *entry.Enabled {
 71				cfg.SetWarning(config.Warning(i), true)
 72			}
 73			if entry.Disabled != nil && *entry.Disabled {
 74				cfg.SetWarning(config.Warning(i), false)
 75			}
 76		}
 77
 78		// Apply feature flags (override standard settings)
 79		for i, entry := range featureFlags {
 80			if entry.Enabled != nil && *entry.Enabled {
 81				cfg.SetFeature(config.Feature(i), true)
 82			}
 83			if entry.Disabled != nil && *entry.Disabled {
 84				cfg.SetFeature(config.Feature(i), false)
 85			}
 86		}
 87
 88		// Set target architecture
 89		cfg.SetTarget(runtime.GOOS, runtime.GOARCH, target)
 90
 91		// Copy over command line settings
 92		cfg.LinkerArgs = append(cfg.LinkerArgs, linkerArgs...)
 93		cfg.LibRequests = append(cfg.LibRequests, libRequests...)
 94		cfg.UserIncludePaths = append(cfg.UserIncludePaths, userIncludePaths...)
 95
 96		// Handle compiler args (-C)
 97		for _, carg := range compilerArgs {
 98			if parts := strings.SplitN(carg, "=", 2); len(parts) == 2 && parts[0] == "linker_args" {
 99				parsedArgs, err := config.ParseCLIString(parts[1])
100				if err != nil {
101					util.Error(token.Token{}, "invalid -C linker_args value: %v", err)
102				}
103				cfg.LinkerArgs = append(cfg.LinkerArgs, parsedArgs...)
104			}
105		}
106
107		// First pass: scan for directives
108		fmt.Println("----------------------")
109		records, allTokens := readAndTokenizeFiles(inputFiles, cfg)
110		util.SetSourceFiles(records)
111		p := parser.NewParser(allTokens, cfg)
112		p.Parse() // picks up directives
113
114		// Now that all directives are processed, determine the final list of source files.
115		finalInputFiles := processInputFiles(inputFiles, cfg)
116		if len(finalInputFiles) == 0 {
117			util.Error(token.Token{}, "no input files specified.")
118		}
119
120		// Second pass: compile everything
121		isTyped := cfg.IsFeatureEnabled(config.FeatTyped)
122		fmt.Printf("Tokenizing %d source file(s) (Typed Pass: %v)...\n", len(finalInputFiles), isTyped)
123		fullRecords, fullTokens := readAndTokenizeFiles(finalInputFiles, cfg)
124		util.SetSourceFiles(fullRecords)
125
126		fmt.Println("Parsing tokens into AST...")
127		fullParser := parser.NewParser(fullTokens, cfg)
128		astRoot := fullParser.Parse()
129
130		fmt.Println("Folding constants...")
131		astRoot = ast.FoldConstants(astRoot)
132
133		if cfg.IsFeatureEnabled(config.FeatTyped) { // recheck after directive processing
134			fmt.Println("Type checking...")
135			tc := typeChecker.NewTypeChecker(cfg)
136			tc.Check(astRoot)
137		}
138
139		fmt.Println("Creating intermediate representation...")
140		cg := codegen.NewContext(cfg)
141		irProg, inlineAsm := cg.GenerateIR(astRoot)
142
143		// Handle --dump-ir/-d flag
144		if dumpIR {
145			fmt.Printf("Dumping IR for '%s' backend...\n", cfg.BackendName)
146			backend := selectBackend(cfg.BackendName)
147			irText, err := backend.GenerateIR(irProg, cfg)
148			if err != nil {
149				util.Error(token.Token{}, "backend IR generation failed: %v", err)
150			}
151			fmt.Print(irText)
152			return nil
153		}
154
155		fmt.Printf("Generating code with '%s' backend...\n", cfg.BackendName)
156		backend := selectBackend(cfg.BackendName)
157		backendOutput, err := backend.Generate(irProg, cfg)
158		if err != nil {
159			util.Error(token.Token{}, "backend code generation failed: %v", err)
160		}
161
162		fmt.Printf("Linking to create '%s'...\n", outFile)
163		if err := assembleAndLink(outFile, backendOutput.String(), inlineAsm, cfg.LinkerArgs); err != nil {
164			util.Error(token.Token{}, "assembler/linker failed: %v", err)
165		}
166
167		fmt.Println("----------------------")
168		fmt.Println("Done!")
169		return nil
170	}
171
172	if err := app.Run(os.Args[1:]); err != nil {
173		os.Exit(1)
174	}
175}
176
177func processInputFiles(args []string, cfg *config.Config) []string {
178	// Use a map to avoid duplicate library entries
179	uniqueLibs := make(map[string]bool)
180	for _, lib := range cfg.LibRequests {
181		uniqueLibs[lib] = true
182	}
183
184	inputFiles := args
185	for libName := range uniqueLibs {
186		if libPath := findLibrary(libName, cfg.UserIncludePaths, cfg); libPath != "" {
187			// Avoid adding the same library file path multiple times
188			found := false
189			for _, inFile := range inputFiles {
190				if inFile == libPath {
191					found = true
192					break
193				}
194			}
195			if !found {
196				inputFiles = append(inputFiles, libPath)
197			}
198		} else {
199			util.Error(token.Token{}, "could not find library '%s' for target %s/%s", libName, cfg.GOOS, cfg.GOARCH)
200		}
201	}
202	return inputFiles
203}
204
205func selectBackend(name string) codegen.Backend {
206	switch name {
207	case "qbe":
208		return codegen.NewQBEBackend()
209	case "llvm":
210		return codegen.NewLLVMBackend()
211	default:
212		util.Error(token.Token{}, "unsupported backend '%s'", name)
213		return nil
214	}
215}
216
217func findLibrary(libName string, userPaths []string, cfg *config.Config) string {
218	// Search for libraries matching the target architecture and OS
219	filenames := []string{
220		fmt.Sprintf("%s_%s_%s.b", libName, cfg.GOARCH, cfg.GOOS),
221		fmt.Sprintf("%s_%s.b", libName, cfg.GOOS),
222		fmt.Sprintf("%s_%s.b", libName, cfg.GOARCH),
223		fmt.Sprintf("%s.b", libName),
224		fmt.Sprintf("%s/%s_%s.b", libName, cfg.GOARCH, cfg.GOOS),
225		fmt.Sprintf("%s/%s.b", libName, cfg.GOOS),
226		fmt.Sprintf("%s/%s.b", libName, cfg.GOARCH),
227		fmt.Sprintf("%s/%s.b", libName, libName),
228	}
229	searchPaths := append(userPaths, []string{"./lib", "/usr/local/lib/gbc", "/usr/lib/gbc", "/lib/gbc"}...)
230	for _, path := range searchPaths {
231		//fmt.Println("path:", path)
232		for _, fname := range filenames {
233			//fmt.Println("fname:", fname)
234			fullPath := filepath.Join(path, fname)
235			//fmt.Println("fullPath:", fullPath)
236			if _, err := os.Stat(fullPath); err == nil {
237				return fullPath
238			}
239		}
240	}
241	util.Error(token.Token{}, "could not find library '%s'", libName)
242	return ""
243}
244
245func assembleAndLink(outFile, mainAsm, inlineAsm string, linkerArgs []string) error {
246	mainAsmFile, err := os.CreateTemp("", "gbc-main-*.s")
247	if err != nil {
248		return fmt.Errorf("failed to create temp file for main asm: %w", err)
249	}
250	defer os.Remove(mainAsmFile.Name())
251	if _, err := mainAsmFile.WriteString(mainAsm); err != nil {
252		return fmt.Errorf("failed to write to temp file for main asm: %w", err)
253	}
254	mainAsmFile.Close()
255
256	// PIE support needs work:
257	//   - LLVM backend has issues
258	//   - QBE backend mostly works but fails on a couple examples
259	// Should default to `-static-pie` eventually
260	ccArgs := []string{"-no-pie", "-o", outFile, mainAsmFile.Name()}
261	if inlineAsm != "" {
262		inlineAsmFile, err := os.CreateTemp("", "gbc-inline-*.s")
263		if err != nil {
264			return fmt.Errorf("failed to create temp file for inline asm: %w", err)
265		}
266		defer os.Remove(inlineAsmFile.Name())
267		if _, err := inlineAsmFile.WriteString(inlineAsm); err != nil {
268			return fmt.Errorf("failed to write to temp file for inline asm: %w", err)
269		}
270		inlineAsmFile.Close()
271		ccArgs = append(ccArgs, inlineAsmFile.Name())
272	}
273	ccArgs = append(ccArgs, linkerArgs...)
274
275	cmd := exec.Command("cc", ccArgs...)
276	if output, err := cmd.CombinedOutput(); err != nil {
277		return fmt.Errorf("cc command failed: %w\nOutput:\n%s", err, string(output))
278	}
279	return nil
280}
281
282func readAndTokenizeFiles(paths []string, cfg *config.Config) ([]util.SourceFileRecord, []token.Token) {
283	var records []util.SourceFileRecord
284	var allTokens []token.Token
285
286	for i, path := range paths {
287		content, err := os.ReadFile(path)
288		if err != nil {
289			util.Error(token.Token{FileIndex: -1}, "could not read file '%s': %v", path, err)
290			continue
291		}
292		runeContent := []rune(string(content))
293		records = append(records, util.SourceFileRecord{Name: path, Content: runeContent})
294		l := lexer.NewLexer(runeContent, i, cfg)
295		for {
296			tok := l.Next()
297			if tok.Type == token.EOF {
298				break
299			}
300			allTokens = append(allTokens, tok)
301		}
302	}
303	finalFileIndex := 0
304	if len(paths) > 0 {
305		finalFileIndex = len(paths) - 1
306	}
307	allTokens = append(allTokens, token.Token{Type: token.EOF, FileIndex: finalFileIndex})
308	return records, allTokens
309}