From 1165cfb7c0897f75b93e6996d2e8e4cced3b3c2f Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Wed, 15 Jul 2026 08:24:50 +0200 Subject: [PATCH] fix comptime specialization, implement io.print --- LANGUAGE.md | 2 +- TODO.md | 2 + compiler/checker/checker.odin | 500 ++++++++++++++++++++++++------ compiler/checker/comptime.odin | 97 ++++-- compiler_tests.odin | 205 +++++++++++- examples/programs/tuples/main.bro | 1 + std/io/io.bro | 81 +++-- 7 files changed, 733 insertions(+), 155 deletions(-) diff --git a/LANGUAGE.md b/LANGUAGE.md index 0d8c02b..dc9157f 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -152,7 +152,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - explicit comptime type parameters such as `max func($T type, a, b T) T`, specialized by type and omitted from the runtime ABI - comptime type/integer/string parameters may appear anywhere, are erased from the runtime ABI, and may be omitted when uniquely recoverable from runtime arguments or the immediate expected result; `_` is an explicit inference hole - forced typed comptime expressions such as `$sum(1, 2)`, `$Point { x = 1, y = 2 }`, and comptime value blocks such as `${ yield 4 }` -- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values +- comptime execution for bodyful Brolang functions with mutable locals, loops, `defer`, `match`, `try`/`catch`, pointer/slice storage mutation, pointer captures, and calls through comptime-known function values; `undefined` storage may be initialized at comptime, but remaining poison cannot be observed - comptime type factories such as `Box func($T type) type { return struct { value T } }`; calls like `Box(i32)` are concrete nominal types and may appear anywhere a type is expected - tuple types are unnamed-field structs (`struct { i32, []u8 }`), tuple values use `{1, "bro"}` / `{1,}` / `{}`, and fields use canonical numeric names such as `.0` - `typeinfo!`, `field!`, `compile_error!`, and semantic `inline for` provide compile-time record reflection and heterogeneous static expansion without runtime metadata; reflected aggregates remain persistent compile-time values, and inline-loop `break` / `continue` must be selected entirely at comptime diff --git a/TODO.md b/TODO.md index 0d2dd6b..2776663 100644 --- a/TODO.md +++ b/TODO.md @@ -867,6 +867,8 @@ 38. place every intrinsic behind direct unqualified `name!(...)` syntax, freeing the bare names for user functions (implemented) +39. aggregate comptime parameters and richer formatting + ## A word on unchecked casts For casts that bypass safety checks, Honey provides builtin functions: diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 99e3ec5..67f2825 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -59,6 +59,11 @@ Static_Binding :: struct { value: Ct_Value_Id, } +Inline_Expansion :: struct { + statement: ast.Stmt_Id, + index: u32, +} + Entry_Point_Kind :: enum u8 { Invalid, Plain, @@ -167,6 +172,15 @@ Type_Factory_Origin :: struct { values: []Comptime_Value, } +Call_Resolution :: struct { + expr: ast.Expr_Id, + ctx: []Comptime_Value, + inline_ctx: []Inline_Expansion, + mapping: []int, + comptime_values: []Comptime_Value, + runtime_types: []types.Type, +} + Checker :: struct { ast_module: ^ast.Module, diagnostics: ^source.Diagnostics, @@ -218,9 +232,11 @@ Checker :: struct { current_comptime_values: []Comptime_Value, static_state: Ct_State, static_bindings: [dynamic]Static_Binding, + inline_context: [dynamic]Inline_Expansion, type_factories: [dynamic]Type_Factory_Entry, generated_types: [dynamic]Generated_Type_Entry, type_factory_origins: [dynamic]Type_Factory_Origin, + call_resolutions: [dynamic]Call_Resolution, target: target.Target, allocator: mem.Allocator, } @@ -289,6 +305,86 @@ build_local_expr :: proc(checker: ^Checker, local: Build_Local, span: source.Spa }) } +block_reads_name :: proc(checker: ^Checker, statements: []ast.Stmt_Id, name: symbol.Id) -> bool { + // Imported C expressions can be deeply nested, so walk the source graph iteratively. + statement_stack: [dynamic]ast.Stmt_Id + statement_stack.allocator = checker.allocator + defer delete(statement_stack) + expr_stack: [dynamic]ast.Expr_Id + expr_stack.allocator = checker.allocator + defer delete(expr_stack) + append(&statement_stack, ..statements) + + for len(statement_stack) > 0 || len(expr_stack) > 0 { + if len(statement_stack) > 0 { + statement_id := pop(&statement_stack) + if statement_id == ast.INVALID_STMT || int(statement_id) >= len(checker.ast_module.statements) { + continue + } + statement := checker.ast_module.statements[statement_id] + #partial switch statement.kind { + case .Declaration, .Assignment, .Return, .Expression, .Yield: + append(&expr_stack, statement.expr, statement.target) + append(&statement_stack, ..statement.body) + case .If: + append(&expr_stack, statement.expr, statement.guard) + append(&statement_stack, ..statement.body) + append(&statement_stack, ..statement.else_body) + case .While: + append(&expr_stack, statement.expr) + append(&statement_stack, ..statement.body) + if statement.update != ast.INVALID_STMT { + append(&statement_stack, statement.update) + } + case .For: + append(&expr_stack, statement.expr) + append(&statement_stack, ..statement.body) + case .Block: + append(&statement_stack, ..statement.body) + case .Defer: + if statement.update != ast.INVALID_STMT { + append(&statement_stack, statement.update) + } + case .Match, .Match_Arm: + append(&expr_stack, statement.expr) + append(&expr_stack, ..statement.patterns) + append(&statement_stack, ..statement.body) + case .Break, .Continue, .Invalid: + } + continue + } + + expr_id := pop(&expr_stack) + if expr_id == ast.INVALID_EXPR || int(expr_id) >= len(checker.ast_module.exprs) { + continue + } + expr := checker.ast_module.exprs[expr_id] + if expr.kind == .Name || expr.kind == .Call { + if !symbol.is_valid(expr.qualifier) && expr.name == name || expr.qualifier == name { + return true + } + } + switch expr.kind { + case .Call, .Array, .Struct_Literal, .Slice: + append(&expr_stack, ..expr.args) + append(&expr_stack, expr.left) + case .Negate, .Not, .Address, .Deref, .Field, .Unwrap, .Try, .Keyed, .Enum_Literal, .Cast: + append(&expr_stack, expr.left) + case .Comptime: + append(&expr_stack, expr.left) + append(&statement_stack, ..expr.body) + case .Catch: + append(&expr_stack, expr.left, expr.right) + append(&statement_stack, ..expr.body) + case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range: + append(&expr_stack, expr.left, expr.right) + case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Inference_Hole, + .Type, .Name, .Function_Literal, .Anonymous_Struct_Type: + } + } + return false +} + record_unused_locals :: proc( checker: ^Checker, locals: []hir.Local, @@ -1520,6 +1616,18 @@ next_runtime_call_arg :: proc(function: ast.Function, mapping: []int, start, sou return index } +call_mapping_mode :: proc(function: ast.Function, mapping: []int) -> Call_Argument_Mode { + if len(mapping) != len(function.params) { + return .Inferred + } + for value, index in mapping { + if value != index { + return .Inferred + } + } + return .Explicit +} + comptime_binding_index :: proc(function: ast.Function, _: int, name: symbol.Id) -> (int, bool) { ordinal := 0 for param in function.params { @@ -1600,29 +1708,6 @@ explicit_comptime_argument_valid :: proc( return false } -mapping_explicit_arguments_valid :: proc( - checker: ^Checker, - function: ast.Function, - mapping: []int, - args: []ast.Expr_Id, - pkg: ast.Package_Id, - file: ast.File_Id, -) -> bool { - for source_index in 0..= len(function.params) || - !function.params[param_index].comptime_value { - continue - } - if !explicit_comptime_argument_valid( - checker, function, function.params[param_index], args[source_index], pkg, file, - ) { - return false - } - } - return true -} - search_call_mappings :: proc(search: ^Call_Mapping_Search, param_index, source_index: int) { if len(search.candidates^) >= COMPTIME_EVAL_QUOTA { return @@ -1664,7 +1749,7 @@ call_mapping_semantically_valid :: proc( locals: []Infer_Local, pkg: ast.Package_Id, file: ast.File_Id, -) -> bool { +) -> (bool, string) { actual_args := make([]types.Type, len(function.params), checker.allocator) defer delete(actual_args, checker.allocator) for source_index in 0.. 0 { + return false, inference_failure + } + return false, fmt.aprintf("could not infer or evaluate every comptime parameter", allocator=checker.allocator) } + delete(inference_failure, checker.allocator) previous := checker.current_comptime_values checker.current_comptime_values = values defer checker.current_comptime_values = previous @@ -1699,35 +1793,50 @@ call_mapping_semantically_valid :: proc( } actual := actual_args[index] declared := type_from_syntax(checker, param.type, function.pkg, function.file) + source_index := -1 + for mapped_param, candidate_source in mapping { + if mapped_param == index { + source_index = candidate_source + break + } + } if !is_runtime_type(checker, actual) { - return false + return false, fmt.aprintf( + "argument %d is not a runtime value", source_index+1, + allocator=checker.allocator, + ) } if types.is_constraint(declared) { if !types.is_valid(types.constraint_target(declared, actual, &checker.module.types)) { - return false + return false, fmt.aprintf( + "argument %d of type %s does not satisfy %s", + source_index+1, type_label(checker, actual), type_label(checker, declared), + allocator=checker.allocator, + ) } } else if !can_implicitly_convert_type(checker, actual, declared) { - source_index := -1 - for mapped_param, candidate_source in mapping { - if mapped_param == index { - source_index = candidate_source - break - } - } if source_index < 0 || !is_numeric_constant_expr(checker, args[source_index]) || !expr_accepts_numeric_demand(checker, args[source_index], declared, locals, pkg, file) { - return false + return false, fmt.aprintf( + "argument %d of type %s cannot convert to %s", + source_index+1, type_label(checker, actual), type_label(checker, declared), + allocator=checker.allocator, + ) } } } if is_runtime_type(checker, expected) { result := function_channel_type(checker, function) if !is_runtime_type(checker, result) || !can_implicitly_convert_type(checker, result, expected) { - return false + return false, fmt.aprintf( + "result type %s cannot convert to expected type %s", + type_label(checker, result), type_label(checker, expected), + allocator=checker.allocator, + ) } } - return true + return true, "" } call_argument_mapping :: proc( @@ -1738,16 +1847,16 @@ call_argument_mapping :: proc( file: ast.File_Id, expected := types.INVALID, locals: []Infer_Local = nil, -) -> (mapping: []int, mode: Call_Argument_Mode, comptime_count: int) { +) -> (mapping: []int, mode: Call_Argument_Mode, comptime_count: int, failure: string) { if function.variadic || function.c_abi { if !valid_call_arity(function^, len(args)) { - return nil, .Invalid, 0 + return nil, .Invalid, 0, "" } mapping = make([]int, len(args), checker.allocator) for &value, index in mapping { value = index } - return mapping, .Explicit, 0 + return mapping, .Explicit, 0, "" } current := make([]int, len(args), checker.allocator) defer delete(current, checker.allocator) @@ -1771,36 +1880,50 @@ call_argument_mapping :: proc( } search_call_mappings(&search, 0, 0) selected_index := -1 + failures: [dynamic]string + failures.allocator = checker.allocator + defer { + for item in failures { + delete(item, checker.allocator) + } + delete(failures) + } if len(candidates) == 1 { selected_index = 0 - } else { - explicit_candidate := -1 - explicit_count := 0 - for candidate, index in candidates { - if mapping_explicit_arguments_valid(checker, function^, candidate, args, pkg, file) { - explicit_candidate = index - explicit_count += 1 - } - } - if explicit_count == 1 { - selected_index = explicit_candidate - } } if selected_index < 0 && len(candidates) > 1 { for candidate, index in candidates { - if !mapping_explicit_arguments_valid(checker, function^, candidate, args, pkg, file) { - continue - } - if call_mapping_semantically_valid(checker, function^, candidate, args, expected, locals, pkg, file) { + valid, reason := call_mapping_semantically_valid( + checker, function^, candidate, args, expected, locals, pkg, file, + ) + if valid { if selected_index >= 0 { - return nil, .Invalid, comptime_param_count(function^) + return nil, .Invalid, comptime_param_count(function^), fmt.aprintf( + "multiple complete argument mappings satisfy the call", + allocator=checker.allocator, + ) } selected_index = index + } else { + append(&failures, reason) } } } if selected_index < 0 { - return nil, .Invalid, comptime_param_count(function^) + if len(failures) > 0 { + builder := strings.builder_make(checker.allocator) + defer strings.builder_destroy(&builder) + for reason, index in failures { + if index > 0 { + strings.write_string(&builder, "; ") + } + fmt.sbprintf(&builder, "candidate %d: %s", index+1, reason) + } + return nil, .Invalid, comptime_param_count(function^), fmt.aprintf( + "%s", strings.to_string(builder), allocator=checker.allocator, + ) + } + return nil, .Invalid, comptime_param_count(function^), "" } selected := make([]int, len(args), checker.allocator) copy(selected, candidates[selected_index]) @@ -1813,7 +1936,7 @@ call_argument_mapping :: proc( } } } - return selected, .Explicit if identity else .Inferred, comptime_param_count(function^) + return selected, .Explicit if identity else .Inferred, comptime_param_count(function^), "" } bind_inferred_comptime :: proc( @@ -2082,6 +2205,7 @@ infer_call_comptime_values :: proc( pkg: ast.Package_Id, file: ast.File_Id, diagnose := false, + failure: ^string = nil, ) -> ([]Comptime_Value, bool) { values := make([]Comptime_Value, prefix, checker.allocator) bound := make([]bool, prefix, checker.allocator) @@ -2123,6 +2247,12 @@ infer_call_comptime_values :: proc( if is_comptime_type_param(checker, param) { actual, ok := resolve_type_argument(checker, arg_id, pkg, file) if !ok { + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf( + "argument %d for comptime type parameter '%s' is not a type", + source_index+1, symbol_text(checker, param.name), allocator=checker.allocator, + ) + } if diagnose { source.addf(checker.diagnostics, expr.span, "argument for comptime type parameter '%s' must be a type", @@ -2136,6 +2266,12 @@ infer_call_comptime_values :: proc( } else if is_comptime_string_param(checker, param, function) { text, text_ok := comptime_string_argument(checker, arg_id, pkg, file) if !text_ok { + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf( + "argument %d for comptime string parameter '%s' does not evaluate to immutable bytes", + source_index+1, symbol_text(checker, param.name), allocator=checker.allocator, + ) + } if diagnose { source.addf(checker.diagnostics, expr.span, "argument for comptime string parameter '%s' must evaluate to immutable bytes", @@ -2155,6 +2291,12 @@ infer_call_comptime_values :: proc( declared := type_from_syntax(checker, param.type, function.pkg, function.file) constant := eval_integer_constant_in_context(checker, arg_id, pkg, file) if constant.kind != .Value { + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf( + "argument %d for comptime parameter '%s' is not a compile-time integer expression", + source_index+1, symbol_text(checker, param.name), allocator=checker.allocator, + ) + } if diagnose { source.addf(checker.diagnostics, expr.span, "argument for comptime parameter '%s' must be a compile-time integer expression", @@ -2164,6 +2306,12 @@ infer_call_comptime_values :: proc( continue } if !fits_integer_type(constant.value, declared, checker.target) { + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf( + "argument %d integer constant %d does not fit in %s", + source_index+1, constant.value, types.name(declared), allocator=checker.allocator, + ) + } if diagnose { source.addf(checker.diagnostics, expr.span, "integer constant %d does not fit in %s", constant.value, types.name(declared)) @@ -2243,6 +2391,12 @@ infer_call_comptime_values :: proc( continue } matched = false + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf( + "cannot infer comptime parameter '%s'", + symbol_text(checker, param.name), allocator=checker.allocator, + ) + } if diagnose { source.addf( checker.diagnostics, param.span, @@ -2253,6 +2407,9 @@ infer_call_comptime_values :: proc( ordinal += 1 } if !matched { + if failure != nil && len(failure^) == 0 { + failure^ = fmt.aprintf("comptime inference produced conflicting bindings", allocator=checker.allocator) + } delete(values, checker.allocator) return nil, false } @@ -2329,6 +2486,77 @@ clone_comptime_values :: proc(values: []Comptime_Value, allocator: mem.Allocator return result } +inline_expansions_equal :: proc(left, right: []Inline_Expansion) -> bool { + if len(left) != len(right) { + return false + } + for value, index in left { + if value != right[index] { + return false + } + } + return true +} + +find_call_resolution :: proc( + checker: ^Checker, + expr: ast.Expr_Id, +) -> (int, bool) { + for index := len(checker.call_resolutions)-1; index >= 0; index -= 1 { + entry := checker.call_resolutions[index] + if entry.expr == expr && + comptime_values_equal(entry.ctx, checker.current_comptime_values) && + inline_expansions_equal(entry.inline_ctx, checker.inline_context[:]) { + return index, true + } + } + return -1, false +} + +store_call_resolution :: proc( + checker: ^Checker, + expr: ast.Expr_Id, + mapping: []int, + comptime_values: []Comptime_Value, + runtime_types: []types.Type, +) { + entry := Call_Resolution{ + expr=expr, + ctx=clone_comptime_values(checker.current_comptime_values, checker.allocator), + inline_ctx=slice.clone(checker.inline_context[:], checker.allocator), + mapping=slice.clone(mapping, checker.allocator), + comptime_values=clone_comptime_values(comptime_values, checker.allocator), + runtime_types=slice.clone(runtime_types, checker.allocator), + } + if index, ok := find_call_resolution(checker, expr); ok { + previous := checker.call_resolutions[index] + delete(previous.ctx, checker.allocator) + delete(previous.inline_ctx, checker.allocator) + delete(previous.mapping, checker.allocator) + delete(previous.comptime_values, checker.allocator) + delete(previous.runtime_types, checker.allocator) + checker.call_resolutions[index] = entry + return + } + append(&checker.call_resolutions, entry) +} + +resolved_call_arg_expected :: proc( + checker: ^Checker, + function: ast.Function, + param_index: int, + resolution_index: int, +) -> types.Type { + if resolution_index < 0 || resolution_index >= len(checker.call_resolutions) { + return call_arg_expected(checker, function, param_index) + } + previous := checker.current_comptime_values + checker.current_comptime_values = checker.call_resolutions[resolution_index].comptime_values + result := call_arg_expected(checker, function, param_index) + checker.current_comptime_values = previous + return result +} + resolve_generated_struct_type :: proc(checker: ^Checker, expr_id: ast.Expr_Id, pkg: ast.Package_Id, file: ast.File_Id) -> types.Type { for entry in checker.generated_types { if entry.expr == expr_id && comptime_values_equal(entry.values, checker.current_comptime_values) { @@ -4273,9 +4501,10 @@ infer_expr :: proc( continue } function := checker.ast_module.functions[template] - mapping, mode, prefix := call_argument_mapping( + mapping, mode, prefix, mapping_failure := call_argument_mapping( checker, &function, expr.args, pkg, file, frame.expected, locals, ) + delete(mapping_failure, checker.allocator) if mode == .Invalid { last = types.INVALID _ = pop(&stack) @@ -4388,6 +4617,10 @@ infer_expr :: proc( defer delete(comptime_values, checker.allocator) if comptime_ok && can_specialize(checker, function, stack[frame_index].args, comptime_values) { + store_call_resolution( + checker, frame.expr, frame.mapping, + comptime_values, stack[frame_index].args, + ) previous_comptime := checker.current_comptime_values checker.current_comptime_values = comptime_values for source_index in 0.. bool { @@ -7586,14 +7831,24 @@ build_expr :: proc( _ = pop(&stack) continue } - infer_locals := make([]Infer_Local, len(locals), checker.allocator) - for local, index in locals { - infer_locals[index] = Infer_Local{name=local.name, type=local.type, declared=local.type} + resolution_index, resolved := find_call_resolution(checker, frame.expr) + mapping: []int + mode := Call_Argument_Mode.Invalid + prefix := comptime_param_count(function) + mapping_failure := "" + if resolved { + mapping = slice.clone(checker.call_resolutions[resolution_index].mapping, checker.allocator) + mode = call_mapping_mode(function, mapping) + } else { + infer_locals := make([]Infer_Local, len(locals), checker.allocator) + for local, index in locals { + infer_locals[index] = Infer_Local{name=local.name, type=local.type, declared=local.type} + } + mapping, mode, prefix, mapping_failure = call_argument_mapping( + checker, &function, expr.args, pkg, file, frame.expected, infer_locals, + ) + delete(infer_locals, checker.allocator) } - mapping, mode, prefix := call_argument_mapping( - checker, &function, expr.args, pkg, file, frame.expected, infer_locals, - ) - delete(infer_locals, checker.allocator) if mode == .Invalid { id := source.INVALID_DIAGNOSTIC if !function_has_comptime_params(function) { @@ -7603,6 +7858,12 @@ build_expr :: proc( checker.diagnostics, expr.span, message, symbol_text(checker, expr.name), len(function.params), len(expr.args), ) + } else if len(mapping_failure) > 0 { + id = source.addf( + checker.diagnostics, expr.span, + "call to '%s' has no unique complete argument mapping: %s", + symbol_text(checker, expr.name), mapping_failure, + ) } else { id = source.addf( checker.diagnostics, expr.span, @@ -7610,6 +7871,7 @@ build_expr :: proc( symbol_text(checker, expr.name), ) } + delete(mapping_failure, checker.allocator) last = invalid_hir_expr(checker, expr.span, id) _ = pop(&stack) continue @@ -7617,6 +7879,7 @@ build_expr :: proc( stack[frame_index].template = template stack[frame_index].arg_mode = mode stack[frame_index].prefix = prefix + stack[frame_index].resolution = resolution_index if resolved else -1 stack[frame_index].mapping = mapping stack[frame_index].built_args = make([]hir.Expr_Id, len(expr.args), checker.allocator) stack[frame_index].arg_types = make([]types.Type, len(function.params), checker.allocator) @@ -7627,7 +7890,9 @@ build_expr :: proc( stack[frame_index].stage = 3 if stack[frame_index].arg_index < len(expr.args) { param_index := call_param_index(mapping, stack[frame_index].arg_index) - arg_expected := call_arg_expected(checker, function, param_index) + arg_expected := resolved_call_arg_expected( + checker, function, param_index, stack[frame_index].resolution, + ) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } @@ -7693,7 +7958,10 @@ build_expr :: proc( stack[frame_index].arg_index = next if next < len(expr.args) { next_param := call_param_index(frame.mapping, next) - arg_expected := call_arg_expected(checker, checker.ast_module.functions[frame.template], next_param) + arg_expected := resolved_call_arg_expected( + checker, checker.ast_module.functions[frame.template], next_param, + frame.resolution, + ) if !is_runtime_type(checker, arg_expected) { arg_expected = types.INVALID } @@ -7704,10 +7972,18 @@ build_expr :: proc( function := checker.ast_module.functions[frame.template] comptime_values: []Comptime_Value comptime_ok := false - comptime_values, comptime_ok = infer_call_comptime_values( - checker, function, frame.prefix, frame.mapping, expr.args, stack[frame_index].arg_types, - frame.expected, pkg, file, diagnose=true, - ) + if frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) { + comptime_values = clone_comptime_values( + checker.call_resolutions[frame.resolution].comptime_values, + checker.allocator, + ) + comptime_ok = true + } else { + comptime_values, comptime_ok = infer_call_comptime_values( + checker, function, frame.prefix, frame.mapping, expr.args, stack[frame_index].arg_types, + frame.expected, pkg, file, diagnose=true, + ) + } defer delete(comptime_values, checker.allocator) if comptime_ok { previous_comptime := checker.current_comptime_values @@ -7759,8 +8035,12 @@ build_expr :: proc( checker.current_comptime_values = previous_comptime } spec := INVALID_SPEC - if comptime_ok { - spec = find_spec(checker, frame.template, stack[frame_index].arg_types, comptime_values) + if comptime_ok && frame.resolution >= 0 && frame.resolution < len(checker.call_resolutions) { + spec = find_spec( + checker, frame.template, + checker.call_resolutions[frame.resolution].runtime_types, + comptime_values, + ) } delete(stack[frame_index].arg_types, checker.allocator) stack[frame_index].arg_types = nil @@ -8178,6 +8458,12 @@ inline_field_bindings :: proc( return nil, .Diagnosed if state.diagnostic != source.INVALID_DIAGNOSTIC else .Invalid } value := state.values[value_id] + if ct_value_contains_undefined(&state, value_id) { + if diagnose { + _ = ct_fail(&state, .Not_Comptime, checker.ast_module.exprs[expr].span, "inline for cannot expand an undefined comptime value") + } + return nil, .Diagnosed if state.diagnostic != source.INVALID_DIAGNOSTIC else .Invalid + } if value.kind == .Range { parts := ct_child_slice(&state, value) if len(parts) != 2 { @@ -8242,9 +8528,16 @@ inline_field_bindings :: proc( return bindings, .None } -push_inline_binding :: proc(checker: ^Checker, binding: Static_Binding, index_name: symbol.Id, index: int) -> int { +push_inline_binding :: proc( + checker: ^Checker, + binding: Static_Binding, + index_name: symbol.Id, + index: int, + statement: ast.Stmt_Id, +) -> int { start := len(checker.static_bindings) append(&checker.static_bindings, binding) + append(&checker.inline_context, Inline_Expansion{statement=statement, index=u32(index)}) if symbol.is_valid(index_name) { value := ct_add_value(&checker.static_state, Ct_Value{kind=.Integer, type=types.USIZE, integer=i128(index)}) append(&checker.static_bindings, Static_Binding{name=index_name, type=types.USIZE, value=value}) @@ -8254,6 +8547,7 @@ push_inline_binding :: proc(checker: ^Checker, binding: Static_Binding, index_na pop_inline_binding :: proc(checker: ^Checker, start: int) { resize(&checker.static_bindings, start) + _ = pop(&checker.inline_context) } Inline_Control :: enum u8 { @@ -8406,10 +8700,12 @@ flatten_inline_iteration :: proc( } if contains_inline_control(checker, statement.body, target_label, true) || contains_inline_control(checker, statement.else_body, target_label, true) { - diagnostic^ = source.add( - checker.diagnostics, statement.span, - "break or continue targeting an inline loop must be compile-time-resolvable", - ) + if diagnostic != nil { + diagnostic^ = source.add( + checker.diagnostics, statement.span, + "break or continue targeting an inline loop must be compile-time-resolvable", + ) + } return .Invalid } } @@ -8443,10 +8739,12 @@ flatten_inline_iteration :: proc( continue } if contains_inline_control(checker, statement.body, target_label, true) { - diagnostic^ = source.add( - checker.diagnostics, statement.span, - "break or continue targeting an inline loop must be compile-time-resolvable", - ) + if diagnostic != nil { + diagnostic^ = source.add( + checker.diagnostics, statement.span, + "break or continue targeting an inline loop must be compile-time-resolvable", + ) + } return .Invalid } } @@ -8467,10 +8765,12 @@ flatten_inline_iteration :: proc( } if (statement.kind == .For || statement.kind == .While || statement.kind == .Defer) && contains_inline_control(checker, []ast.Stmt_Id{statement_id}, target_label, false) { - diagnostic^ = source.add( - checker.diagnostics, statement.span, - "break or continue targeting an inline loop must be compile-time-resolvable", - ) + if diagnostic != nil { + diagnostic^ = source.add( + checker.diagnostics, statement.span, + "break or continue targeting an inline loop must be compile-time-resolvable", + ) + } return .Invalid } append(out, statement_id) @@ -9274,7 +9574,7 @@ build_block :: proc( ctx.problematic^ = true } else if inline_error == .None { for binding, inline_index in bindings { - binding_start := push_inline_binding(checker, binding, statement.index_name, inline_index) + binding_start := push_inline_binding(checker, binding, statement.index_name, inline_index, statement_id) iteration: [dynamic]ast.Stmt_Id iteration.allocator = checker.allocator diagnostic := source.INVALID_DIAGNOSTIC @@ -11166,6 +11466,9 @@ build_function :: proc(checker: ^Checker, id: Spec_Id) { hir.Local{name = param.name, type = param_type, parameter = true}, param.span, ) + if param.name != checker.sink_symbol && block_reads_name(checker, function.body, param.name) { + local_used[int(local_id)] = true + } append(&locals, Build_Local{name = param.name, type = param_type, id = local_id}) append(¶ms, local_id) runtime_index += 1 @@ -11765,7 +12068,9 @@ check :: proc( checker.type_factories.allocator = allocator checker.generated_types.allocator = allocator checker.type_factory_origins.allocator = allocator + checker.call_resolutions.allocator = allocator checker.static_bindings.allocator = allocator + checker.inline_context.allocator = allocator checker.static_state = ct_state_make(&checker, 0, ast.INVALID_FILE) build_symbol_indexes(&checker) checker.global_types = make([]types.Type, len(ast_module.globals), allocator) @@ -11828,11 +12133,20 @@ check :: proc( for origin in checker.type_factory_origins { delete(origin.values, allocator) } + for resolution in checker.call_resolutions { + delete(resolution.ctx, allocator) + delete(resolution.inline_ctx, allocator) + delete(resolution.mapping, allocator) + delete(resolution.comptime_values, allocator) + delete(resolution.runtime_types, allocator) + } delete(checker.type_factories) delete(checker.generated_types) delete(checker.type_factory_origins) + delete(checker.call_resolutions) ct_state_destroy(&checker.static_state) delete(checker.static_bindings) + delete(checker.inline_context) } for function, index in ast_module.functions { diff --git a/compiler/checker/comptime.odin b/compiler/checker/comptime.odin index 274a0f6..926cf6c 100644 --- a/compiler/checker/comptime.odin +++ b/compiler/checker/comptime.odin @@ -210,6 +210,7 @@ ct_place_id :: proc(index: int) -> Ct_Place_Id { Ct_Value_Kind :: enum u8 { Invalid, Void, + Undefined, Integer, Float, Bool, @@ -460,6 +461,34 @@ ct_child_slice :: proc(state: ^Ct_State, value: Ct_Value) -> []Ct_Value_Id { return state.children[start:end] } +ct_value_contains_undefined :: proc(state: ^Ct_State, id: Ct_Value_Id, depth := 0) -> bool { + if depth > 64 || id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return false + } + value := state.values[id] + if value.kind == .Undefined { + return true + } + for child in ct_child_slice(state, value) { + if ct_value_contains_undefined(state, child, depth+1) { + return true + } + } + return false +} + +ct_observe_value :: proc(state: ^Ct_State, id: Ct_Value_Id, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { + if id == INVALID_CT_VALUE || int(id) >= len(state.values) { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + if state.values[id].kind == .Undefined { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( + state, .Not_Comptime, span, "cannot read an undefined value at comptime", + ) + } + return id, ct_flow(.Normal), true +} + ct_fail :: proc(state: ^Ct_State, kind: Ct_Error_Kind, span: source.Span, message: string) -> bool { if state.error == .None { state.error = kind @@ -795,6 +824,11 @@ ct_materialize_value :: proc( } value := state.values[materialized] #partial switch value.kind { + case .Undefined: + if state.diagnostic == source.INVALID_DIAGNOSTIC { + state.diagnostic = source.add(checker.diagnostics, span, "cannot materialize an undefined comptime value") + } + return invalid_hir_expr(checker, span, state.diagnostic, value.type) case .Integer: if types.is_enum(value.type, &checker.module.types) { int_value := i64(value.integer) if value.integer < 0 else transmute(i64)u64(value.integer) @@ -920,20 +954,8 @@ ct_undefined_value :: proc(state: ^Ct_State, value_type: types.Type, depth := 0) return INVALID_CT_VALUE, false } store := &state.checker.module.types - if types.is_concrete_integer(value_type) || types.is_enum(value_type, store) { - return ct_add_value(state, Ct_Value{kind=.Integer, type=value_type}), true - } - if types.is_bool(value_type) { - return ct_add_value(state, Ct_Value{kind=.Bool, type=value_type}), true - } - if types.is_float(value_type, state.checker.target) { - return ct_add_value(state, Ct_Value{kind=.Float, type=value_type}), true - } item, ok := types.node(store, value_type) - if !ok { - return INVALID_CT_VALUE, false - } - if item.kind == .Array { + if ok && item.kind == .Array { children := make([]Ct_Value_Id, int(item.count), state.checker.allocator) defer delete(children, state.checker.allocator) for &child in children { @@ -944,7 +966,7 @@ ct_undefined_value :: proc(state: ^Ct_State, value_type: types.Type, depth := 0) append(&state.children, ..children) return ct_add_value(state, Ct_Value{kind=.Array, type=value_type, start=start, count=u32(len(children))}), true } - if item.kind == .Struct && !item.opaque { + if ok && item.kind == .Struct && !item.opaque { fields := types.fields_for(store, value_type) children := make([]Ct_Value_Id, len(fields), state.checker.allocator) defer delete(children, state.checker.allocator) @@ -956,7 +978,7 @@ ct_undefined_value :: proc(state: ^Ct_State, value_type: types.Type, depth := 0) append(&state.children, ..children) return ct_add_value(state, Ct_Value{kind=.Struct, type=value_type, start=start, count=u32(len(children))}), true } - return INVALID_CT_VALUE, false + return ct_add_value(state, Ct_Value{kind=.Undefined, type=value_type}), true } ct_eval_expr :: proc( @@ -992,7 +1014,7 @@ ct_eval_expr :: proc( case .Name: if !symbol.is_valid(expr.qualifier) { if index, ok := ct_find_binding_index(state, expr.name); ok { - return ct_binding_value(state, index), ct_flow(.Normal), true + return ct_observe_value(state, ct_binding_value(state, index), expr.span) } if value, ok := current_comptime_value(checker, expr.name); ok { if value.kind == .Integer { @@ -1113,7 +1135,10 @@ ct_eval_expr :: proc( return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime slice index out of bounds") } value, value_ok := ct_place_get(state, place) - return value, ct_flow(.Normal), value_ok + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + return ct_observe_value(state, value, expr.span) } if base.kind == .Pointer { pointer_item, pointer_ok := types.node(store, base.type) @@ -1124,7 +1149,10 @@ ct_eval_expr :: proc( return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime pointer index out of bounds") } value, value_ok := ct_place_get(state, place) - return value, ct_flow(.Normal), value_ok + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + return ct_observe_value(state, value, expr.span) } if array_item, array_ok := types.node(store, pointer_item.child); array_ok && array_item.kind == .Array { base_place, _, _ := ct_pointer_place(state, base) @@ -1133,7 +1161,10 @@ ct_eval_expr :: proc( } place := ct_extend_place(state, base_place, Ct_Path_Elem{kind=.Index, index=u32(index_value)}, array_item.child, pointer_item.mutable && array_item.mutable) value, value_ok := ct_place_get(state, place) - return value, ct_flow(.Normal), value_ok + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + return ct_observe_value(state, value, expr.span) } } } @@ -1249,7 +1280,7 @@ ct_eval_expr :: proc( if !value_ok { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, expr.span, "comptime pointer no longer points to live storage") } - return value, ct_flow(.Normal), true + return ct_observe_value(state, value, expr.span) case .Slice: return ct_eval_slice_expr(state, expr, depth+1) case .Undefined: @@ -1611,7 +1642,7 @@ ct_eval_field_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, name: symbol } field_place := ct_extend_place(state, place, Ct_Path_Elem{kind=.Field, index=u32(index)}, field.type, false) if value, value_ok := ct_place_get(state, field_place); value_ok { - return value, ct_flow(.Normal), true + return ct_observe_value(state, value, span) } } } @@ -1627,12 +1658,12 @@ ct_eval_field_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, name: symbol if int(base.active) != index || len(children) == 0 || children[0] == INVALID_CT_VALUE { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "variant '%s' has no payload to read", field_name) } - return children[0], ct_flow(.Normal), true + return ct_observe_value(state, children[0], span) } if index < 0 || index >= len(children) || types.is_void(field.type) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "field '%s' has no value", field_name) } - return children[index], ct_flow(.Normal), true + return ct_observe_value(state, children[index], span) } ct_eval_tuple_field_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, index: u64, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { @@ -1649,14 +1680,17 @@ ct_eval_tuple_field_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, index: } field_place := ct_extend_place(state, place, Ct_Path_Elem{kind=.Field, index=u32(field_index)}, field.type, false) value, value_ok := ct_place_get(state, field_place) - return value, ct_flow(.Normal), value_ok + if !value_ok { + return INVALID_CT_VALUE, ct_flow(.Normal), false + } + return ct_observe_value(state, value, span) } field_index, _, ok := find_tuple_field(state.checker, base_type, index) children := ct_child_slice(state, base) if !ok || field_index < 0 || field_index >= len(children) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "tuple field index is out of bounds") } - return children[field_index], ct_flow(.Normal), true + return ct_observe_value(state, children[field_index], span) } ct_eval_index_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, index: int, span: source.Span) -> (Ct_Value_Id, Ct_Flow, bool) { @@ -1670,7 +1704,7 @@ ct_eval_index_value :: proc(state: ^Ct_State, base_id: Ct_Value_Id, index: int, if index < 0 || index >= len(children) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime array index out of bounds") } - return children[index], ct_flow(.Normal), true + return ct_observe_value(state, children[index], span) } if base.kind == .String { if base.index >= u64(len(checker.ast_module.strings)) || index < 0 || index >= len(checker.ast_module.strings[base.index]) { @@ -2546,6 +2580,12 @@ ct_eval_template_call :: proc( if !ok { return INVALID_CT_VALUE, ct_flow(.Normal), false } + if ct_value_contains_undefined(state, value) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( + state, .Not_Comptime, checker.ast_module.exprs[args[index]].span, + "cannot pass an undefined value at comptime", + ) + } append(&runtime_values, value) append(&runtime_types, param_type) append(&runtime_names, param.name) @@ -2583,6 +2623,11 @@ ct_eval_template_call :: proc( return INVALID_CT_VALUE, ct_flow(.Normal), ct_failf(state, .Not_Comptime, span, "comptime function '%s' did not return a value", symbol_text(checker, function.name)) } result := flow.value + if ct_value_contains_undefined(state, result) { + return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail( + state, .Not_Comptime, span, "comptime function returned an undefined value", + ) + } if ct_value_references_dead_storage(state, result) { return INVALID_CT_VALUE, ct_flow(.Normal), ct_fail(state, .Not_Comptime, span, "comptime function returned a pointer to expired storage") } diff --git a/compiler_tests.odin b/compiler_tests.odin index a550640..e3faea8 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2425,6 +2425,7 @@ milestone_37_tuples_reflection_inline_for_and_debug_print_compile_and_run :: pro testing.expect_value(t, len(diagnostics.items), 0) testing.expect(t, !strings.contains(llvm_text, "FormatToken")) testing.expect(t, !strings.contains(llvm_text, "parse_format")) + testing.expect(t, !strings.contains(llvm_text, "format_field_name")) testing.expect(t, !strings.contains(llvm_text, "FieldInfo")) testing.expect(t, !strings.contains(llvm_text, "RecordInfo")) @@ -2441,7 +2442,7 @@ milestone_37_tuples_reflection_inline_for_and_debug_print_compile_and_run :: pro defer delete(stderr) testing.expect_value(t, state.exit_code, 0) testing.expect_value(t, string(stdout), "") - testing.expect_value(t, string(stderr), "tuple=40/bro, limits=-9223372036854775808/18446744073709551615") + testing.expect_value(t, string(stderr), "hello!\ntuple=40/bro, limits=-9223372036854775808/18446744073709551615") } @(test) @@ -2531,6 +2532,171 @@ milestone_37_inline_loop_control_must_be_statically_resolvable :: proc(t: ^testi testing.expect(t, found) } +@(test) +milestone_37_comptime_undefined_aggregates_support_full_initialization :: proc(t: ^testing.T) { + text := `Token :: struct { + text []u8 + count usize +} +Partial :: struct { initialized i32, text []u8 } +make_tokens func() [2]mut Token { + tokens [2]mut Token = undefined + tokens[0] = Token {text = "a", count = 1} + tokens[1].text = "bro" + tokens[1].count = 3 + return tokens +} +read_initialized_sibling func() i32 { + value Partial = undefined + value.initialized = 42 + return value.initialized +} +answer :: $read_initialized_sibling() +main func() i32 { + total usize = 0 + inline for make_tokens() |token| { + total += token.text.len + token.count + } + if answer != 42 or total != 8 { + return 1 + } + return 0 +} +` + 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), 0) +} + +@(test) +milestone_37_comptime_undefined_values_cannot_be_observed :: proc(t: ^testing.T) { + text := `Bad :: struct { value i32, text []u8 } +read_scalar func() i32 { + value i32 = undefined + return value +} +return_partial func() Bad { + value Bad = undefined + value.value = 1 + return value +} +take_bad func(value Bad) i32 { + return value.value +} +pass_partial func() i32 { + value Bad = undefined + value.value = 1 + return take_bad(value) +} +bad_scalar :: $read_scalar() +bad_record :: $return_partial() +bad_argument :: $pass_partial() +main func() void {} +` + source_file := source.Source{path="test.bro", text=text} + diagnostics := source.init_diagnostics(&source_file) + defer source.destroy_diagnostics(&diagnostics) + symbols := symbol.init_table() + defer symbol.destroy_table(&symbols) + stream := lexer.lex(&source_file, &diagnostics, &symbols) + defer delete(stream.items) + ast_module := parser.parse(&stream, &source_file, &diagnostics) + defer ast.destroy_module(&ast_module) + hir_module := checker.check(&ast_module, &diagnostics, &symbols) + defer hir.destroy_module(&hir_module) + + found_read := false + found_return := false + found_pass := false + for diagnostic in diagnostics.items { + found_read = found_read || strings.contains(diagnostic.message, "cannot read an undefined value at comptime") + found_return = found_return || strings.contains(diagnostic.message, "comptime function returned an undefined value") + found_pass = found_pass || strings.contains(diagnostic.message, "cannot pass an undefined value at comptime") + } + testing.expect(t, found_read) + testing.expect(t, found_return) + testing.expect(t, found_pass) +} + +@(test) +milestone_37_inline_expansions_keep_distinct_call_resolutions :: proc(t: ^testing.T) { + text := `identity func($T type, value T) T { + return value +} +main func() i32 { + total i64 = 0 + inline for {{i8(1), i16(2)}, {i32(3), i64(4)}} |row| { + inline for row |value| { + total += i64(identity(value)) + } + } + return i32(total - 10) +} +` + 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) + + identity_symbol := symbol.intern(&symbols, "identity") + specializations := 0 + for function in hir_module.functions { + specializations += 1 if function.name == identity_symbol else 0 + } + testing.expect_value(t, len(diagnostics.items), 0) + testing.expect_value(t, specializations, 4) +} + +@(test) +milestone_37_inline_control_prunes_inference_after_static_exit :: proc(t: ^testing.T) { + text := `take_i8 func(value i8) void { _ = value } +main func() void { + inline for {i8(1), "skip"} |value, index| { + if index == 1 { + continue + } + take_i8(value) + } + inline for {i8(1), "stop"} |value, index| { + if index == 1 { + break + } + take_i8(value) + } +} +` + 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), 0) +} + @(test) milestone_33_rejects_an_incompatible_runtime_write_declaration :: proc(t: ^testing.T) { text := `write c_func(_ c_int, _ c_int, _ c_ulong) c_long @@ -2877,6 +3043,36 @@ main func() void { testing.expect(t, found_function) } +@(test) +generic_parameter_usage_is_source_based :: proc(t: ^testing.T) { + text := `choose func($N usize, used, unused i32) i32 { + if N > 0 { + return used + } + return 0 +} +main func() void { + _ = choose(0, 1, 2) +} +` + 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].severity, source.Severity.Warning) + testing.expect(t, strings.contains(diagnostics.items[0].message, "unused parameter 'unused'")) + testing.expect(t, !strings.contains(diagnostics.items[0].message, "unused parameter 'used'")) +} + @(test) recursive_specialization_reaches_a_fixed_point :: proc(t: ^testing.T) { text := `a func(value int) i32 { @@ -3204,6 +3400,7 @@ unknown func($T type) T { value T = undefined; return value } partial func($T type, $N usize, value T) T { return value } use_ignored func($T type, value Ignored(T)) i32 { return value } use_alias func($T type, value BoxAlias(T)) T { return value.value } +mapping_fail func($A usize, value i32, $B usize) void {} main func() void { a i32 :: 1 b u32 :: 2 @@ -3213,6 +3410,7 @@ main func() void { _ = use_ignored(a) box Box(i32) :: Box(i32) { value = 1 } _ = use_alias(box) + mapping_fail(true, false) } ` source_file := source.Source{path="test.bro", text=text} @@ -3230,12 +3428,16 @@ main func() void { found_conflict := false found_unknown := false found_partial := false + found_candidate_failures := false unrecoverable := 0 for diagnostic in diagnostics.items { message := diagnostic.message found_conflict = found_conflict || strings.contains(message, "conflicting inference for comptime parameter 'T': i32 and u32") found_unknown = found_unknown || strings.contains(message, "cannot infer comptime parameter 'T'") found_partial = found_partial || strings.contains(message, "cannot infer comptime parameter 'N'") + found_candidate_failures = found_candidate_failures || + strings.contains(message, "candidate 1: cannot infer comptime parameter 'B'") && + strings.contains(message, "candidate 2: cannot infer comptime parameter 'A'") if strings.contains(message, "cannot infer comptime parameter 'T'") { unrecoverable += 1 } @@ -3243,6 +3445,7 @@ main func() void { testing.expect(t, found_conflict) testing.expect(t, found_unknown) testing.expect(t, found_partial) + testing.expect(t, found_candidate_failures) testing.expect(t, unrecoverable >= 3) } diff --git a/examples/programs/tuples/main.bro b/examples/programs/tuples/main.bro index 7a3ff29..1b7b442 100644 --- a/examples/programs/tuples/main.bro +++ b/examples/programs/tuples/main.bro @@ -77,6 +77,7 @@ main func() i32 { return 4 } field!(&numbers, "2") += 1 + debug.print("hello!\n", {}) debug.print(format(), { numbers.2, "bro", diff --git a/std/io/io.bro b/std/io/io.bro index 779dda6..b736643 100644 --- a/std/io/io.bro +++ b/std/io/io.bro @@ -130,13 +130,13 @@ hide FormatToken :: struct { kind FormatTokenKind start usize end usize - arg usize + field []u8 } hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { tokens [N]mut FormatToken = undefined for (usize(0))..format.len |index| { - tokens[index] = FormatToken {kind = .unused, start = 0, end = 0, arg = 0} + tokens[index] = FormatToken {kind = .unused, start = 0, end = 0, field = ""} } field_count usize = 0 match typeinfo!(Args) { @@ -160,12 +160,12 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { compile_error!("io.print format has an unmatched '{'") } if cursor > literal_start { - tokens[token_count] = FormatToken {kind = .literal, start = literal_start, end = cursor, arg = 0} + tokens[token_count] = FormatToken {kind = .literal, start = literal_start, end = cursor, field = ""} token_count += 1 } next :: format[cursor + 1] if next == '{' { - tokens[token_count] = FormatToken {kind = .literal, start = cursor, end = cursor + 1, arg = 0} + tokens[token_count] = FormatToken {kind = .literal, start = cursor, end = cursor + 1, field = ""} token_count += 1 cursor += 2 literal_start = cursor @@ -182,7 +182,15 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { } else { compile_error!("io.print format has an unknown specifier") } - tokens[token_count] = FormatToken {kind = kind, start = 0, end = 0, arg = argument_count} + if argument_count >= field_count { + compile_error!("io.print format argument count does not match the tuple") + } + tokens[token_count] = FormatToken { + kind = kind, + start = 0, + end = 0, + field = format_field_name(Args, argument_count), + } token_count += 1 argument_count += 1 cursor += 3 @@ -194,10 +202,10 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { compile_error!("io.print format has an unmatched '}'") } if cursor > literal_start { - tokens[token_count] = FormatToken {kind = .literal, start = literal_start, end = cursor, arg = 0} + tokens[token_count] = FormatToken {kind = .literal, start = literal_start, end = cursor, field = ""} token_count += 1 } - tokens[token_count] = FormatToken {kind = .literal, start = cursor, end = cursor + 1, arg = 0} + tokens[token_count] = FormatToken {kind = .literal, start = cursor, end = cursor + 1, field = ""} token_count += 1 cursor += 2 literal_start = cursor @@ -206,7 +214,7 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { cursor += 1 } if literal_start < format.len { - tokens[token_count] = FormatToken {kind = .literal, start = literal_start, end = format.len, arg = 0} + tokens[token_count] = FormatToken {kind = .literal, start = literal_start, end = format.len, field = ""} } if argument_count != field_count { compile_error!("io.print format argument count does not match the tuple") @@ -214,39 +222,44 @@ hide parse_format func($N usize, $format []u8, $Args type) [N]mut FormatToken { return tokens } +hide format_field_name func($T type, index usize) []u8 { + match typeinfo!(T) { + .record |record|: return record.fields[index].name + else: compile_error!("io.print arguments must be a tuple") + } +} + +hide write_decimal func(writer Writer, $T type, value T) void ! WriteError { + match typeinfo!(T) { + .integer: if minval!(T) < 0 { + try write_decimal_signed(writer, i64(value)) + } else { + try write_decimal_unsigned(writer, u64(value)) + } + else: compile_error!("io.print '{d}' requires an integer argument") + } + return +} + print func( writer Writer, $format []u8, $Args type, args Args, ) void ! WriteError { - match typeinfo!(Args) { - .record |record|: inline for $parse_format(format.len, format, Args) |token| { - if (token.kind == .literal) { - try write_all(writer, format[token.start..token.end]) - } - if (token.kind == .string or token.kind == .decimal) { - inline for record.fields |field| { - if (field.index == token.arg) { - if (token.kind == .string) { - try write_all(writer, field!(args, field.name)) - } else { - match typeinfo!(field.type) { - .integer: { - if minval!(field.type) < 0 { - try write_decimal_signed(writer, i64(field!(args, field.name))) - } else { - try write_decimal_unsigned(writer, u64(field!(args, field.name))) - } - } - else: compile_error!("io.print '{d}' requires an integer argument") - } - } - } - } - } + inline for parse_format(format.len, format, Args) |token| { + if (token.kind == .unused) { + break } - else: compile_error!("io.print arguments must be a tuple") + if (token.kind == .literal) { + try write_all(writer, format[token.start..token.end]) + continue + } + if (token.kind == .string) { + try write_all(writer, field!(args, token.field)) + continue + } + try write_decimal(writer, field!(args, token.field)) } return }