From 5244bfbf1b04a17558e20ded8648be2cbcd821f4 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Sun, 9 Aug 2026 22:41:40 +0200 Subject: [PATCH] improve error messages --- compiler/checker/checker.odin | 80 ++++++++++++++++++++++++++++++++++- compiler_tests.odin | 30 ++++++++++++- 2 files changed, 108 insertions(+), 2 deletions(-) diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 9c5abc8..dadefb8 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -3905,17 +3905,83 @@ runtime_write_declaration_matches :: proc(checker: ^Checker, function: ast.Funct type_from_syntax(checker, function.result, function.pkg, function.file) == types.C_LONG } +signature_type_diagnostic :: proc( + checker: ^Checker, + function: ast.Function, + value: types.Type, + depth := 0, +) -> source.Diagnostic_Id { + if depth > 64 || is_type_metatype_syntax(checker, value) { + return source.INVALID_DIAGNOSTIC + } + item, ok := types.node(&checker.module.types, value) + if !ok { + return source.INVALID_DIAGNOSTIC + } + if item.kind == .Named && !item.declared && item.qualifier == 0 { + name := symbol.Id(item.name) + if param, found := comptime_param_for_name(function, name); + found && is_comptime_type_param(checker, param) { + return source.INVALID_DIAGNOSTIC + } + span := function.span + for type_use in checker.ast_module.type_uses { + if type_use.file == function.file && type_use.type == value && + type_use.span.start >= function.span.start && type_use.span.end <= function.span.end { + span = type_use.span + break + } + } + diagnostic := source.addf( + checker.diagnostics, + span, + "unknown symbol '%s'", + symbol_text(checker, name), + ) + source.set_primary_label(checker.diagnostics, diagnostic, "unknown symbol") + return diagnostic + } + if diagnostic := signature_type_diagnostic( + checker, function, item.child, depth+1, + ); diagnostic != source.INVALID_DIAGNOSTIC { + return diagnostic + } + if diagnostic := signature_type_diagnostic( + checker, function, item.extra, depth+1, + ); diagnostic != source.INVALID_DIAGNOSTIC { + return diagnostic + } + if item.kind == .Function { + for param in types.params_for(&checker.module.types, value) { + if diagnostic := signature_type_diagnostic( + checker, function, param.type, depth+1, + ); diagnostic != source.INVALID_DIAGNOSTIC { + return diagnostic + } + } + } + return source.INVALID_DIAGNOSTIC +} + validate_declarations :: proc(checker: ^Checker) { for function, function_id in checker.ast_module.functions { if len(function.unsupported_reason) > 0 { continue } has_comptime := function_has_comptime_params(function) - signature_poisoned := function.diagnostic != source.INVALID_DIAGNOSTIC + signature_poisoned := checker.template_diagnostics[function_id] != source.INVALID_DIAGNOSTIC locals: [dynamic]symbol.Id locals.allocator = checker.allocator comptime_prefix := 0 for param in function.params { + if !signature_poisoned { + if diagnostic := signature_type_diagnostic( + checker, function, param.type, + ); diagnostic != source.INVALID_DIAGNOSTIC { + checker.template_diagnostics[function_id] = diagnostic + signature_poisoned = true + } + } dependent := param.comptime_value && type_pattern_mentions_comptime( checker, function, comptime_prefix, param.type, ) @@ -3994,6 +4060,18 @@ validate_declarations :: proc(checker: ^Checker) { ) } } + if !signature_poisoned { + signature_types := [2]types.Type{function.result, function.error} + for value in signature_types { + if diagnostic := signature_type_diagnostic( + checker, function, value, + ); diagnostic != source.INVALID_DIAGNOSTIC { + checker.template_diagnostics[function_id] = diagnostic + signature_poisoned = true + break + } + } + } if !has_comptime && !signature_poisoned { result_type := type_from_syntax(checker, function.result, function.pkg, function.file) if function.c_abi && types.is_noreturn(result_type) { diff --git a/compiler_tests.odin b/compiler_tests.odin index 5bbf7cf..11a59e1 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -10432,7 +10432,7 @@ package_and_file_local_visibility_is_enforced :: proc(t: ^testing.T) { found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'") found_file_sibling = found_file_sibling || strings.contains(diagnostic.message, "unknown symbol 'file_sibling'") found_file_sibling_value = found_file_sibling_value || strings.contains(diagnostic.message, "unknown symbol 'file_sibling_value'") - found_file_sibling_type = found_file_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type 'File_Sibling'") + found_file_sibling_type = found_file_sibling_type || strings.contains(diagnostic.message, "unknown symbol 'File_Sibling'") } testing.expect(t, !found_sibling) testing.expect(t, !found_sibling_value) @@ -15042,6 +15042,34 @@ missing_qualified_signature_symbol_reports_one_root_error :: proc(t: ^testing.T) testing.expect(t, !strings.contains(formatted, "could not resolve the 'int' constraint")) } +@(test) +missing_unqualified_signature_type_reports_one_root_error :: proc(t: ^testing.T) { + text := `render_stmt func(node NodeId) void {} +main func() void { + render_stmt(0) +} +` + source_file := source.Source{path="renderer.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) + + testing.expect_value(t, len(diagnostics.items), 1) + testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'NodeId'") + formatted := source.format(&diagnostics, source.Diagnostic_Id(0)) + defer delete(formatted) + testing.expect(t, strings.contains(formatted, "renderer.bro:1:23")) + testing.expect(t, strings.contains(formatted, "^^^^^^ unknown symbol")) + testing.expect(t, !strings.contains(formatted, "could not resolve specialization")) +} + @(test) poisoned_global_and_local_types_do_not_create_inference_fallbacks :: proc(t: ^testing.T) { directory := "/tmp/brolang-test-poisoned-declarations"