repos / gbc

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

gbc / pkg / util
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}