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