xplshn
·
2025-08-13
util.go
Go
1package util
2
3import (
4 "fmt"
5 "os"
6 "path/filepath"
7 "runtime"
8 "strings"
9
10 "github.com/xplshn/gbc/pkg/config"
11 "github.com/xplshn/gbc/pkg/token"
12)
13
14// ANSI color and formatting constants
15const (
16 colorRed = "\033[31m"
17 colorYellow = "\033[33m"
18 colorReset = "\033[0m"
19 colorGray = "\033[90m"
20 colorBoldGray = "\033[1;90m"
21 formatItalic = "\033[3m"
22)
23
24// SourceFileRecord stores a file's name and content
25type SourceFileRecord struct {
26 Name string
27 Content []rune
28}
29
30var sourceFiles []SourceFileRecord
31
32// SetSourceFiles updates the global source files list
33func SetSourceFiles(files []SourceFileRecord) {
34 sourceFiles = files
35}
36
37// findFileAndLine extracts file name, line, and column from a token
38func findFileAndLine(tok token.Token) (string, int, int) {
39 if tok.FileIndex < 0 || tok.FileIndex >= len(sourceFiles) {
40 return "<unknown>", tok.Line, tok.Column
41 }
42 return filepath.Base(sourceFiles[tok.FileIndex].Name), tok.Line, tok.Column
43}
44
45// callerFile retrieves the caller's file name, skipping specified stack frames
46func callerFile(skip int) string {
47 _, file, _, ok := runtime.Caller(skip)
48 if !ok {
49 return "<unknown>"
50 }
51 return filepath.Base(file)
52}
53
54// printSourceContext prints source code context with line numbers, caret, message, and caller info
55func printSourceContext(stream *os.File, tok token.Token, isError bool, msg, caller string) {
56 if tok.FileIndex < 0 || tok.FileIndex >= len(sourceFiles) || tok.Line <= 0 {
57 return
58 }
59
60 content := sourceFiles[tok.FileIndex].Content
61 lines := strings.Split(string(content), "\n")
62
63 start := tok.Line - 2
64 if start < 0 {
65 start = 0
66 }
67 end := tok.Line + 1
68 if end > len(lines) {
69 end = len(lines)
70 }
71 lineNumWidth := len(fmt.Sprintf("%d", end))
72 linePrefix := strings.Repeat(" ", 3)
73
74 for i := start; i < end; i++ {
75 lineNum := i + 1
76 line := strings.ReplaceAll(lines[i], "\t", " ")
77 isErrorLine := lineNum == tok.Line
78
79 gutter := fmt.Sprintf("%s%*d | ", linePrefix, lineNumWidth, lineNum)
80 if isErrorLine {
81 gutter = boldGray(gutter)
82 } else {
83 gutter = gray(gutter)
84 }
85
86 fmt.Fprintf(stream, " %s%s\n", gutter, line)
87
88 if isErrorLine {
89 colPos := caretColumn(line, tok.Column)
90 caretLine := strings.Repeat(" ", colPos-1) + "^"
91 if tok.Len > 1 {
92 caretLine += strings.Repeat("~", tok.Len-1)
93 }
94
95 caretGutter := strings.Repeat("-", lineNumWidth) + " | "
96 caretGutter = boldGray(caretGutter)
97
98 var caretColored, msgColored, callerColored string
99 if isError {
100 caretColored = red(caretLine)
101 msgColored = italic(msg)
102 } else {
103 caretColored = yellow(caretLine)
104 msgColored = italic(msg)
105 }
106 callerColored = italic(gray(fmt.Sprintf("(emitted from %s)", boldGray(caller))))
107
108 fmt.Fprintf(stream, " %s%s%s %s %s%s\n", linePrefix, caretGutter, caretColored, msgColored, callerColored, colorReset)
109 }
110 }
111 fmt.Fprintln(stream)
112}
113
114// caretColumn calculates the display column accounting for tabs
115func caretColumn(line string, col int) int {
116 if col < 1 {
117 col = 1
118 }
119 runes := []rune(line)
120 pos := 0
121 for i := 0; i < col-1 && i < len(runes); i++ {
122 if runes[i] == '\t' {
123 pos += 4
124 } else {
125 pos++
126 }
127 }
128 return pos + 1
129}
130
131// ANSI formatting helpers
132func italic(s string) string { return formatItalic + s + colorReset }
133func gray(s string) string { return colorGray + s + colorReset }
134func boldGray(s string) string { return colorBoldGray + s + colorReset }
135func red(s string) string { return colorRed + s + colorReset }
136func yellow(s string) string { return colorYellow + s + colorReset }
137
138// Error prints an error message with source context and exits
139func Error(tok token.Token, format string, args ...interface{}) {
140 msg := fmt.Sprintf(format, args...)
141 // Handle non-source related errors gracefully
142 if tok.FileIndex < 0 || tok.FileIndex >= len(sourceFiles) || tok.Line <= 0 {
143 fmt.Fprintf(os.Stderr, "gbc: %serror:%s %s\n", colorRed, colorReset, msg)
144 os.Exit(1)
145 }
146
147 filename, line, col := findFileAndLine(tok)
148 caller := callerFile(2)
149
150 fmt.Fprintf(os.Stderr, "%s:%d:%d: %serror%s:\n", filename, line, col, colorRed, colorReset)
151 printSourceContext(os.Stderr, tok, true, msg, caller)
152 os.Exit(1)
153}
154
155// Warn prints a warning message with source context if the warning is enabled
156func Warn(cfg *config.Config, wt config.Warning, tok token.Token, format string, args ...interface{}) {
157 if !cfg.IsWarningEnabled(wt) {
158 return
159 }
160 msg := fmt.Sprintf(format, args...) + fmt.Sprintf(" [-W%s]", cfg.Warnings[wt].Name)
161
162 // Handle non-source related warnings gracefully
163 if tok.FileIndex < 0 || tok.FileIndex >= len(sourceFiles) || tok.Line <= 0 {
164 fmt.Fprintf(os.Stderr, "gbc: %swarning:%s %s\n", colorYellow, colorReset, msg)
165 return
166 }
167
168 filename, line, col := findFileAndLine(tok)
169 caller := callerFile(2)
170
171 fmt.Fprintf(os.Stderr, "%s:%d:%d: %swarning%s:\n", filename, line, col, colorYellow, colorReset)
172 printSourceContext(os.Stderr, tok, false, msg, caller)
173}