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
+11
View File
@@ -63,6 +63,14 @@ index :: proc(id: $T, invalid: T, count: int) -> (int, bool) {
Type_Syntax :: types.Type 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 { Expr_Kind :: enum u8 {
Invalid, Invalid,
Integer, Integer,
@@ -325,6 +333,7 @@ Module :: struct {
c_trampolines: [dynamic]Trampoline, c_trampolines: [dynamic]Trampoline,
strings: [dynamic]string, strings: [dynamic]string,
type_fields: [dynamic]types.Field, type_fields: [dynamic]types.Field,
type_uses: [dynamic]Type_Use,
type_store: types.Store, type_store: types.Store,
allocator: mem.Allocator, allocator: mem.Allocator,
} }
@@ -345,6 +354,7 @@ init_module :: proc(allocator := context.allocator) -> Module {
module.c_trampolines.allocator = allocator module.c_trampolines.allocator = allocator
module.strings.allocator = allocator module.strings.allocator = allocator
module.type_fields.allocator = allocator module.type_fields.allocator = allocator
module.type_uses.allocator = allocator
return module return module
} }
@@ -397,5 +407,6 @@ destroy_module :: proc(module: ^Module) {
delete(module.c_trampolines) delete(module.c_trampolines)
delete(module.strings) delete(module.strings)
delete(module.type_fields) delete(module.type_fields)
delete(module.type_uses)
types.destroy_store(&module.type_store) types.destroy_store(&module.type_store)
} }
+277 -35
View File
@@ -208,6 +208,7 @@ Checker :: struct {
record_field_conflicts: []types.Type, record_field_conflicts: []types.Type,
record_field_conflict_spans: []source.Span, record_field_conflict_spans: []source.Span,
record_field_demands_dirty: bool, record_field_demands_dirty: bool,
poisoned_packages: []bool,
external_global_canonical: []ast.Global_Id, external_global_canonical: []ast.Global_Id,
external_global_diagnostics: []source.Diagnostic_Id, external_global_diagnostics: []source.Diagnostic_Id,
constants: []Constant, 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 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 { 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 { 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)) 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 { 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 { 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) { if symbol.is_valid(expr.qualifier) {
return source.addf( return source.addf(
@@ -1516,12 +1531,20 @@ add_name_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target
symbol_text(checker, expr.name), 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 { 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 { 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) { if symbol.is_valid(expr.qualifier) {
return source.addf( return source.addf(
@@ -1532,7 +1555,10 @@ add_call_resolution_diagnostic :: proc(checker: ^Checker, expr: ast.Expr, target
symbol_text(checker, expr.name), 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) { 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) target_pkg, available := expr_package(checker, expr, pkg, file, true)
if !available { if !available {
_ = add_package_resolution_diagnostic(checker, expr, file)
return types.INVALID return types.INVALID
} }
template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file)) template := find_template(checker, expr.name, target_pkg, expr_lookup_file(expr, file))
@@ -3145,6 +3172,7 @@ validate_declarations :: proc(checker: ^Checker) {
continue continue
} }
has_comptime := function_has_comptime_params(function) has_comptime := function_has_comptime_params(function)
signature_poisoned := function.diagnostic != source.INVALID_DIAGNOSTIC
locals: [dynamic]symbol.Id locals: [dynamic]symbol.Id
locals.allocator = checker.allocator locals.allocator = checker.allocator
for param in function.params { for param in function.params {
@@ -3152,7 +3180,7 @@ validate_declarations :: proc(checker: ^Checker) {
if param.comptime_value || !has_comptime { if param.comptime_value || !has_comptime {
param_type = type_from_syntax(checker, param.type, function.pkg, function.file) 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 { if function.c_abi {
checker.template_diagnostics[function_id] = source.add( checker.template_diagnostics[function_id] = source.add(
checker.diagnostics, checker.diagnostics,
@@ -3169,7 +3197,7 @@ validate_declarations :: proc(checker: ^Checker) {
symbol_text(checker, param.name), 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); if diagnostic := add_unsupported_type_diagnostic(checker, param.span, param_type);
diagnostic != source.INVALID_DIAGNOSTIC { diagnostic != source.INVALID_DIAGNOSTIC {
checker.template_diagnostics[function_id] = diagnostic checker.template_diagnostics[function_id] = diagnostic
@@ -3197,7 +3225,7 @@ validate_declarations :: proc(checker: ^Checker) {
) )
} }
append(&locals, param.name) 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.template_diagnostics[function_id] = source.addf(
checker.diagnostics, checker.diagnostics,
param.span, 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) result_type := type_from_syntax(checker, function.result, function.pkg, function.file)
if diagnostic := add_unsupported_type_diagnostic(checker, function.span, result_type); if diagnostic := add_unsupported_type_diagnostic(checker, function.span, result_type);
diagnostic != source.INVALID_DIAGNOSTIC { 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_type := type_from_syntax(checker, function.error, function.pkg, function.file)
error_sum := types.is_enum(error_type, &checker.module.types) || error_sum := types.is_enum(error_type, &checker.module.types) ||
types.is_tagged_union(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.template_diagnostics[function_id] = source.addf(
checker.diagnostics, checker.diagnostics,
function.span, function.span,
@@ -3247,7 +3275,7 @@ validate_declarations :: proc(checker: ^Checker) {
symbol_text(checker, function.name), 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.template_diagnostics[function_id] = source.addf(
checker.diagnostics, checker.diagnostics,
function.span, function.span,
@@ -3255,7 +3283,7 @@ validate_declarations :: proc(checker: ^Checker) {
symbol_text(checker, function.name), 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 { for param in function.params {
param_type := type_from_syntax(checker, param.type, function.pkg, function.file) param_type := type_from_syntax(checker, param.type, function.pkg, function.file)
if add_unsupported_type_diagnostic(checker, param.span, param_type) != 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]) 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 { for item in checker.module.types.nodes {
if !(item.declared && (item.kind == .Struct || item.kind == .Union) && if !(item.declared && (item.kind == .Struct || item.kind == .Union) &&
!item.c_layout && symbol.is_valid(symbol.Id(item.name))) { !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) start := int(item.field_start)
if slot >= start && slot < start+int(item.field_count) && if slot >= start && slot < start+int(item.field_count) &&
slot >= 0 && slot < len(checker.module.types.fields) { 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) { 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) { if !types.is_constraint(constraint) {
continue 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 { if !ok {
continue continue
} }
@@ -3509,7 +3537,8 @@ finalize_record_field_inference :: proc(checker: ^Checker) {
symbol_text(checker, record_name), symbol_text(checker, field_name), 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( source.addf(
checker.diagnostics, checker.diagnostics,
source.Span{}, source.Span{},
@@ -5622,6 +5651,10 @@ infer_all :: proc(checker: ^Checker) {
// Demands accumulate in global_demands so the default never blocks a later // Demands accumulate in global_demands so the default never blocks a later
// cross-family demand (e.g. integer literal -> unsigned or float). // cross-family demand (e.g. integer literal -> unsigned or float).
for global, index in checker.ast_module.globals { 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( declared := resolve_inferred_array(
checker, checker,
type_from_syntax(checker, global.type, global.pkg, global.file), 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 // Backward demands: a global pushes its own (declared or already-resolved) type
// onto open numeric slots reachable through names and numeric arithmetic. // onto open numeric slots reachable through names and numeric arithmetic.
for global, index in checker.ast_module.globals { for global, index in checker.ast_module.globals {
if global.external { if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC {
continue continue
} }
demand := checker.global_types[index] 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 // concrete-typed ones) for its side effect of specializing called functions and
// recording demands from call arguments in their initializers. // recording demands from call arguments in their initializers.
for global, index in checker.ast_module.globals { for global, index in checker.ast_module.globals {
if global.external { if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC {
continue continue
} }
declared := resolve_inferred_array( declared := resolve_inferred_array(
@@ -5765,7 +5798,8 @@ infer_all :: proc(checker: ^Checker) {
// No authoritative demand can still arrive. Assign final defaults, then // No authoritative demand can still arrive. Assign final defaults, then
// continue the same fixpoint so dependent globals/specs observe them. // continue the same fixpoint so dependent globals/specs observe them.
for global, index in checker.ast_module.globals { 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 continue
} }
if checker.global_open_const[index] { 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) mark_spec_demanded(checker, find_spec(checker, checker.io_provider_template, nil), &stack)
} }
for global in checker.ast_module.globals { for global in checker.ast_module.globals {
if global.external { if global.external || global.diagnostic != source.INVALID_DIAGNOSTIC {
continue continue
} }
_ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack) _ = 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) { add_unique_global :: proc(values: ^[dynamic]hir.Global_Id, value: hir.Global_Id) {
for existing in values { for existing in values {
if existing == value { if existing == value {
@@ -5935,6 +5986,9 @@ coerce_expr :: proc(
if expr_id == hir.INVALID_EXPR { if expr_id == hir.INVALID_EXPR {
return expr_id return expr_id
} }
if _, invalid := invalid_expr_diagnostic(checker, expr_id); invalid {
return expr_id
}
actual := checker.module.exprs[expr_id].type actual := checker.module.exprs[expr_id].type
if types.equal(actual, expected) { if types.equal(actual, expected) {
return expr_id 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) 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) { if !hir_is_location(checker, value) {
id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location") id := source.add(checker.diagnostics, expr.span, "'&' requires an addressable location")
return invalid_hir_expr(checker, expr.span, id) return invalid_hir_expr(checker, expr.span, id)
@@ -6936,6 +6993,9 @@ build_compound_expr :: proc(
}) })
case .Deref: case .Deref:
pointer := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) 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 pointer_type := checker.module.exprs[pointer].type
if !types.is_pointer(pointer_type, store) { if !types.is_pointer(pointer_type, store) {
id := source.add(checker.diagnostics, expr.span, "postfix '^' requires a pointer") 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) 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 := build_nested_expr(checker, expr.right, locals, global_reads, calls, types.USIZE, pkg, file)
index = coerce_expr(checker, index, types.USIZE, expr.span) 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 container_type := checker.module.exprs[container].type
item, ok := types.container(container_type, store) item, ok := types.container(container_type, store)
if !ok { 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) 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 container_type := checker.module.exprs[container].type
item, ok := types.container(container_type, store) item, ok := types.container(container_type, store)
if !ok { if !ok {
@@ -6987,6 +7053,10 @@ build_compound_expr :: proc(
if bound != ast.INVALID_EXPR { if bound != ast.INVALID_EXPR {
bounds[index] = build_nested_expr(checker, bound, locals, global_reads, calls, types.USIZE, pkg, file) 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) 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 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) 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) 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 base_type := checker.module.exprs[base].type
result, _ := build_field_from_value(checker, expr, base, base_type) result, _ := build_field_from_value(checker, expr, base, base_type)
return result return result
case .Unwrap: case .Unwrap:
optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) 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 optional_type := checker.module.exprs[optional].type
if !types.is_optional(optional_type, store) { if !types.is_optional(optional_type, store) {
id := source.add(checker.diagnostics, expr.span, "postfix '?' requires an optional") id := source.add(checker.diagnostics, expr.span, "postfix '?' requires an optional")
@@ -7016,6 +7092,9 @@ build_compound_expr :: proc(
}) })
case .Orelse: case .Orelse:
optional := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.INVALID, pkg, file) 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 optional_type := checker.module.exprs[optional].type
if !types.is_optional(optional_type, store) { if !types.is_optional(optional_type, store) {
id := source.add(checker.diagnostics, expr.span, "'orelse' requires an optional left operand") 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 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) 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 channel_type := checker.module.exprs[channel].type
success := types.fallible_success(channel_type, store) success := types.fallible_success(channel_type, store)
if !types.is_valid(success) { if !types.is_valid(success) {
@@ -7076,6 +7158,9 @@ build_compound_expr :: proc(
case .Catch: case .Catch:
left_expected := expected if checker.ast_module.exprs[expr.left].kind == .Call else types.INVALID 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) 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 channel_type := checker.module.exprs[channel].type
success := types.fallible_success(channel_type, store) success := types.fallible_success(channel_type, store)
if !types.is_valid(success) { 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) 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) 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 child := expected_child
if !types.is_valid(child) { if !types.is_valid(child) {
child = types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type) child = types.widest(checker.module.exprs[left].type, checker.module.exprs[right].type)
@@ -7174,6 +7262,9 @@ build_compound_expr :: proc(
}) })
case .Not: case .Not:
operand := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) 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 operand_type := checker.module.exprs[operand].type
if checker.module.exprs[operand].kind != .Invalid && !types.is_bool(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") id := source.add(checker.diagnostics, expr.span, "'!' requires a bool operand")
@@ -7186,6 +7277,9 @@ build_compound_expr :: proc(
case .And, .Or: case .And, .Or:
left := build_nested_expr(checker, expr.left, locals, global_reads, calls, types.BOOL, pkg, file) 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) 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 left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].type right_type := checker.module.exprs[right].type
left_ok := checker.module.exprs[left].kind == .Invalid || types.is_bool(left_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) 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) 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 left_type := checker.module.exprs[left].type
right_type := checker.module.exprs[right].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 operand_type := types.INVALID
if types.is_enum(left_type, store) || types.is_enum(right_type, store) { 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) { 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, left, right: hir.Expr_Id,
span: source.Span, span: source.Span,
) -> hir.Expr_Id { ) -> 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). // Pointer arithmetic is only defined for `+` (many-pointer + usize).
if op == .Add && if op == .Add &&
types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) && types.is_many_pointer(checker.module.exprs[left].type, &checker.module.types) &&
@@ -7960,6 +8057,9 @@ build_expr :: proc(
checker.diagnostics, expr.span, message, checker.diagnostics, expr.span, message,
symbol_text(checker, expr.name), len(function.params), len(expr.args), 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 { } else if len(mapping_failure) > 0 {
id = source.addf( id = source.addf(
checker.diagnostics, expr.span, checker.diagnostics, expr.span,
@@ -8009,6 +8109,11 @@ build_expr :: proc(
} }
if frame.stage == 5 { if frame.stage == 5 {
operand := last 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 operand_type := checker.module.exprs[operand].type
if !types.is_signed(operand_type, checker.target) && !types.is_float(operand_type, checker.target) { 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") id := source.add(checker.diagnostics, expr.span, "negation requires a signed integer or float")
@@ -8071,6 +8176,17 @@ build_expr :: proc(
continue 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] function := checker.ast_module.functions[frame.template]
comptime_values: []Comptime_Value comptime_values: []Comptime_Value
comptime_ok := false comptime_ok := false
@@ -8236,6 +8352,11 @@ build_expr :: proc(
} }
if frame.stage == 6 { if frame.stage == 6 {
callee := last 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) _, function_item, function_type, ok := types.function_pointer(checker.module.exprs[callee].type, &checker.module.types)
if !ok { if !ok {
id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer") id := source.add(checker.diagnostics, expr.span, "call target is not a function pointer")
@@ -8279,6 +8400,17 @@ build_expr :: proc(
continue 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 callee_type := checker.module.exprs[frame.left].type
_, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types) _, function_item, function_type, ok := types.function_pointer(callee_type, &checker.module.types)
if !ok { if !ok {
@@ -8897,6 +9029,22 @@ build_block :: proc(
duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start duplicate_start := scope_start if duplicate_scope_start < 0 else duplicate_scope_start
for statement_id in statements { for statement_id in statements {
statement := checker.ast_module.statements[statement_id] 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 { switch statement.kind {
case .Declaration: case .Declaration:
// A value block (`x :: { ... yield v }` / `x T = { ... }`): the parser // 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, checker, statement.target, ctx.locals^[:], ctx.global_reads, ctx.calls,
types.INVALID, ctx.pkg, ctx.file, 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 target_type := checker.module.exprs[target_expr].type
if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { if !hir_location_writable(checker, target_expr, ctx.locals^[:]) {
id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") 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, checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls,
rhs_expected, ctx.pkg, ctx.file, 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) { if types.is_many_pointer(target_type, &checker.module.types) {
assignment_op = .Pointer_Add assignment_op = .Pointer_Add
if statement.assignment_op != .Add { if statement.assignment_op != .Add {
@@ -9208,6 +9374,15 @@ build_block :: proc(
continue continue
} }
target_expr := build_global_reference(checker, global, statement.span, ctx.global_reads) 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 target_type := checker.module.exprs[target_expr].type
if !hir_location_writable(checker, target_expr, ctx.locals^[:]) { if !hir_location_writable(checker, target_expr, ctx.locals^[:]) {
id := source.add(checker.diagnostics, statement.span, "assignment target is not writable") id := source.add(checker.diagnostics, statement.span, "assignment target is not writable")
@@ -9425,7 +9600,14 @@ build_block :: proc(
}) })
case .Expression: case .Expression:
value := build_expr(checker, statement.expr, ctx.locals^[:], ctx.global_reads, ctx.calls, types.INVALID, ctx.pkg, ctx.file) 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 '_'") 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(&body, hir.stmt_id(len(checker.module.statements)))
append(&checker.module.statements, hir.Stmt{ 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 checker.current_comptime_values = spec.comptime_values
defer checker.current_comptime_values = previous_comptime defer checker.current_comptime_values = previous_comptime
signature_diagnostic := source.INVALID_DIAGNOSTIC 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 checker.specs[id].result = types.I64
spec.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 { for arg in spec.args {
if !is_runtime_type(checker, arg) { 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 checker.template_diagnostics[spec.template] != source.INVALID_DIAGNOSTIC
native_main := function.pkg == 0 && function.name == checker.main_symbol && checker.entry_point == .Plain native_main := function.pkg == 0 && function.name == checker.main_symbol && checker.entry_point == .Plain
if !function.has_body { 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))) assert(spec.hir_id == hir.function_id(len(checker.module.functions)))
append( append(
&checker.module.functions, &checker.module.functions,
@@ -11665,6 +11852,22 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) {
append(&body, block_stmt) append(&body, block_stmt)
} }
delete(block, checker.allocator) 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 && fallible_void := types.kind(spec.result, &checker.module.types) == .Fallible &&
types.is_void(types.fallible_success(spec.result, &checker.module.types)) 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) { build_globals :: proc(checker: ^Checker) {
for global, global_index in checker.ast_module.globals { 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 { if global.external {
global_type := checker.global_types[global_index] global_type := checker.global_types[global_index]
writable := global.writable writable := global.writable
@@ -11841,6 +12066,12 @@ build_globals :: proc(checker: ^Checker) {
global_type = checker.module.exprs[expr].type global_type = checker.module.exprs[expr].type
} }
diagnostic := source.INVALID_DIAGNOSTIC 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) { if !global.immutable && is_undefined_expr(checker, global.expr) {
diagnostic = source.add( diagnostic = source.add(
checker.diagnostics, checker.diagnostics,
@@ -12188,6 +12419,7 @@ check :: proc(
checker.record_field_defaults = make([]types.Type, len(checker.module.types.fields), allocator) 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_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.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) init_record_field_inference(&checker)
checker.external_global_canonical = make([]ast.Global_Id, len(ast_module.globals), allocator) 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) 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.constants = make([]Constant, len(ast_module.exprs), allocator)
checker.template_diagnostics = make([]source.Diagnostic_Id, len(ast_module.functions), allocator) checker.template_diagnostics = make([]source.Diagnostic_Id, len(ast_module.functions), allocator)
for &diagnostic in checker.template_diagnostics { for &diagnostic, index in checker.template_diagnostics {
diagnostic = source.INVALID_DIAGNOSTIC 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 { defer {
for spec in checker.specs { for spec in checker.specs {
@@ -12220,6 +12461,7 @@ check :: proc(
delete(checker.record_field_defaults, allocator) delete(checker.record_field_defaults, allocator)
delete(checker.record_field_conflicts, allocator) delete(checker.record_field_conflicts, allocator)
delete(checker.record_field_conflict_spans, allocator) delete(checker.record_field_conflict_spans, allocator)
delete(checker.poisoned_packages, allocator)
delete(checker.external_global_canonical, allocator) delete(checker.external_global_canonical, allocator)
delete(checker.external_global_diagnostics, allocator) delete(checker.external_global_diagnostics, allocator)
delete(checker.constants, allocator) delete(checker.constants, allocator)
+98 -1
View File
@@ -1486,7 +1486,7 @@ validate_declaration_aliases :: proc(state: ^State) {
import_id := find_type_import(state.module, alias.file, alias.qualifier) import_id := find_type_import(state.module, alias.file, alias.qualifier)
if import_id == ast.INVALID_IMPORT { 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 alias.valid = false
continue continue
} }
@@ -1613,6 +1613,81 @@ canonical_type :: proc(
return resolved 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) { canonicalize_types :: proc(module: ^ast.Module, allocator: mem.Allocator) {
original_count := len(module.type_store.nodes) original_count := len(module.type_store.nodes)
mapping := make([]types.Type, original_count, allocator) 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(mapping, allocator)
defer delete(visiting, allocator) defer delete(visiting, allocator)
for &function in module.functions { 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 &param in function.params { for &param in function.params {
param.type = canonical_type(module, param.type, mapping, visiting) 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) function.error = canonical_type(module, function.error, mapping, visiting)
} }
for &global in module.globals { 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) global.type = canonical_type(module, global.type, mapping, visiting)
} }
for &statement in module.statements { 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) statement.type = canonical_type(module, statement.type, mapping, visiting)
} }
for &field in module.type_fields { for &field in module.type_fields {
@@ -1687,6 +1783,7 @@ load :: proc(
} }
validate_imports(&state) validate_imports(&state)
validate_declaration_aliases(&state) validate_declaration_aliases(&state)
diagnose_qualified_type_uses(&state)
canonicalize_types(&module, allocator) canonicalize_types(&module, allocator)
return module, !state.root_failed return module, !state.root_failed
} }
+7
View File
@@ -428,6 +428,13 @@ parse_type_atom :: proc(parser: ^Parser) -> ast.Type_Syntax {
count_expr=u32(call), 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 return named
} }
source.add(parser.diagnostics, tok.span, "expected a type") source.add(parser.diagnostics, tok.span, "expected a type")
+233 -10
View File
@@ -2,6 +2,7 @@ package source
import "core:fmt" import "core:fmt"
import "core:mem" import "core:mem"
import "core:strings"
Source_Id :: distinct u32 Source_Id :: distinct u32
Diagnostic_Id :: distinct u32 Diagnostic_Id :: distinct u32
@@ -58,6 +59,20 @@ Diagnostic :: struct {
severity: Severity, 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 { Severity :: enum u8 {
Error, Error,
Warning, Warning,
@@ -67,6 +82,7 @@ Diagnostics :: struct {
source: ^Source, source: ^Source,
store: ^Store, store: ^Store,
items: [dynamic]Diagnostic, items: [dynamic]Diagnostic,
annotations: [dynamic]Annotation,
index: map[Diagnostic_Key]Diagnostic_Id, index: map[Diagnostic_Key]Diagnostic_Id,
allocator: mem.Allocator, allocator: mem.Allocator,
} }
@@ -130,6 +146,7 @@ init_diagnostics :: proc(source_file: ^Source, allocator := context.allocator) -
result.source = source_file result.source = source_file
result.allocator = allocator result.allocator = allocator
result.items.allocator = allocator result.items.allocator = allocator
result.annotations.allocator = allocator
result.index.allocator = allocator result.index.allocator = allocator
return result return result
} }
@@ -139,6 +156,7 @@ init_store_diagnostics :: proc(store: ^Store, allocator := context.allocator) ->
result.store = store result.store = store
result.allocator = allocator result.allocator = allocator
result.items.allocator = allocator result.items.allocator = allocator
result.annotations.allocator = allocator
result.index.allocator = allocator result.index.allocator = allocator
return result return result
} }
@@ -148,7 +166,11 @@ destroy_diagnostics :: proc(diagnostics: ^Diagnostics) {
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
delete(diagnostic.message, diagnostics.allocator) delete(diagnostic.message, diagnostics.allocator)
} }
for annotation in diagnostics.annotations {
delete(annotation.message, diagnostics.allocator)
}
delete(diagnostics.items) delete(diagnostics.items)
delete(diagnostics.annotations)
} }
add_with_severity :: proc(diagnostics: ^Diagnostics, span: Span, message: string, severity: Severity) -> Diagnostic_Id { 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) 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) { line_and_column :: proc(source_file: ^Source, offset: Offset) -> (line, column: int) {
if len(source_file.line_starts) > 0 { if len(source_file.line_starts) > 0 {
limit := min(int(offset), len(source_file.text)) limit := min(int(offset), len(source_file.text))
@@ -232,6 +298,140 @@ source_for_span :: proc(diagnostics: ^Diagnostics, span: Span) -> ^Source {
return diagnostics.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 { format :: proc(diagnostics: ^Diagnostics, id: Diagnostic_Id, allocator := context.allocator) -> string {
index, ok := diagnostic_index(id, len(diagnostics.items)) index, ok := diagnostic_index(id, len(diagnostics.items))
if !ok { if !ok {
@@ -246,19 +446,42 @@ format :: proc(diagnostics: ^Diagnostics, id: Diagnostic_Id, allocator := contex
diagnostic := diagnostics.items[index] diagnostic := diagnostics.items[index]
source_file := source_for_span(diagnostics, diagnostic.span) source_file := source_for_span(diagnostics, diagnostic.span)
severity := "warning" if diagnostic.severity == .Warning else "error" 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 { if source_file == nil {
return fmt.aprintf("<unknown>: %s: %s", severity, diagnostic.message, allocator=allocator) return fmt.aprintf("<unknown>: %s: %s", severity, diagnostic.message, allocator=allocator)
} }
line, column := line_and_column(source_file, diagnostic.span.start) builder := strings.builder_make(allocator)
return fmt.aprintf( fmt.sbprintf(&builder, "%s: %s\n", severity, diagnostic.message)
"%s:%d:%d: %s: %s", primary_label := ""
source_file.path, if annotation, found := annotation_for(diagnostics, id, .Primary); found {
line, primary_label = annotation.message
column, }
severity, _ = write_excerpt(&builder, diagnostics, diagnostic.span, primary_label, true)
diagnostic.message, for annotation in diagnostics.annotations {
allocator=allocator, 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) { print_all :: proc(diagnostics: ^Diagnostics) {
+222 -15
View File
@@ -917,8 +917,8 @@ multi_source_diagnostics_report_the_originating_file :: proc(t: ^testing.T) {
found := false found := false
for _, diagnostic_index in diagnostics.items { for _, diagnostic_index in diagnostics.items {
message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index)) message := source.format(&diagnostics, source.diagnostic_id(diagnostic_index))
if strings.contains(message, "/b.bro:2:9:") && if strings.contains(message, "--> ") && strings.contains(message, "/b.bro:2:9") &&
strings.contains(message, "unknown package alias 'math'") { strings.contains(message, "unknown symbol 'math'") {
found = true found = true
} }
delete(message) delete(message)
@@ -1956,7 +1956,7 @@ old_intrinsic_spellings_are_not_recognized :: proc(t: ^testing.T) {
} }
found := [len(names)]bool{} found := [len(names)]bool{}
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
if !strings.contains(diagnostic.message, "unresolved function") { if !strings.contains(diagnostic.message, "unknown symbol") {
continue continue
} }
for name, index in names { for name, index in names {
@@ -4306,6 +4306,34 @@ main func() i32 {
testing.expect(t, indirect_calls > 0) 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) @(test)
function_literals_do_not_capture_locals :: proc(t: ^testing.T) { function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
text := `main func() i32 { text := `main func() i32 {
@@ -4330,7 +4358,7 @@ function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
found := false found := false
for diagnostic in diagnostics.items { 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) 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, len(store.items[source_id].line_starts), 3)
testing.expect_value(t, first, second) testing.expect_value(t, first, second)
testing.expect_value(t, len(diagnostics.items), 2) 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) @(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, len(diagnostics.items), 2)
testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning) testing.expect_value(t, diagnostics.items[warning].severity, source.Severity.Warning)
testing.expect_value(t, diagnostics.items[err].severity, source.Severity.Error) 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_warning, "warning: same\n --> test.bro:1:1"))
testing.expect(t, strings.contains(formatted_error, "test.bro:1:1: error: same")) 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) @(test)
@@ -7923,7 +8035,7 @@ referenced_missing_package_traps_at_reference :: proc(t: ^testing.T) {
defer delete(stdout) defer delete(stdout)
defer delete(stderr) defer delete(stderr)
testing.expect(t, !state.success) 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) @(test)
@@ -7991,8 +8103,8 @@ file_hidden_declarations_are_not_package_members :: proc(t: ^testing.T) {
found_import := false found_import := false
found_collision := false found_collision := false
for diagnostic in diagnostics.items { for diagnostic in diagnostics.items {
found_sibling = found_sibling || strings.contains(diagnostic.message, "unresolved function 'sibling'") found_sibling = found_sibling || strings.contains(diagnostic.message, "unknown symbol 'sibling'")
found_sibling_value = found_sibling_value || strings.contains(diagnostic.message, "unresolved global 'sibling_value'") 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_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_import = found_import || strings.contains(diagnostic.message, "package 'dep' has no member 'secret'")
found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'") found_collision = found_collision || strings.contains(diagnostic.message, "duplicate function 'collision'")
@@ -8221,7 +8333,7 @@ main func() void {}
wants := []string{ wants := []string{
"has no member 'missing'", "has no member 'missing'",
"is file-hidden", "is file-hidden",
"unknown package alias 'nope'", "unknown symbol 'nope'",
"unavailable imported package 'gone'", "unavailable imported package 'gone'",
"package member 'dep.ambiguous' is ambiguous", "package member 'dep.ambiguous' is ambiguous",
"duplicate declaration alias 'duplicate'", "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") 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") 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") 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'") immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable local 'value'")
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate 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_value(t, count_mismatches, 2)
testing.expect(t, duplicate) testing.expect(t, duplicate)
@@ -8978,7 +9090,7 @@ if_unwrap_binding_is_scoped_to_then_block :: proc(t: ^testing.T) {
found := false found := false
for diagnostic in diagnostics.items { 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) testing.expect(t, found)
} }
@@ -9433,7 +9545,7 @@ main func() void {
redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'") redeclaration = redeclaration || strings.contains(diagnostic.message, "duplicate local 'item'")
immutable = immutable || strings.contains(diagnostic.message, "cannot assign immutable 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") 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 integer_bounds += 1 if strings.contains(diagnostic.message, "range bounds must be compatible concrete integers") else 0
} }
testing.expect(t, unsupported) testing.expect(t, unsupported)
@@ -11920,6 +12032,101 @@ main func() void {
testing.expect(t, found) 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( named_record_field_type :: proc(
module: ^hir.Module, module: ^hir.Module,
symbols: ^symbol.Table, symbols: ^symbol.Table,
+1 -1
View File
@@ -1,4 +1,4 @@
mem :: import "@std/mem" import "@std/mem"
ArrayList func($T type) type { ArrayList func($T type) type {
return struct { return struct {
+2 -2
View File
@@ -1,5 +1,5 @@
c :: import "@ffi/c" import "@ffi/c"
io :: import "@std/io" import "@std/io"
hide write func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { hide write func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError {
request usize = bytes.len request usize = bytes.len
+4 -9
View File
@@ -1,5 +1,5 @@
c :: import "@ffi/c" import "@ffi/c"
meta :: import "@std/meta" import "@std/meta"
ReadError :: enum { ReadError :: enum {
read_failed read_failed
@@ -276,7 +276,7 @@ hide write_integer func(writer Writer, $T type, value T, base u64, uppercase boo
return 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 { hide write_float func(writer Writer, $T type, value T, scientific bool) void ! WriteError {
match typeinfo!(T) { match typeinfo!(T) {
.float: { .float: {
@@ -353,12 +353,7 @@ hide write_default func(writer Writer, $T type, value T) void ! WriteError {
return return
} }
print func( print func(writer Writer, $format []u8, $Args type, args Args) void ! WriteError {
writer Writer,
$format []u8,
$Args type,
args Args,
) void ! WriteError {
inline for parse_format(format.len, format, Args) |token| { inline for parse_format(format.len, format, Args) |token| {
if (token.kind == .unused) { if (token.kind == .unused) {
break break
+1 -1
View File
@@ -1,4 +1,4 @@
c :: import "@ffi/c" import "@ffi/c"
AllocError :: enum { AllocError :: enum {
out_of_memory out_of_memory
+1 -1
View File
@@ -1,4 +1,4 @@
io :: import "@std/io" import "@std/io"
Init :: struct { Init :: struct {
io io.Io io io.Io