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}