diagnostics upgrade

This commit is contained in:
2026-07-15 22:55:31 +02:00
parent c4fa8e930f
commit 7de0b7f268
11 changed files with 857 additions and 75 deletions
+233 -10
View File
@@ -2,6 +2,7 @@ package source
import "core:fmt"
import "core:mem"
import "core:strings"
Source_Id :: distinct u32
Diagnostic_Id :: distinct u32
@@ -58,6 +59,20 @@ Diagnostic :: struct {
severity: Severity,
}
Annotation_Kind :: enum u8 {
Primary,
Secondary,
Note,
Help,
}
Annotation :: struct {
owner: Diagnostic_Id,
span: Span,
message: string,
kind: Annotation_Kind,
}
Severity :: enum u8 {
Error,
Warning,
@@ -67,6 +82,7 @@ Diagnostics :: struct {
source: ^Source,
store: ^Store,
items: [dynamic]Diagnostic,
annotations: [dynamic]Annotation,
index: map[Diagnostic_Key]Diagnostic_Id,
allocator: mem.Allocator,
}
@@ -130,6 +146,7 @@ init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -
result.source = source_file
result.allocator = allocator
result.items.allocator = allocator
result.annotations.allocator = allocator
result.index.allocator = allocator
return result
}
@@ -139,6 +156,7 @@ init_store_diagnostics :: proc(store: ^Store, allocator := context.allocator) ->
result.store = store
result.allocator = allocator
result.items.allocator = allocator
result.annotations.allocator = allocator
result.index.allocator = allocator
return result
}
@@ -148,7 +166,11 @@ destroy_diagnostics :: proc(diagnostics: ^Diagnostics) {
for diagnostic in diagnostics.items {
delete(diagnostic.message, diagnostics.allocator)
}
for annotation in diagnostics.annotations {
delete(annotation.message, diagnostics.allocator)
}
delete(diagnostics.items)
delete(diagnostics.annotations)
}
add_with_severity :: proc(diagnostics: ^Diagnostics, span: Span, message: string, severity: Severity) -> Diagnostic_Id {
@@ -192,6 +214,50 @@ addf_warning :: proc(diagnostics: ^Diagnostics, span: Span, format: string, args
return addf_with_severity(diagnostics, span, .Warning, format, ..args)
}
add_annotation :: proc(
diagnostics: ^Diagnostics,
owner: Diagnostic_Id,
kind: Annotation_Kind,
span: Span,
message: string,
) {
if _, ok := diagnostic_index(owner, len(diagnostics.items)); !ok {
return
}
for annotation in diagnostics.annotations {
if annotation.owner == owner && annotation.kind == kind && annotation.span == span &&
annotation.message == message {
return
}
}
append(&diagnostics.annotations, Annotation{
owner=owner,
kind=kind,
span=span,
message=fmt.aprintf("%s", message, allocator=diagnostics.allocator),
})
}
set_primary_label :: proc(diagnostics: ^Diagnostics, owner: Diagnostic_Id, message: string) {
index, ok := diagnostic_index(owner, len(diagnostics.items))
if !ok {
return
}
add_annotation(diagnostics, owner, .Primary, diagnostics.items[index].span, message)
}
add_secondary_label :: proc(diagnostics: ^Diagnostics, owner: Diagnostic_Id, span: Span, message: string) {
add_annotation(diagnostics, owner, .Secondary, span, message)
}
add_note :: proc(diagnostics: ^Diagnostics, owner: Diagnostic_Id, message: string) {
add_annotation(diagnostics, owner, .Note, {}, message)
}
add_help :: proc(diagnostics: ^Diagnostics, owner: Diagnostic_Id, message: string) {
add_annotation(diagnostics, owner, .Help, {}, message)
}
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))
@@ -232,6 +298,140 @@ source_for_span :: proc(diagnostics: ^Diagnostics, span: Span) -> ^Source {
return diagnostics.source
}
annotation_for :: proc(diagnostics: ^Diagnostics, owner: Diagnostic_Id, kind: Annotation_Kind) -> (Annotation, bool) {
for annotation in diagnostics.annotations {
if annotation.owner == owner && annotation.kind == kind {
return annotation, true
}
}
return {}, false
}
line_bounds :: proc(source_file: ^Source, line: int) -> (start, end: int, ok: bool) {
if line <= 0 {
return 0, 0, false
}
if len(source_file.line_starts) == 0 {
current := 1
start = 0
for value, index in transmute([]byte)source_file.text {
if current == line && value == '\n' {
end = index
if end > start && source_file.text[end-1] == '\r' {
end -= 1
}
return start, end, true
}
if value == '\n' {
current += 1
start = index+1
}
}
if current == line {
return start, len(source_file.text), true
}
return 0, 0, false
}
if line > len(source_file.line_starts) {
return 0, 0, false
}
start = int(source_file.line_starts[line-1])
end = len(source_file.text)
if line < len(source_file.line_starts) {
end = int(source_file.line_starts[line])-1
}
if end > start && source_file.text[end-1] == '\r' {
end -= 1
}
return start, end, true
}
write_spaces :: proc(builder: ^strings.Builder, count: int) {
for _ in 0..<max(count, 0) {
strings.write_byte(builder, ' ')
}
}
write_expanded :: proc(builder: ^strings.Builder, text: string, start_column := 0) -> int {
column := start_column
for value in transmute([]byte)text {
if value == '\t' {
width := 4-column%4
write_spaces(builder, width)
column += width
} else {
strings.write_byte(builder, value)
column += 1
}
}
return column
}
display_width :: proc(text: string, start_column := 0) -> int {
column := start_column
for value in transmute([]byte)text {
column += 4-column%4 if value == '\t' else 1
}
return column-start_column
}
decimal_width :: proc(value: int) -> int {
width := 1
for remaining := value; remaining >= 10; remaining /= 10 {
width += 1
}
return width
}
write_excerpt :: proc(
builder: ^strings.Builder,
diagnostics: ^Diagnostics,
span: Span,
label: string,
primary: bool,
) -> bool {
if span == (Span{}) {
return false
}
source_file := source_for_span(diagnostics, span)
if source_file == nil {
return false
}
line, column := line_and_column(source_file, span.start)
line_start, line_end, ok := line_bounds(source_file, line)
if !ok {
return false
}
prefix := " -->" if primary else " :::"
fmt.sbprintf(builder, "%s %s:%d:%d\n", prefix, source_file.path, line, column)
gutter := decimal_width(line)
write_spaces(builder, gutter+1)
strings.write_string(builder, "|\n")
fmt.sbprintf(builder, "%*d | ", gutter, line)
_ = write_expanded(builder, source_file.text[line_start:line_end])
strings.write_byte(builder, '\n')
write_spaces(builder, gutter+1)
strings.write_string(builder, "| ")
start := clamp(int(span.start), line_start, line_end)
indent := display_width(source_file.text[line_start:start])
width := 1
if start < line_end {
end := clamp(int(span.end), start+1, line_end)
width = max(display_width(source_file.text[start:end], indent), 1)
}
write_spaces(builder, indent)
marker := u8('^') if primary else u8('-')
for _ in 0..<width {
strings.write_byte(builder, marker)
}
if len(label) > 0 {
strings.write_byte(builder, ' ')
strings.write_string(builder, label)
}
strings.write_byte(builder, '\n')
return true
}
format :: proc(diagnostics: ^Diagnostics, id: Diagnostic_Id, allocator := context.allocator) -> string {
index, ok := diagnostic_index(id, len(diagnostics.items))
if !ok {
@@ -246,19 +446,42 @@ format :: proc(diagnostics: ^Diagnostics, id: Diagnostic_Id, allocator := contex
diagnostic := diagnostics.items[index]
source_file := source_for_span(diagnostics, diagnostic.span)
severity := "warning" if diagnostic.severity == .Warning else "error"
if diagnostic.span == (Span{}) {
path := source_file.path if source_file != nil else "<unknown>"
return fmt.aprintf("%s: %s: %s", path, severity, diagnostic.message, allocator=allocator)
}
if source_file == nil {
return fmt.aprintf("<unknown>: %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,
)
builder := strings.builder_make(allocator)
fmt.sbprintf(&builder, "%s: %s\n", severity, diagnostic.message)
primary_label := ""
if annotation, found := annotation_for(diagnostics, id, .Primary); found {
primary_label = annotation.message
}
_ = write_excerpt(&builder, diagnostics, diagnostic.span, primary_label, true)
for annotation in diagnostics.annotations {
if annotation.owner == id && annotation.kind == .Secondary {
_ = write_excerpt(&builder, diagnostics, annotation.span, annotation.message, false)
}
}
for annotation in diagnostics.annotations {
if annotation.owner != id {
continue
}
#partial switch annotation.kind {
case .Note:
fmt.sbprintf(&builder, "note: %s\n", annotation.message)
case .Help:
fmt.sbprintf(&builder, "help: %s\n", annotation.message)
case:
}
}
result := strings.to_string(builder)
if len(result) > 0 && result[len(result)-1] == '\n' {
return result[:len(result)-1]
}
return result
}
print_all :: proc(diagnostics: ^Diagnostics) {