package source import "core:fmt" import "core:mem" Source_Id :: distinct u32 Diagnostic_Id :: distinct u32 Offset :: distinct u32 INVALID_SOURCE :: Source_Id(0xffff_ffff) INVALID_DIAGNOSTIC :: Diagnostic_Id(0xffff_ffff) source_id :: proc(index: int) -> Source_Id { assert(index >= 0 && u64(index) < u64(INVALID_SOURCE)) return Source_Id(index) } diagnostic_id :: proc(index: int) -> Diagnostic_Id { assert(index >= 0 && u64(index) < u64(INVALID_DIAGNOSTIC)) return Diagnostic_Id(index) } source_index :: proc(id: Source_Id, count: int) -> (int, bool) { index := int(id) return index, id != INVALID_SOURCE && index < count } diagnostic_index :: proc(id: Diagnostic_Id, count: int) -> (int, bool) { index := int(id) return index, id != INVALID_DIAGNOSTIC && index < count } fits_source_length :: proc(length: u64) -> bool { return length <= u64(max(Offset)) } Span :: struct { file: Source_Id, start: Offset, end: Offset, } Source :: struct { id: Source_Id, path: string, text: string, line_starts: []Offset, } Store :: struct { items: [dynamic]Source, allocator: mem.Allocator, } Diagnostic :: struct { span: Span, message: string, severity: Severity, } Severity :: enum u8 { Error, Warning, } Diagnostics :: struct { source: ^Source, store: ^Store, items: [dynamic]Diagnostic, index: map[Diagnostic_Key]Diagnostic_Id, allocator: mem.Allocator, } Diagnostic_Key :: struct { span: Span, message: string, severity: Severity, } init_store :: proc(allocator := context.allocator) -> Store { result: Store result.allocator = allocator result.items.allocator = allocator return result } destroy_store :: proc(store: ^Store) { for item in store.items { delete(item.path, store.allocator) delete(item.text, store.allocator) delete(item.line_starts, store.allocator) } delete(store.items) } make_line_starts :: proc(text: string, allocator: mem.Allocator) -> []Offset { assert(fits_source_length(u64(len(text)))) result: [dynamic]Offset result.allocator = allocator append(&result, 0) for value, offset in transmute([]byte)text { if value == '\n' { append(&result, Offset(offset+1)) } } return result[:] } add_source_owned :: proc(store: ^Store, path: string, text: []byte) -> Source_Id { assert(fits_source_length(u64(len(text)))) id := source_id(len(store.items)) owned_text := string(text) append(&store.items, Source{ id=id, path=fmt.aprintf("%s", path, allocator=store.allocator), text=owned_text, line_starts=make_line_starts(owned_text, store.allocator), }) return id } add_source :: proc(store: ^Store, path, text: string) -> Source_Id { owned := make([]byte, len(text), store.allocator) copy(owned, transmute([]byte)text) return add_source_owned(store, path, owned) } init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -> Diagnostics { result: Diagnostics result.source = source_file result.allocator = allocator result.items.allocator = allocator result.index.allocator = allocator return result } init_store_diagnostics :: proc(store: ^Store, allocator := context.allocator) -> Diagnostics { result: Diagnostics result.store = store result.allocator = allocator result.items.allocator = allocator result.index.allocator = allocator return result } destroy_diagnostics :: proc(diagnostics: ^Diagnostics) { delete(diagnostics.index) for diagnostic in diagnostics.items { delete(diagnostic.message, diagnostics.allocator) } delete(diagnostics.items) } add_with_severity :: proc(diagnostics: ^Diagnostics, span: Span, message: string, severity: Severity) -> Diagnostic_Id { key := Diagnostic_Key{span=span, message=message, severity=severity} if id, ok := diagnostics.index[key]; ok { return id } id := diagnostic_id(len(diagnostics.items)) cloned := fmt.aprintf("%s", message, allocator=diagnostics.allocator) append(&diagnostics.items, Diagnostic{span=span, message=cloned, severity=severity}) diagnostics.index[Diagnostic_Key{span=span, message=cloned, severity=severity}] = id return id } add :: proc(diagnostics: ^Diagnostics, span: Span, message: string) -> Diagnostic_Id { return add_with_severity(diagnostics, span, message, .Error) } add_warning :: proc(diagnostics: ^Diagnostics, span: Span, message: string) -> Diagnostic_Id { return add_with_severity(diagnostics, span, message, .Warning) } addf_with_severity :: proc(diagnostics: ^Diagnostics, span: Span, severity: Severity, format: string, args: ..any) -> Diagnostic_Id { message := fmt.aprintf(format, ..args, allocator=diagnostics.allocator) key := Diagnostic_Key{span=span, message=message, severity=severity} if id, ok := diagnostics.index[key]; ok { delete(message, diagnostics.allocator) return id } id := diagnostic_id(len(diagnostics.items)) append(&diagnostics.items, Diagnostic{span=span, message=message, severity=severity}) diagnostics.index[Diagnostic_Key{span=span, message=message, severity=severity}] = id return id } addf :: proc(diagnostics: ^Diagnostics, span: Span, format: string, args: ..any) -> Diagnostic_Id { return addf_with_severity(diagnostics, span, .Error, format, ..args) } addf_warning :: proc(diagnostics: ^Diagnostics, span: Span, format: string, args: ..any) -> Diagnostic_Id { return addf_with_severity(diagnostics, span, .Warning, format, ..args) } line_and_column :: proc(source_file: ^Source, offset: Offset) -> (line, column: int) { if len(source_file.line_starts) > 0 { limit := min(int(offset), len(source_file.text)) low := 0 high := len(source_file.line_starts) for low < high { middle := low + (high-low)/2 if int(source_file.line_starts[middle]) <= limit { low = middle+1 } else { high = middle } } line = max(low, 1) column = limit-int(source_file.line_starts[line-1])+1 return } line = 1 column = 1 limit := min(int(offset), len(source_file.text)) for byte_value in transmute([]byte)source_file.text[:limit] { if byte_value == '\n' { line += 1 column = 1 } else { column += 1 } } return } source_for_span :: proc(diagnostics: ^Diagnostics, span: Span) -> ^Source { if diagnostics.store != nil { if index, ok := source_index(span.file, len(diagnostics.store.items)); ok { return &diagnostics.store.items[index] } } return diagnostics.source } format :: proc(diagnostics: ^Diagnostics, id: Diagnostic_Id, allocator := context.allocator) -> string { index, ok := diagnostic_index(id, len(diagnostics.items)) if !ok { path := "" if diagnostics.source != nil { path = diagnostics.source.path } else if diagnostics.store != nil && len(diagnostics.store.items) > 0 { path = diagnostics.store.items[0].path } return fmt.aprintf("%s: compiler recovery error", path, allocator=allocator) } diagnostic := diagnostics.items[index] source_file := source_for_span(diagnostics, diagnostic.span) severity := "warning" if diagnostic.severity == .Warning else "error" if source_file == nil { return fmt.aprintf(": %s: %s", severity, diagnostic.message, allocator=allocator) } line, column := line_and_column(source_file, diagnostic.span.start) return fmt.aprintf( "%s:%d:%d: %s: %s", source_file.path, line, column, severity, diagnostic.message, allocator=allocator, ) } print_all :: proc(diagnostics: ^Diagnostics) { for _, id in diagnostics.items { message := format(diagnostics, diagnostic_id(id)) fmt.eprintln(message) delete(message) } }