From eb06b2ee629806c4e25f3f87d767be61b5f76e3f Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 30 Jun 2026 22:20:35 +0200 Subject: [PATCH] report global cycle endpoints --- TODO.md | 16 ++++++---------- compiler/checker/checker.odin | 33 +++++++++++++++++++++++++++++++-- compiler_tests.odin | 27 +++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/TODO.md b/TODO.md index 62f95e0..a92a88a 100644 --- a/TODO.md +++ b/TODO.md @@ -1,6 +1,10 @@ -# "quick" / "easy" fixes +# general todos -- for global initialization cycles, report also starting and ending lines +- sum-type ABI/layout polish + - consider dynamic tag-width shrinking after the fixed-`u16` ABI has real pressure + - consider all-void channel collapse after fallible channels are otherwise stable + - define cross-module/global-id ABI determinism before multi-module builds depend on it + - keep backed/C enum composition and must-consume fallible linting as later policy work # milestones @@ -590,17 +594,9 @@ spelling for void-payload variants - named fallible signatures can use inline unbacked enum and `union(enum)` error types after `!` - - qualified same-name disambiguation remains deferred; existing ambiguous fallible return - diagnostics cover the current surface 23.7. anonymous struct payloads and keyed payload sugar -23.8. sum-type ABI/layout polish - - consider dynamic tag-width shrinking after the fixed-`u16` ABI has real pressure - - consider all-void channel collapse after fallible channels are otherwise stable - - define cross-module/global-id ABI determinism before multi-module builds depend on it - - keep backed/C enum composition and must-consume fallible linting as later policy work - 24. dynamic heap allocation - see below for direction - notes below are too big in scope for a first pass and the language is not mature enough to support it yet diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 0947816..59c6c87 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -7088,6 +7088,28 @@ Cycle_Frame :: struct { next_dependency: int, } +cycle_global_location :: proc(checker: ^Checker, global_id: hir.Global_Id) -> (name, path: string, line: int) { + name = "" + path = "" + line = 1 + index := int(global_id) + if global_id == hir.INVALID_GLOBAL || index >= len(checker.module.globals) { + return + } + name = symbol_text(checker, checker.module.globals[global_id].name) + if index >= len(checker.ast_module.globals) { + return + } + span := checker.ast_module.globals[global_id].span + source_file := source.source_for_span(checker.diagnostics, span) + if source_file == nil { + return + } + path = source_file.path + line, _ = source.line_and_column(source_file, span.start) + return +} + detect_global_cycles_visit :: proc(checker: ^Checker, global_id: hir.Global_Id, states: []u8) { if states[global_id] == 2 { return @@ -7120,11 +7142,18 @@ detect_global_cycles_visit :: proc(checker: ^Checker, global_id: hir.Global_Id, continue } if states[dependency] == 1 { + start_name, start_path, start_line := cycle_global_location(checker, dependency) + end_name, end_path, end_line := cycle_global_location(checker, frame.global) id := source.addf( checker.diagnostics, checker.ast_module.globals[dependency].span, - "global initialization cycle involving '%s'", - symbol_text(checker, checker.module.globals[dependency].name), + "global initialization cycle from '%s' at %s:%d to '%s' at %s:%d", + start_name, + start_path, + start_line, + end_name, + end_path, + end_line, ) checker.module.globals[dependency].diagnostic = id checker.module.globals[dependency].problematic = true diff --git a/compiler_tests.odin b/compiler_tests.odin index f06a1e6..caf3608 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -4684,6 +4684,33 @@ hundred_thousand_term_runtime_addition_uses_iterative_pipeline :: proc(t: ^testi testing.expect(t, instruction_count > 100_000) } +@(test) +global_cycle_diagnostic_reports_endpoint_lines :: proc(t: ^testing.T) { + text := `a i32 :: b +b i32 :: a +main func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found := false + for _, diagnostic_index in diagnostics.items { + message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index)) + found = found || strings.contains(message, "global initialization cycle from 'a' at test.bro:1 to 'b' at test.bro:2") + delete(message) + } + testing.expect(t, found) +} + @(test) deep_global_cycle_detection_uses_iterative_dfs :: proc(t: ^testing.T) { count := 50_000