From 7de0b7f268f90385208102741d2a56d652c5727b Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 15 Jul 2026 22:55:31 +0200 Subject: [PATCH] diagnostics upgrade --- compiler/ast/ast.odin | 11 ++ compiler/checker/checker.odin | 312 ++++++++++++++++++++++++++++++---- compiler/loader/loader.odin | 99 ++++++++++- compiler/parser/parser.odin | 7 + compiler/source/source.odin | 243 ++++++++++++++++++++++++-- compiler_tests.odin | 237 ++++++++++++++++++++++++-- std/arraylist/arraylist.bro | 2 +- std/debug/debug.bro | 4 +- std/io/io.bro | 13 +- std/mem/mem.bro | 2 +- std/process/process.bro | 2 +- 11 files changed, 857 insertions(+), 75 deletions(-) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index 3582ced..27084cd 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -63,6 +63,14 @@ index :: proc(id: $T, invalid: T, count: int) -> (int, bool) { Type_Syntax :: types.Type +Type_Use :: struct { + type: Type_Syntax, + span: source.Span, + pkg: Package_Id, + file: File_Id, + diagnostic: source.Diagnostic_Id, +} + Expr_Kind :: enum u8 { Invalid, Integer, @@ -325,6 +333,7 @@ Module :: struct { c_trampolines: [dynamic]Trampoline, strings: [dynamic]string, type_fields: [dynamic]types.Field, + type_uses: [dynamic]Type_Use, type_store: types.Store, allocator: mem.Allocator, } @@ -345,6 +354,7 @@ init_module :: proc(allocator := context.allocator) -> Module { module.c_trampolines.allocator = allocator module.strings.allocator = allocator module.type_fields.allocator = allocator + module.type_uses.allocator = allocator return module } @@ -397,5 +407,6 @@ destroy_module :: proc(module: ^Module) { delete(module.c_trampolines) delete(module.strings) delete(module.type_fields) + delete(module.type_uses) types.destroy_store(&module.type_store) } diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index fd00e7f..e11105c 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -208,6 +208,7 @@ Checker :: struct { record_field_conflicts: []types.Type, record_field_conflict_spans: []source.Span, record_field_demands_dirty: bool, + poisoned_packages: []bool, external_global_canonical: []ast.Global_Id, external_global_diagnostics: []source.Diagnostic_Id, constants: []Constant, @@ -1496,16 +1497,30 @@ expr_lookup_file :: proc(expr: ast.Expr, file: ast.File_Id) -> ast.File_Id { return ast.INVALID_FILE if symbol.is_valid(expr.qualifier) else file } +expr_symbol_span :: proc(checker: ^Checker, expr: ast.Expr, name: symbol.Id) -> source.Span { + span := expr.span + span.end = span.start+source.Offset(len(symbol_text(checker, name))) + return span +} + add_package_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, file: ast.File_Id) -> source.Diagnostic_Id { if find_import(checker, file, expr.qualifier) == ast.INVALID_IMPORT { - return source.addf(checker.diagnostics, expr.span, "unknown package alias '%s'", symbol_text(checker, expr.qualifier)) + span := expr_symbol_span(checker, expr, expr.qualifier) + id := source.addf(checker.diagnostics, span, "unknown symbol '%s'", symbol_text(checker, expr.qualifier)) + source.set_primary_label(checker.diagnostics, id, "unknown symbol") + return id } return source.addf(checker.diagnostics, expr.span, "unavailable imported package '%s'", symbol_text(checker, expr.qualifier)) } add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id { if find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_FUNCTION { - return source.addf(checker.diagnostics, expr.span, "'%s' is a function, not a global value", symbol_text(checker, expr.name)) + template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) + id := source.addf(checker.diagnostics, expr_symbol_span(checker, expr, expr.name), "'%s' is a function, not a global value", symbol_text(checker, expr.name)) + if !checker.ast_module.functions[template].imported { + source.add_secondary_label(checker.diagnostics, id, checker.ast_module.functions[template].span, "function declared here") + } + return id } if symbol.is_valid(expr.qualifier) { return source.addf( @@ -1516,12 +1531,20 @@ add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target symbol_text(checker, expr.name), ) } - return source.addf(checker.diagnostics, expr.span, "unresolved global '%s'", symbol_text(checker, expr.name)) + span := expr_symbol_span(checker, expr, expr.name) + id := source.addf(checker.diagnostics, span, "unknown symbol '%s'", symbol_text(checker, expr.name)) + source.set_primary_label(checker.diagnostics, id, "unknown symbol") + return id } add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target_pkg: ast.Package_Id, file: ast.File_Id) -> source.Diagnostic_Id { if find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) != ast.INVALID_GLOBAL { - return source.addf(checker.diagnostics, expr.span, "'%s' is a global, not a function", symbol_text(checker, expr.name)) + global := find_global(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) + id := source.addf(checker.diagnostics, expr_symbol_span(checker, expr, expr.name), "'%s' is a global, not a function", symbol_text(checker, expr.name)) + if !checker.ast_module.globals[global].external { + source.add_secondary_label(checker.diagnostics, id, checker.ast_module.globals[global].span, "global declared here") + } + return id } if symbol.is_valid(expr.qualifier) { return source.addf( @@ -1532,7 +1555,10 @@ add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target symbol_text(checker, expr.name), ) } - return source.addf(checker.diagnostics, expr.span, "unresolved function '%s'", symbol_text(checker, expr.name)) + span := expr_symbol_span(checker, expr, expr.name) + id := source.addf(checker.diagnostics, span, "unknown symbol '%s'", symbol_text(checker, expr.name)) + source.set_primary_label(checker.diagnostics, id, "unknown symbol") + return id } find_unsupported :: proc(checker: ^Checker, pkg: ast.Package_Id, name: symbol.Id) -> (ast.Unsupported, bool) { @@ -2649,6 +2675,7 @@ resolve_type_factory_call :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: } target_pkg, available := expr_package(checker, expr, pkg, file, true) if !available { + _ = add_package_resolution_diagnostic(checker, expr, file) return types.INVALID } template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) @@ -3145,6 +3172,7 @@ validate_declarations :: proc(checker: ^Checker) { continue } has_comptime := function_has_comptime_params(function) + signature_poisoned := function.diagnostic != source.INVALID_DIAGNOSTIC locals: [dynamic]symbol.Id locals.allocator = checker.allocator for param in function.params { @@ -3152,7 +3180,7 @@ validate_declarations :: proc(checker: ^Checker) { if param.comptime_value || !has_comptime { param_type = type_from_syntax(checker, param.type, function.pkg, function.file) } - if param.comptime_value { + if param.comptime_value && !signature_poisoned { if function.c_abi { checker.template_diagnostics[function_id] = source.add( checker.diagnostics, @@ -3169,7 +3197,7 @@ validate_declarations :: proc(checker: ^Checker) { symbol_text(checker, param.name), ) } - } else if !has_comptime { + } else if !has_comptime && !signature_poisoned { if diagnostic := add_unsupported_type_diagnostic(checker, param.span, param_type); diagnostic != source.INVALID_DIAGNOSTIC { checker.template_diagnostics[function_id] = diagnostic @@ -3197,7 +3225,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } append(&locals, param.name) - if !has_comptime && types.contains_c_struct_by_value(param_type, &checker.module.types) { + if !has_comptime && !signature_poisoned && types.contains_c_struct_by_value(param_type, &checker.module.types) { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, param.span, @@ -3206,7 +3234,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - if !has_comptime { + if !has_comptime && !signature_poisoned { result_type := type_from_syntax(checker, function.result, function.pkg, function.file) if diagnostic := add_unsupported_type_diagnostic(checker, function.span, result_type); diagnostic != source.INVALID_DIAGNOSTIC { @@ -3221,7 +3249,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - if types.is_valid(function.error) { + if types.is_valid(function.error) && !signature_poisoned { error_type := type_from_syntax(checker, function.error, function.pkg, function.file) error_sum := types.is_enum(error_type, &checker.module.types) || types.is_tagged_union(error_type, &checker.module.types) @@ -3239,7 +3267,7 @@ validate_declarations :: proc(checker: ^Checker) { ) } } - if !function.has_body && !function.c_abi { + if !function.has_body && !function.c_abi && !signature_poisoned { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, @@ -3247,7 +3275,7 @@ validate_declarations :: proc(checker: ^Checker) { symbol_text(checker, function.name), ) } - if function.variadic && (!function.c_abi || function.has_body) { + if function.variadic && (!function.c_abi || function.has_body) && !signature_poisoned { checker.template_diagnostics[function_id] = source.addf( checker.diagnostics, function.span, @@ -3255,7 +3283,7 @@ validate_declarations :: proc(checker: ^Checker) { symbol_text(checker, function.name), ) } - if !function.has_body && function.c_abi { + if !function.has_body && function.c_abi && !signature_poisoned { for param in function.params { param_type := type_from_syntax(checker, param.type, function.pkg, function.file) if add_unsupported_type_diagnostic(checker, param.span, param_type) != @@ -3351,7 +3379,7 @@ is_inferred_record_field :: proc(checker: ^Checker, slot: int) -> bool { types.is_constraint(checker.record_field_constraints[slot]) } -record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.Id, bool) { +record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.Id, ast.Package_Id, bool) { for item in checker.module.types.nodes { if !(item.declared && (item.kind == .Struct || item.kind == .Union) && !item.c_layout && symbol.is_valid(symbol.Id(item.name))) { @@ -3360,10 +3388,10 @@ record_field_owner :: proc(checker: ^Checker, slot: int) -> (symbol.Id, symbol.I start := int(item.field_start) if slot >= start && slot < start+int(item.field_count) && slot >= 0 && slot < len(checker.module.types.fields) { - return symbol.Id(item.name), symbol.Id(checker.module.types.fields[slot].name), true + return symbol.Id(item.name), symbol.Id(checker.module.types.fields[slot].name), ast.Package_Id(item.pkg), true } } - return symbol.INVALID, symbol.INVALID, false + return symbol.INVALID, symbol.INVALID, ast.INVALID_PACKAGE, false } record_field_conflict :: proc(checker: ^Checker, slot: int, actual: types.Type, span: source.Span) { @@ -3484,7 +3512,7 @@ finalize_record_field_inference :: proc(checker: ^Checker) { if !types.is_constraint(constraint) { continue } - record_name, field_name, ok := record_field_owner(checker, slot) + record_name, field_name, record_pkg, ok := record_field_owner(checker, slot) if !ok { continue } @@ -3509,7 +3537,8 @@ finalize_record_field_inference :: proc(checker: ^Checker) { symbol_text(checker, record_name), symbol_text(checker, field_name), ) } - } else if types.is_constraint(current) { + } else if types.is_constraint(current) && + !(int(record_pkg) < len(checker.poisoned_packages) && checker.poisoned_packages[record_pkg]) { source.addf( checker.diagnostics, source.Span{}, @@ -5622,6 +5651,10 @@ infer_all :: proc(checker: ^Checker) { // Demands accumulate in global_demands so the default never blocks a later // cross-family demand (e.g. integer literal -> unsigned or float). for global, index in checker.ast_module.globals { + if global.diagnostic != source.INVALID_DIAGNOSTIC { + checker.global_types[index] = types.I64 + continue + } declared := resolve_inferred_array( checker, type_from_syntax(checker, global.type, global.pkg, global.file), @@ -5675,7 +5708,7 @@ infer_all :: proc(checker: ^Checker) { // Backward demands: a global pushes its own (declared or already-resolved) type // onto open numeric slots reachable through names and numeric arithmetic. for global, index in checker.ast_module.globals { - if global.external { + if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC { continue } demand := checker.global_types[index] @@ -5689,7 +5722,7 @@ infer_all :: proc(checker: ^Checker) { // concrete-typed ones) for its side effect of specializing called functions and // recording demands from call arguments in their initializers. for global, index in checker.ast_module.globals { - if global.external { + if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC { continue } declared := resolve_inferred_array( @@ -5765,7 +5798,8 @@ infer_all :: proc(checker: ^Checker) { // No authoritative demand can still arrive. Assign final defaults, then // continue the same fixpoint so dependent globals/specs observe them. for global, index in checker.ast_module.globals { - if global.external || is_runtime_type(checker, checker.global_types[index]) { + if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC || + is_runtime_type(checker, checker.global_types[index]) { continue } if checker.global_open_const[index] { @@ -5807,7 +5841,7 @@ prune_specs :: proc(checker: ^Checker) { mark_spec_demanded(checker, find_spec(checker, checker.io_provider_template, nil), &stack) } for global in checker.ast_module.globals { - if global.external { + if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC { continue } _ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack) @@ -5859,6 +5893,23 @@ invalid_hir_expr :: proc( ) } +invalid_expr_diagnostic :: proc(checker: ^Checker, id: hir.Expr_Id) -> (source.Diagnostic_Id, bool) { + if id == hir.INVALID_EXPR || int(id) >= len(checker.module.exprs) { + return source.INVALID_DIAGNOSTIC, false + } + expr := checker.module.exprs[id] + return expr.diagnostic, expr.kind == .Invalid && expr.diagnostic != source.INVALID_DIAGNOSTIC +} + +propagate_invalid_expr :: proc(checker: ^Checker, span: source.Span, values: ..hir.Expr_Id) -> (hir.Expr_Id, bool) { + for value in values { + if diagnostic, invalid := invalid_expr_diagnostic(checker, value); invalid { + return invalid_hir_expr(checker, span, diagnostic), true + } + } + return hir.INVALID_EXPR, false +} + add_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) { for existing in values { if existing == value { @@ -5935,6 +5986,9 @@ coerce_expr :: proc( if expr_id == hir.INVALID_EXPR { return expr_id } + if _, invalid := invalid_expr_diagnostic(checker, expr_id); invalid { + return expr_id + } actual := checker.module.exprs[expr_id].type if types.equal(actual, expected) { return expr_id @@ -6918,6 +6972,9 @@ build_compound_expr :: proc( }) } value := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, value); propagated { + return invalid + } if !hir_is_location(checker, value) { id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location") return invalid_hir_expr(checker, expr.span, id) @@ -6936,6 +6993,9 @@ build_compound_expr :: proc( }) case .Deref: pointer := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, pointer); propagated { + return invalid + } pointer_type := checker.module.exprs[pointer].type if !types.is_pointer(pointer_type, store) { id := source.add(checker.diagnostics, expr.span, "postfix '^' requires a pointer") @@ -6949,6 +7009,9 @@ build_compound_expr :: proc( container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) index := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.USIZE, pkg, file) index = coerce_expr(checker, index, types.USIZE, expr.span) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, container, index); propagated { + return invalid + } container_type := checker.module.exprs[container].type item, ok := types.container(container_type, store) if !ok { @@ -6970,6 +7033,9 @@ build_compound_expr :: proc( }) } container := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, container); propagated { + return invalid + } container_type := checker.module.exprs[container].type item, ok := types.container(container_type, store) if !ok { @@ -6987,6 +7053,10 @@ build_compound_expr :: proc( if bound != ast.INVALID_EXPR { bounds[index] = build_nested_expr(checker, bound, locals, global_reads, calls, types.USIZE, pkg, file) bounds[index] = coerce_expr(checker, bounds[index], types.USIZE, checker.ast_module.exprs[bound].span) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, bounds[index]); propagated { + delete(bounds, checker.allocator) + return invalid + } } } preserve_sentinel := item.has_sentinel && expr.args[1] == ast.INVALID_EXPR @@ -7000,11 +7070,17 @@ build_compound_expr :: proc( return enum_member_hir(checker, enum_type, expr.name, expr.span) } base := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, base); propagated { + return invalid + } base_type := checker.module.exprs[base].type result, _ := build_field_from_value(checker, expr, base, base_type) return result case .Unwrap: optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, optional); propagated { + return invalid + } optional_type := checker.module.exprs[optional].type if !types.is_optional(optional_type, store) { id := source.add(checker.diagnostics, expr.span, "postfix '?' requires an optional") @@ -7016,6 +7092,9 @@ build_compound_expr :: proc( }) case .Orelse: optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, optional); propagated { + return invalid + } optional_type := checker.module.exprs[optional].type if !types.is_optional(optional_type, store) { id := source.add(checker.diagnostics, expr.span, "'orelse' requires an optional left operand") @@ -7035,6 +7114,9 @@ build_compound_expr :: proc( } left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file) + if diagnostic, invalid := invalid_expr_diagnostic(checker, channel); invalid { + return invalid_hir_expr(checker, expr.span, diagnostic) + } channel_type := checker.module.exprs[channel].type success := types.fallible_success(channel_type, store) if !types.is_valid(success) { @@ -7076,6 +7158,9 @@ build_compound_expr :: proc( case .Catch: left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID channel := build_nested_expr(checker, expr.left, locals, global_reads, calls, left_expected, pkg, file) + if diagnostic, invalid := invalid_expr_diagnostic(checker, channel); invalid { + return invalid_hir_expr(checker, expr.span, diagnostic) + } channel_type := checker.module.exprs[channel].type success := types.fallible_success(channel_type, store) if !types.is_valid(success) { @@ -7145,6 +7230,9 @@ build_compound_expr :: proc( left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) } + if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated { + return invalid + } child := expected_child if !types.is_valid(child) { child = types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) @@ -7174,6 +7262,9 @@ build_compound_expr :: proc( }) case .Not: operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, operand); propagated { + return invalid + } operand_type := checker.module.exprs[operand].type if checker.module.exprs[operand].kind != .Invalid && !types.is_bool(operand_type) { id := source.add(checker.diagnostics, expr.span, "'!' requires a bool operand") @@ -7186,6 +7277,9 @@ build_compound_expr :: proc( case .And, .Or: left := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) right := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.BOOL, pkg, file) + if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated { + return invalid + } left_type := checker.module.exprs[left].type right_type := checker.module.exprs[right].type left_ok := checker.module.exprs[left].kind == .Invalid || types.is_bool(left_type) @@ -7226,11 +7320,11 @@ build_compound_expr :: proc( left = build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) right = build_nested_expr(checker, expr.right, locals, global_reads, calls, types.INVALID, pkg, file) } + if invalid, propagated := propagate_invalid_expr(checker, expr.span, left, right); propagated { + return invalid + } left_type := checker.module.exprs[left].type right_type := checker.module.exprs[right].type - if checker.module.exprs[left].kind == .Invalid || checker.module.exprs[right].kind == .Invalid { - return invalid_hir_expr(checker, expr.span, expr.diagnostic, types.BOOL) - } operand_type := types.INVALID if types.is_enum(left_type, store) || types.is_enum(right_type, store) { if !types.equal(left_type, right_type) || (expr.kind != .Eq && expr.kind != .Ne) { @@ -7420,6 +7514,9 @@ build_binary_arith :: proc( left, right: hir.Expr_Id, span: source.Span, ) -> hir.Expr_Id { + if invalid, propagated := propagate_invalid_expr(checker, span, left, right); propagated { + return invalid + } // Pointer arithmetic is only defined for `+` (many-pointer + usize). if op == .Add && types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) && @@ -7960,6 +8057,9 @@ build_expr :: proc( checker.diagnostics, expr.span, message, symbol_text(checker, expr.name), len(function.params), len(expr.args), ) + if !function.imported { + source.add_secondary_label(checker.diagnostics, id, function.span, "function declared here") + } } else if len(mapping_failure) > 0 { id = source.addf( checker.diagnostics, expr.span, @@ -8009,6 +8109,11 @@ build_expr :: proc( } if frame.stage == 5 { operand := last + if invalid, propagated := propagate_invalid_expr(checker, expr.span, operand); propagated { + last = invalid + _ = pop(&stack) + continue + } operand_type := checker.module.exprs[operand].type if !types.is_signed(operand_type, checker.target) && !types.is_float(operand_type, checker.target) { id := source.add(checker.diagnostics, expr.span, "negation requires a signed integer or float") @@ -8071,6 +8176,17 @@ build_expr :: proc( continue } } + if invalid, propagated := propagate_invalid_expr(checker, expr.span, ..stack[frame_index].built_args); propagated { + delete(stack[frame_index].arg_types, checker.allocator) + stack[frame_index].arg_types = nil + delete(stack[frame_index].built_args, checker.allocator) + stack[frame_index].built_args = nil + delete(stack[frame_index].mapping, checker.allocator) + stack[frame_index].mapping = nil + last = invalid + _ = pop(&stack) + continue + } function := checker.ast_module.functions[frame.template] comptime_values: []Comptime_Value comptime_ok := false @@ -8236,6 +8352,11 @@ build_expr :: proc( } if frame.stage == 6 { callee := last + if invalid, propagated := propagate_invalid_expr(checker, expr.span, callee); propagated { + last = invalid + _ = pop(&stack) + continue + } _, function_item, function_type, ok := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types) if !ok { id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer") @@ -8279,6 +8400,17 @@ build_expr :: proc( continue } } + invalid, propagated := propagate_invalid_expr(checker, expr.span, frame.left) + if !propagated { + invalid, propagated = propagate_invalid_expr(checker, expr.span, ..stack[frame_index].built_args) + } + if propagated { + delete(stack[frame_index].built_args, checker.allocator) + stack[frame_index].built_args = nil + last = invalid + _ = pop(&stack) + continue + } callee_type := checker.module.exprs[frame.left].type _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) if !ok { @@ -8897,6 +9029,22 @@ build_block :: proc( duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start for statement_id in statements { statement := checker.ast_module.statements[statement_id] + if statement.diagnostic != source.INVALID_DIAGNOSTIC { + if statement.kind == .Declaration && symbol.is_valid(statement.name) { + if _, found := find_build_local(ctx.locals^[duplicate_start:], statement.name); !found { + _ = append_build_local( + ctx, statement.name, types.I64, !statement.immutable, statement.span, + ) + } + } + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = statement.diagnostic, + }) + ctx.problematic^ = true + continue + } switch statement.kind { case .Declaration: // A value block (`x :: { ... yield v }` / `x T = { ... }`): the parser @@ -9070,6 +9218,15 @@ build_block :: proc( checker, statement.target, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file, ) + if diagnostic, invalid := invalid_expr_diagnostic(checker, target_expr); invalid { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, + target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=diagnostic, + }) + ctx.problematic^ = true + continue + } target_type := checker.module.exprs[target_expr].type if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") @@ -9092,6 +9249,15 @@ build_block :: proc( checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, rhs_expected, ctx.pkg, ctx.file, ) + if diagnostic, invalid := invalid_expr_diagnostic(checker, value); invalid { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, + target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=diagnostic, + }) + ctx.problematic^ = true + continue + } if types.is_many_pointer(target_type, &checker.module.types) { assignment_op = .Pointer_Add if statement.assignment_op != .Add { @@ -9208,6 +9374,15 @@ build_block :: proc( continue } target_expr := build_global_reference(checker, global, statement.span, ctx.global_reads) + if diagnostic, invalid := invalid_expr_diagnostic(checker, target_expr); invalid { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Trap, span=statement.span, local=hir.INVALID_LOCAL, + target=hir.INVALID_EXPR, expr=hir.INVALID_EXPR, diagnostic=diagnostic, + }) + ctx.problematic^ = true + continue + } target_type := checker.module.exprs[target_expr].type if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") @@ -9425,7 +9600,14 @@ build_block :: proc( }) case .Expression: value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) - if !types.is_void(checker.module.exprs[value].type) { + if diagnostic, invalid := invalid_expr_diagnostic(checker, value); invalid { + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind = .Trap, span = statement.span, expr = hir.INVALID_EXPR, + local = hir.INVALID_LOCAL, diagnostic = diagnostic, + }) + ctx.problematic^ = true + } else if !types.is_void(checker.module.exprs[value].type) { id := source.add(checker.diagnostics, statement.span, "non-void expression result must be consumed or assigned to '_'") append(&body, hir.stmt_id(len(checker.module.statements))) append(&checker.module.statements, hir.Stmt{ @@ -11504,15 +11686,10 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { checker.current_comptime_values = spec.comptime_values defer checker.current_comptime_values = previous_comptime signature_diagnostic := source.INVALID_DIAGNOSTIC - if !types.is_void(spec.result) && !is_runtime_type(checker, spec.result) { + unresolved_result := !types.is_void(spec.result) && !is_runtime_type(checker, spec.result) + if unresolved_result { checker.specs[id].result = types.I64 spec.result = types.I64 - signature_diagnostic = source.addf( - checker.diagnostics, - function.span, - "could not resolve a concrete result type for '%s'", - symbol_text(checker, function.name), - ) } for arg in spec.args { if !is_runtime_type(checker, arg) { @@ -11582,6 +11759,16 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { checker.template_diagnostics[spec.template] != source.INVALID_DIAGNOSTIC native_main := function.pkg == 0 && function.name == checker.main_symbol && checker.entry_point == .Plain if !function.has_body { + if unresolved_result && signature_diagnostic == source.INVALID_DIAGNOSTIC && + checker.template_diagnostics[spec.template] == source.INVALID_DIAGNOSTIC { + signature_diagnostic = source.addf( + checker.diagnostics, + function.span, + "could not resolve a concrete result type for '%s'", + symbol_text(checker, function.name), + ) + problematic = true + } assert(spec.hir_id == hir.function_id(len(checker.module.functions))) append( &checker.module.functions, @@ -11665,6 +11852,22 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { append(&body, block_stmt) } delete(block, checker.allocator) + if unresolved_result && signature_diagnostic == source.INVALID_DIAGNOSTIC && + checker.template_diagnostics[spec.template] == source.INVALID_DIAGNOSTIC && !problematic { + signature_diagnostic = source.addf( + checker.diagnostics, + function.span, + "could not resolve a concrete result type for '%s'", + symbol_text(checker, function.name), + ) + append(&body, hir.stmt_id(len(checker.module.statements))) + append(&checker.module.statements, hir.Stmt{ + kind=.Trap, span=function.span, expr=hir.INVALID_EXPR, + local=hir.INVALID_LOCAL, diagnostic=signature_diagnostic, + }) + problematic = true + returns = true + } fallible_void := types.kind(spec.result, &checker.module.types) == .Fallible && types.is_void(types.fallible_success(spec.result, &checker.module.types)) @@ -11773,6 +11976,28 @@ static_integer_value :: proc(module: ^hir.Module, expr_id: hir.Expr_Id) -> (i64, build_globals :: proc(checker: ^Checker) { for global, global_index in checker.ast_module.globals { + if global.diagnostic != source.INVALID_DIAGNOSTIC { + global_type := checker.global_types[global_index] + if !is_runtime_type(checker, global_type) { + global_type = types.I64 + } + expr := hir.INVALID_EXPR + if !global.external { + expr = invalid_hir_expr(checker, global.span, global.diagnostic, global_type) + } + append(&checker.module.globals, hir.Global{ + name=global.name, + link_name=strings.clone(global.link_name, checker.allocator), + type=global_type, + expr=expr, + external=global.external, + writable=global.writable || !global.immutable, + direct_problem=true, + problematic=true, + diagnostic=global.diagnostic, + }) + continue + } if global.external { global_type := checker.global_types[global_index] writable := global.writable @@ -11841,6 +12066,12 @@ build_globals :: proc(checker: ^Checker) { global_type = checker.module.exprs[expr].type } diagnostic := source.INVALID_DIAGNOSTIC + if root, invalid := invalid_expr_diagnostic(checker, expr); invalid { + diagnostic = root + if !is_runtime_type(checker, global_type) { + global_type = types.I64 + } + } if !global.immutable && is_undefined_expr(checker, global.expr) { diagnostic = source.add( checker.diagnostics, @@ -12188,6 +12419,7 @@ check :: proc( checker.record_field_defaults = make([]types.Type, len(checker.module.types.fields), allocator) checker.record_field_conflicts = make([]types.Type, len(checker.module.types.fields), allocator) checker.record_field_conflict_spans = make([]source.Span, len(checker.module.types.fields), allocator) + checker.poisoned_packages = make([]bool, max(len(ast_module.packages), 1), allocator) init_record_field_inference(&checker) checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator) checker.external_global_diagnostics = make([]source.Diagnostic_Id, len(ast_module.globals), allocator) @@ -12199,8 +12431,17 @@ check :: proc( } checker.constants = make([]Constant, len(ast_module.exprs), allocator) checker.template_diagnostics = make([]source.Diagnostic_Id, len(ast_module.functions), allocator) - for &diagnostic in checker.template_diagnostics { - diagnostic = source.INVALID_DIAGNOSTIC + for &diagnostic, index in checker.template_diagnostics { + diagnostic = ast_module.functions[index].diagnostic + pkg := ast_module.functions[index].pkg + if diagnostic != source.INVALID_DIAGNOSTIC && int(pkg) < len(checker.poisoned_packages) { + checker.poisoned_packages[pkg] = true + } + } + for global in ast_module.globals { + if global.diagnostic != source.INVALID_DIAGNOSTIC && int(global.pkg) < len(checker.poisoned_packages) { + checker.poisoned_packages[global.pkg] = true + } } defer { for spec in checker.specs { @@ -12220,6 +12461,7 @@ check :: proc( delete(checker.record_field_defaults, allocator) delete(checker.record_field_conflicts, allocator) delete(checker.record_field_conflict_spans, allocator) + delete(checker.poisoned_packages, allocator) delete(checker.external_global_canonical, allocator) delete(checker.external_global_diagnostics, allocator) delete(checker.constants, allocator) diff --git a/compiler/loader/loader.odin b/compiler/loader/loader.odin index cc8267c..b7fa185 100644 --- a/compiler/loader/loader.odin +++ b/compiler/loader/loader.odin @@ -1486,7 +1486,7 @@ validate_declaration_aliases :: proc(state: ^State) { import_id := find_type_import(state.module, alias.file, alias.qualifier) if import_id == ast.INVALID_IMPORT { - alias.diagnostic = source.addf(state.diagnostics, alias.span, "unknown package alias '%s'", symbol.resolve(state.symbols, alias.qualifier)) + alias.diagnostic = source.addf(state.diagnostics, alias.span, "unknown symbol '%s'", symbol.resolve(state.symbols, alias.qualifier)) alias.valid = false continue } @@ -1613,6 +1613,81 @@ canonical_type :: proc( return resolved } +diagnose_qualified_type_uses :: proc(state: ^State) { + module := state.module + for &type_use in module.type_uses { + item, ok := types.node(&module.type_store, type_use.type) + if !ok || item.qualifier == 0 || item.kind != .Named { + continue + } + qualifier := symbol.Id(item.qualifier) + name := symbol.Id(item.name) + import_id := find_type_import(module, type_use.file, qualifier) + if import_id == ast.INVALID_IMPORT { + type_use.diagnostic = source.addf( + state.diagnostics, + type_use.span, + "unknown symbol '%s'", + symbol.resolve(state.symbols, qualifier), + ) + source.set_primary_label(state.diagnostics, type_use.diagnostic, "unknown symbol") + continue + } + import_item := module.imports[import_id] + if !import_item.valid || import_item.target == ast.INVALID_PACKAGE || + int(import_item.target) >= len(module.packages) || !module.packages[import_item.target].available { + type_use.diagnostic = source.addf( + state.diagnostics, + type_use.span, + "unavailable imported package '%s'", + symbol.resolve(state.symbols, qualifier), + ) + continue + } + if !types.is_valid(types.find_named(&module.type_store, u32(import_item.target), u32(name))) { + type_use.diagnostic = source.addf( + state.diagnostics, + type_use.span, + "package '%s' has no member '%s'", + symbol.resolve(state.symbols, qualifier), + symbol.resolve(state.symbols, name), + ) + } + } +} + +type_resolution_diagnostic :: proc(module: ^ast.Module, value: types.Type, file: ast.File_Id, depth := 0) -> source.Diagnostic_Id { + if depth > 64 { + return source.INVALID_DIAGNOSTIC + } + for type_use in module.type_uses { + if type_use.file == file && type_use.type == value && type_use.diagnostic != source.INVALID_DIAGNOSTIC { + return type_use.diagnostic + } + } + item, ok := types.node(&module.type_store, value) + if !ok { + return source.INVALID_DIAGNOSTIC + } + if diagnostic := type_resolution_diagnostic(module, item.child, file, depth+1); + diagnostic != source.INVALID_DIAGNOSTIC { + return diagnostic + } + if diagnostic := type_resolution_diagnostic(module, item.extra, file, depth+1); + diagnostic != source.INVALID_DIAGNOSTIC { + return diagnostic + } + if item.kind == .Function { + for param in types.params_for(&module.type_store, value) { + if diagnostic := type_resolution_diagnostic(module, param.type, file, depth+1); + diagnostic != source.INVALID_DIAGNOSTIC { + return diagnostic + } + } + } + return source.INVALID_DIAGNOSTIC +} + canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) { original_count := len(module.type_store.nodes) mapping := make([]types.Type, original_count, allocator) @@ -1620,6 +1695,19 @@ canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) { defer delete(mapping, allocator) defer delete(visiting, allocator) for &function in module.functions { + for param in function.params { + if diagnostic := type_resolution_diagnostic(module, param.type, function.file); + diagnostic != source.INVALID_DIAGNOSTIC && function.diagnostic == source.INVALID_DIAGNOSTIC { + function.diagnostic = diagnostic + } + } + signature_results := [2]types.Type{function.result, function.error} + for value in signature_results { + if diagnostic := type_resolution_diagnostic(module, value, function.file); + diagnostic != source.INVALID_DIAGNOSTIC && function.diagnostic == source.INVALID_DIAGNOSTIC { + function.diagnostic = diagnostic + } + } for ¶m in function.params { param.type = canonical_type(module, param.type, mapping, visiting) } @@ -1627,9 +1715,17 @@ canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) { function.error = canonical_type(module, function.error, mapping, visiting) } for &global in module.globals { + if diagnostic := type_resolution_diagnostic(module, global.type, global.file); + diagnostic != source.INVALID_DIAGNOSTIC && global.diagnostic == source.INVALID_DIAGNOSTIC { + global.diagnostic = diagnostic + } global.type = canonical_type(module, global.type, mapping, visiting) } for &statement in module.statements { + if diagnostic := type_resolution_diagnostic(module, statement.type, ast.File_Id(statement.span.file)); + diagnostic != source.INVALID_DIAGNOSTIC && statement.diagnostic == source.INVALID_DIAGNOSTIC { + statement.diagnostic = diagnostic + } statement.type = canonical_type(module, statement.type, mapping, visiting) } for &field in module.type_fields { @@ -1687,6 +1783,7 @@ load :: proc( } validate_imports(&state) validate_declaration_aliases(&state) + diagnose_qualified_type_uses(&state) canonicalize_types(&module, allocator) return module, !state.root_failed } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 8c5440c..d880fc9 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -428,6 +428,13 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax { count_expr=u32(call), }) } + append(&parser.module.type_uses, ast.Type_Use{ + type=named, + span=first.span, + pkg=parser.pkg, + file=parser.file, + diagnostic=source.INVALID_DIAGNOSTIC, + }) return named } source.add(parser.diagnostics, tok.span, "expected a type") diff --git a/compiler/source/source.odin b/compiler/source/source.odin index 6d82146..52313c4 100644 --- a/compiler/source/source.odin +++ b/compiler/source/source.odin @@ -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.. 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.. 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 "" + return fmt.aprintf("%s: %s: %s", path, severity, diagnostic.message, allocator=allocator) + } 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, - ) + 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) { diff --git a/compiler_tests.odin b/compiler_tests.odin index b0811f6..f146445 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -917,8 +917,8 @@ multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) { found := false for _, diagnostic_index in diagnostics.items { message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index)) - if strings.contains(message, "/b.bro:2:9:") && - strings.contains(message, "unknown package alias 'math'") { + if strings.contains(message, "--> ") && strings.contains(message, "/b.bro:2:9") && + strings.contains(message, "unknown symbol 'math'") { found = true } delete(message) @@ -1956,7 +1956,7 @@ old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) { } found := [len(names)]bool{} for diagnostic in diagnostics.items { - if !strings.contains(diagnostic.message, "unresolved function") { + if !strings.contains(diagnostic.message, "unknown symbol") { continue } for name, index in names { @@ -4306,6 +4306,34 @@ main func() i32 { testing.expect(t, indirect_calls > 0) } +@(test) +qualified_value_calls_do_not_imply_package_resolution :: proc(t: ^testing.T) { + text := `Callbacks :: struct { + call @func() void +} +main func() void { + callbacks Callbacks = Callbacks{call = func() void {}} + callbacks.call() + missing.call() +} +` + 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) + + testing.expect_value(t, len(diagnostics.items), 1) + testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'") + testing.expect(t, !strings.contains(diagnostics.items[0].message, "package")) +} + @(test) function_literals_do_not_capture_locals :: proc(t: ^testing.T) { text := `main func() i32 { @@ -4330,7 +4358,7 @@ function_literals_do_not_capture_locals :: proc(t: ^testing.T) { found := false for diagnostic in diagnostics.items { - found = found || strings.contains(diagnostic.message, "unresolved global 'offset'") + found = found || strings.contains(diagnostic.message, "unknown symbol 'offset'") } testing.expect(t, found) } @@ -7210,7 +7238,7 @@ source_store_owns_buffers_indexes_lines_and_deduplicates_diagnostics :: proc(t: testing.expect_value(t, len(store.items[source_id].line_starts), 3) testing.expect_value(t, first, second) testing.expect_value(t, len(diagnostics.items), 2) - testing.expect(t, strings.contains(formatted, "owned.bro:2:1:")) + testing.expect(t, strings.contains(formatted, "--> owned.bro:2:1")) } @(test) @@ -7233,8 +7261,92 @@ diagnostic_warnings_format_and_dedupe_by_severity :: proc(t: ^testing.T) { testing.expect_value(t, len(diagnostics.items), 2) testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning) testing.expect_value(t, diagnostics.items[err].severity, source.Severity.Error) - testing.expect(t, strings.contains(formatted_warning, "test.bro:1:1: warning: same")) - testing.expect(t, strings.contains(formatted_error, "test.bro:1:1: error: same")) + testing.expect(t, strings.contains(formatted_warning, "warning: same\n --> test.bro:1:1")) + testing.expect(t, strings.contains(formatted_error, "error: same\n --> test.bro:1:1")) +} + +@(test) +rich_diagnostics_render_labels_notes_help_and_tabs :: proc(t: ^testing.T) { + store := source.init_store() + defer source.destroy_store(&store) + primary_file := source.add_source(&store, "main.bro", "\tmissing()\nnext()\n") + definition_file := source.add_source(&store, "dep.bro", "value :: 1\n") + diagnostics := source.init_store_diagnostics(&store) + defer source.destroy_diagnostics(&diagnostics) + id := source.add(&diagnostics, source.Span{file=primary_file, start=1, end=8}, "unknown symbol 'missing'") + source.set_primary_label(&diagnostics, id, "unknown symbol") + source.set_primary_label(&diagnostics, id, "unknown symbol") + source.add_secondary_label( + &diagnostics, id, + source.Span{file=definition_file, start=0, end=5}, + "related declaration", + ) + source.add_note(&diagnostics, id, "names resolve in the current scope") + source.add_note(&diagnostics, id, "names resolve in the current scope") + source.add_help(&diagnostics, id, "declare 'missing' before using it") + formatted := source.format(&diagnostics, id) + defer delete(formatted) + multiline := source.add( + &diagnostics, + source.Span{file=primary_file, start=1, end=17}, + "multiline failure", + ) + multiline_formatted := source.format(&diagnostics, multiline) + defer delete(multiline_formatted) + unknown := source.add(&diagnostics, source.Span{}, "no location") + unknown_formatted := source.format(&diagnostics, unknown) + defer delete(unknown_formatted) + + testing.expect_value(t, len(diagnostics.annotations), 4) + testing.expect(t, strings.contains(formatted, "error: unknown symbol 'missing'")) + testing.expect(t, strings.contains(formatted, "--> main.bro:1:2")) + testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol")) + testing.expect(t, strings.contains(formatted, "::: dep.bro:1:1")) + testing.expect(t, strings.contains(formatted, "----- related declaration")) + testing.expect(t, strings.contains(formatted, "note: names resolve in the current scope")) + testing.expect(t, strings.contains(formatted, "help: declare 'missing' before using it")) + testing.expect(t, strings.contains(multiline_formatted, "1 | missing()")) + testing.expect(t, !strings.contains(multiline_formatted, "next()")) + testing.expect_value(t, unknown_formatted, "main.bro: error: no location") +} + +@(test) +poisoned_expressions_preserve_independent_root_diagnostics :: proc(t: ^testing.T) { + text := `bad :: missing_global +sink func(value i32) void { _ = value } +broken func(value int) int { return missing_return + value } +main func() void { + _ = missing_add + 1 + _ = try missing_try() + missing_catch() catch |_| {} + sink(missing_arg) + missing_stmt() + missing_target.field = 1 + value i32 = 0 + value += missing_rhs + _ = broken(1) +} +` + 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) + + testing.expect_value(t, len(diagnostics.items), 9) + for diagnostic in diagnostics.items { + testing.expect(t, strings.has_prefix(diagnostic.message, "unknown symbol 'missing_")) + testing.expect(t, !strings.contains(diagnostic.message, "fallible expression")) + testing.expect(t, !strings.contains(diagnostic.message, "must be consumed")) + testing.expect(t, !strings.contains(diagnostic.message, "compatible numeric operands")) + testing.expect(t, !strings.contains(diagnostic.message, "implicitly convert")) + } } @(test) @@ -7923,7 +8035,7 @@ referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) { defer delete(stdout) defer delete(stderr) testing.expect(t, !state.success) - testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6:")) + testing.expect(t, strings.contains(string(stderr), "/missing_used/app/main.bro:4:6")) } @(test) @@ -7991,8 +8103,8 @@ file_hidden_declarations_are_not_package_members :: proc(t: ^testing.T) { found_import := false found_collision := false for diagnostic in diagnostics.items { - found_sibling = found_sibling || strings.contains(diagnostic.message, "unresolved function 'sibling'") - found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unresolved global 'sibling_value'") + found_sibling = found_sibling || strings.contains(diagnostic.message, "unknown symbol 'sibling'") + found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unknown symbol 'sibling_value'") found_sibling_type = found_sibling_type || strings.contains(diagnostic.message, "unknown or opaque record type 'Sibling'") found_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member 'secret'") found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'") @@ -8221,7 +8333,7 @@ main func() void {} wants := []string{ "has no member 'missing'", "is file-hidden", - "unknown package alias 'nope'", + "unknown symbol 'nope'", "unavailable imported package 'gone'", "package member 'dep.ambiguous' is ambiguous", "duplicate declaration alias 'duplicate'", @@ -8937,10 +9049,10 @@ conditional_unwrap_diagnostics_cover_counts_guards_and_capture_scope :: proc(t: duplicate = duplicate || strings.contains(diagnostic.message, "unwrap captures must have distinct names") non_optional = non_optional || strings.contains(diagnostic.message, "unwrap requires an optional value") guard = guard || strings.contains(diagnostic.message, "unwrap guard must be a bool") - outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unresolved global 'earlier'") + outer_operand_scope = outer_operand_scope || strings.contains(diagnostic.message, "unknown symbol 'earlier'") immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'") redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'value'") - capture_scope += 1 if strings.contains(diagnostic.message, "unresolved global 'value'") else 0 + capture_scope += 1 if strings.contains(diagnostic.message, "unknown symbol 'value'") else 0 } testing.expect_value(t, count_mismatches, 2) testing.expect(t, duplicate) @@ -8978,7 +9090,7 @@ if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) { found := false for diagnostic in diagnostics.items { - found = found || strings.contains(diagnostic.message, "unresolved global 'v'") + found = found || strings.contains(diagnostic.message, "unknown symbol 'v'") } testing.expect(t, found) } @@ -9433,7 +9545,7 @@ main func() void { redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'") immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'item'") immutable_pointer = immutable_pointer || strings.contains(diagnostic.message, "assignment target is not writable") - scope = scope || strings.contains(diagnostic.message, "unresolved global 'item'") + scope = scope || strings.contains(diagnostic.message, "unknown symbol 'item'") integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0 } testing.expect(t, unsupported) @@ -11920,6 +12032,101 @@ main func() void { testing.expect(t, found) } +@(test) +missing_qualified_signature_symbol_reports_one_root_error :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-root-diagnostic" + defer _ = os2.remove_all(directory) + _ = os2.remove_all(directory) + testing.expect(t, os.make_directory(directory) == nil) + token_text := `Token :: struct { start int } +` + lexer_text := `scan func(cursor usize) void ! missing.Error { + token Token = Token{start = cursor} + _ = token +} +` + main_text := `main func() void { + scan(1) catch |_| { return } +} +` + testing.expect(t, os.write_entire_file( + "/tmp/brolang-test-root-diagnostic/token.bro", + transmute([]byte)token_text, + )) + testing.expect(t, os.write_entire_file( + "/tmp/brolang-test-root-diagnostic/lexer.bro", + transmute([]byte)lexer_text, + )) + testing.expect(t, os.write_entire_file( + "/tmp/brolang-test-root-diagnostic/main.bro", + transmute([]byte)main_text, + )) + + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + module, loaded := loader.load(directory, &sources, &diagnostics, &symbols) + defer ast.destroy_module(&module) + hir_module := checker.check(&module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + testing.expect(t, loaded) + testing.expect_value(t, len(diagnostics.items), 1) + testing.expect_value(t, diagnostics.items[0].message, "unknown symbol 'missing'") + formatted := source.format(&diagnostics, source.Diagnostic_Id(0)) + defer delete(formatted) + testing.expect(t, strings.contains(formatted, "/lexer.bro:1:32")) + testing.expect(t, strings.contains(formatted, "^^^^^^^ unknown symbol")) + testing.expect(t, !strings.contains(formatted, "fallible expression")) + testing.expect(t, !strings.contains(formatted, "must be consumed")) + testing.expect(t, !strings.contains(formatted, "could not resolve the 'int' constraint")) +} + +@(test) +poisoned_global_and_local_types_do_not_create_inference_fallbacks :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-poisoned-declarations" + defer _ = os2.remove_all(directory) + _ = os2.remove_all(directory) + testing.expect(t, os.make_directory(directory) == nil) + text := `bad missing.Global :: 1 +main func() void { + value absent.Local = 1 + _ = value +} +` + testing.expect(t, os.write_entire_file( + "/tmp/brolang-test-poisoned-declarations/main.bro", + transmute([]byte)text, + )) + + sources := source.init_store() + defer source.destroy_store(&sources) + diagnostics := source.init_store_diagnostics(&sources) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + module, loaded := loader.load(directory, &sources, &diagnostics, &symbols) + defer ast.destroy_module(&module) + hir_module := checker.check(&module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + testing.expect(t, loaded) + testing.expect_value(t, len(diagnostics.items), 2) + unknown_missing := false + unknown_absent := false + for diagnostic in diagnostics.items { + unknown_missing = unknown_missing || diagnostic.message == "unknown symbol 'missing'" + unknown_absent = unknown_absent || diagnostic.message == "unknown symbol 'absent'" + testing.expect(t, !strings.contains(diagnostic.message, "could not resolve")) + testing.expect(t, !strings.contains(diagnostic.message, "could not infer")) + } + testing.expect(t, unknown_missing) + testing.expect(t, unknown_absent) +} + named_record_field_type :: proc( module: ^hir.Module, symbols: ^symbol.Table, diff --git a/std/arraylist/arraylist.bro b/std/arraylist/arraylist.bro index 93a9cc7..bcd11c2 100644 --- a/std/arraylist/arraylist.bro +++ b/std/arraylist/arraylist.bro @@ -1,4 +1,4 @@ -mem :: import "@std/mem" +import "@std/mem" ArrayList func($T type) type { return struct { diff --git a/std/debug/debug.bro b/std/debug/debug.bro index 6c5954f..ecaaeeb 100644 --- a/std/debug/debug.bro +++ b/std/debug/debug.bro @@ -1,5 +1,5 @@ -c :: import "@ffi/c" -io :: import "@std/io" +import "@ffi/c" +import "@std/io" hide write func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { request usize = bytes.len diff --git a/std/io/io.bro b/std/io/io.bro index 6847aca..24d57ae 100644 --- a/std/io/io.bro +++ b/std/io/io.bro @@ -1,5 +1,5 @@ -c :: import "@ffi/c" -meta :: import "@std/meta" +import "@ffi/c" +import "@std/meta" ReadError :: enum { read_failed @@ -276,7 +276,7 @@ hide write_integer func(writer Writer, $T type, value T, base u64, uppercase boo return } -# ponytail: libc keeps float formatting small; replace it with a native shortest-roundtrip writer if locale independence matters. +# note: libc keeps float formatting small; replace it with a native shortest-roundtrip writer if locale independence matters. hide write_float func(writer Writer, $T type, value T, scientific bool) void ! WriteError { match typeinfo!(T) { .float: { @@ -353,12 +353,7 @@ hide write_default func(writer Writer, $T type, value T) void ! WriteError { return } -print func( - writer Writer, - $format []u8, - $Args type, - args Args, -) void ! WriteError { +print func(writer Writer, $format []u8, $Args type, args Args) void ! WriteError { inline for parse_format(format.len, format, Args) |token| { if (token.kind == .unused) { break diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 310486d..12a75dd 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -1,4 +1,4 @@ -c :: import "@ffi/c" +import "@ffi/c" AllocError :: enum { out_of_memory diff --git a/std/process/process.bro b/std/process/process.bro index 7334191..7bbeab0 100644 --- a/std/process/process.bro +++ b/std/process/process.bro @@ -1,4 +1,4 @@ -io :: import "@std/io" +import "@std/io" Init :: struct { io io.Io