From 66e41e1d9a9e96f42ded1cec4eaa55dbe80f23a9 Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Fri, 12 Jun 2026 17:27:47 +0200 Subject: [PATCH] prune dead specializations --- TODO.md | 1 - benchmarks/symbols/README.md | 5 ++ compiler/checker/checker.odin | 122 +++++++++++++++++++++++++++------- compiler_tests.odin | 107 +++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 26 deletions(-) diff --git a/TODO.md b/TODO.md index b9af602..70969d3 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,6 @@ # compiler hardening follow-ups -- prune unreachable function specializations before HIR construction and emission - support unary minus, including the signed i64 minimum literal boundary - move ignored example binaries into a dedicated build directory and remove `.review_tmp` diff --git a/benchmarks/symbols/README.md b/benchmarks/symbols/README.md index 5fb5e3a..c95561a 100644 --- a/benchmarks/symbols/README.md +++ b/benchmarks/symbols/README.md @@ -35,3 +35,8 @@ After migrating persistent compiler references and spans to compact IDs on `Span` is 12 bytes and `Token` is 24 bytes; AST expressions, HIR expressions, and IR instructions are 64, 88, and 88 bytes respectively. These sizes are also printed by the benchmark to catch layout regressions. + +After pruning stale function specializations on 2026-06-12, peak memory +remained 8,651,659 bytes and allocations fell to 30,121. Lookup-only +specialization resolution avoids constructing temporary signatures for +already discovered calls. diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 90f11cf..aaaa2a4 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -510,16 +510,28 @@ find_infer_local :: proc(locals: []Infer_Local, name: symbol.Id) -> types.Type { return types.INVALID } -spec_signature_equal :: proc(spec: Spec, template: ast.Function_Id, args: []types.Type) -> bool { - if spec.template != template || len(spec.args) != len(args) { - return false - } - for arg, index in args { - if !types.equal(spec.args[index], arg) { - return false +find_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { + function := checker.ast_module.functions[template] + for spec, index in checker.specs { + if spec.template != template || len(spec.args) != len(function.params) { + continue + } + matches := true + for param, param_index in function.params { + actual := types.INVALID + if param_index < len(actual_args) { + actual = actual_args[param_index] + } + if !types.equal(spec.args[param_index], specialized_param_type(param.type, actual)) { + matches = false + break + } + } + if matches { + return spec_id(index) } } - return true + return INVALID_SPEC } specialized_param_type :: proc(syntax: ast.Type_Syntax, actual: types.Type) -> types.Type { @@ -544,6 +556,9 @@ can_specialize :: proc(function: ast.Function, actual_args: []types.Type) -> boo } ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: []types.Type) -> Spec_Id { + if existing := find_spec(checker, template, actual_args); existing != INVALID_SPEC { + return existing + } function := checker.ast_module.functions[template] signature: [dynamic]types.Type signature.allocator = checker.allocator @@ -554,12 +569,6 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [ } append(&signature, specialized_param_type(param.type, actual)) } - for spec, index in checker.specs { - if spec_signature_equal(spec, template, signature[:]) { - delete(signature) - return spec_id(index) - } - } result := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == .Int { result = types.I32 @@ -572,6 +581,14 @@ ensure_spec :: proc(checker: ^Checker, template: ast.Function_Id, actual_args: [ return index } +mark_spec_demanded :: proc(checker: ^Checker, id: Spec_Id, stack: ^[dynamic]Spec_Id) { + if id == INVALID_SPEC || checker.specs[id].hir_id != hir.INVALID_FUNCTION { + return + } + checker.specs[id].hir_id = hir.Function_Id(0) + append(stack, id) +} + Infer_Frame :: struct { expr: ast.Expr_Id, stage: u8, @@ -587,6 +604,7 @@ infer_expr :: proc( locals: []Infer_Local, pkg := ast.Package_Id(0), file := ast.File_Id(0), + demanded: ^[dynamic]Spec_Id = nil, ) -> types.Type { stack := checker.infer_stack clear_dynamic_array(&stack) @@ -694,8 +712,19 @@ infer_expr :: proc( } function := checker.ast_module.functions[frame.template] if can_specialize(function, stack[frame_index].args) { - spec := ensure_spec(checker, frame.template, stack[frame_index].args) - last = checker.specs[spec].result + spec := INVALID_SPEC + if demanded == nil { + spec = ensure_spec(checker, frame.template, stack[frame_index].args) + } else if len(expr.args) == len(function.params) { + spec = find_spec(checker, frame.template, stack[frame_index].args) + mark_spec_demanded(checker, spec, demanded) + } + if spec != INVALID_SPEC { + last = checker.specs[spec].result + } else { + declared := type_from_syntax(function.result) + last = declared if declared.kind == .Concrete || declared.kind == .Void else types.INVALID + } } else { declared := type_from_syntax(function.result) if function.pkg == 0 && function.name == checker.main_symbol && function.result == .Int { @@ -712,7 +741,7 @@ infer_expr :: proc( return last } -infer_spec_result :: proc(checker: ^Checker, id: Spec_Id) -> types.Type { +infer_spec_result :: proc(checker: ^Checker, id: Spec_Id, demanded: ^[dynamic]Spec_Id = nil) -> types.Type { spec := checker.specs[id] function := checker.ast_module.functions[spec.template] declared := type_from_syntax(function.result) @@ -736,17 +765,17 @@ infer_spec_result :: proc(checker: ^Checker, id: Spec_Id) -> types.Type { statement := checker.ast_module.statements[statement_id] #partial switch statement.kind { case .Declaration: - value_type := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) + value_type := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded) declared_local := type_from_syntax(statement.type) if declared_local.kind == .Concrete { value_type = declared_local } append(&locals, Infer_Local{name = statement.name, type = value_type}) case .Assignment, .Expression: - _ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) + _ = infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded) case .Return: if statement.expr != ast.INVALID_EXPR { - returned := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file) + returned := infer_expr(checker, statement.expr, locals[:], function.pkg, function.file, demanded) if !types.is_valid(result) { result = returned } else { @@ -794,10 +823,10 @@ infer_all :: proc(checker: ^Checker) { changed := false spec_count := len(checker.specs) for global, index in checker.ast_module.globals { + inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file) if type_from_syntax(global.type).kind == .Concrete { continue } - inferred := infer_expr(checker, global.expr, nil, global.pkg, global.file) changed = merge_inferred_type(&checker.global_types[index], inferred) || changed } for index := 0; index < len(checker.specs); index += 1 { @@ -814,6 +843,38 @@ infer_all :: proc(checker: ^Checker) { } } +prune_specs :: proc(checker: ^Checker) { + stack: [dynamic]Spec_Id + stack.allocator = checker.allocator + defer delete(stack) + + main_template := find_template(checker, checker.main_symbol, 0) + if main_template != ast.INVALID_FUNCTION { + mark_spec_demanded(checker, find_spec(checker, main_template, nil), &stack) + } + for global in checker.ast_module.globals { + _ = infer_expr(checker, global.expr, nil, global.pkg, global.file, &stack) + } + for len(stack) > 0 { + id := pop(&stack) + _ = infer_spec_result(checker, id, &stack) + } + + retained := 0 + for spec in checker.specs { + if spec.hir_id == hir.INVALID_FUNCTION { + delete(spec.args, checker.allocator) + continue + } + checker.specs[retained] = spec + checker.specs[retained].hir_id = hir.function_id(retained) + retained += 1 + } + for len(checker.specs) > retained { + _ = pop(&checker.specs) + } +} + add_hir_expr :: proc(checker: ^Checker, expr: hir.Expr) -> hir.Expr_Id { id := hir.expr_id(len(checker.module.exprs)) append(&checker.module.exprs, expr) @@ -1125,9 +1186,22 @@ build_expr :: proc( continue } } - spec := ensure_spec(checker, frame.template, stack[frame_index].arg_types) + spec := find_spec(checker, frame.template, stack[frame_index].arg_types) delete(stack[frame_index].arg_types, checker.allocator) stack[frame_index].arg_types = nil + if spec == INVALID_SPEC { + id := source.addf( + checker.diagnostics, + expr.span, + "could not resolve specialization of '%s'", + symbol_text(checker, expr.name), + ) + delete(stack[frame_index].built_args, checker.allocator) + stack[frame_index].built_args = nil + last = invalid_hir_expr(checker, expr.span, id) + _ = pop(&stack) + continue + } for _, index in stack[frame_index].built_args { stack[frame_index].built_args[index] = coerce_expr( checker, @@ -1997,9 +2071,7 @@ check :: proc( validate_declarations(&checker) infer_all(&checker) - for index in 0..