prune dead specializations

This commit is contained in:
2026-06-12 17:27:47 +02:00
parent 4112b79c6b
commit 66e41e1d9a
4 changed files with 209 additions and 26 deletions
-1
View File
@@ -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`
+5
View File
@@ -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.
+97 -25
View File
@@ -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..<len(checker.specs) {
checker.specs[index].hir_id = hir.function_id(index)
}
prune_specs(&checker)
build_globals(&checker)
for index := 0; index < len(checker.specs); index += 1 {
build_function(&checker, spec_id(index))
+107
View File
@@ -869,6 +869,113 @@ main :: func() void {
testing.expect_value(t, len(hir_module.functions), 3)
}
@(test)
stale_specializations_are_pruned_after_inference :: proc(t: ^testing.T) {
text := `derived :: identity(make())
wide :: delayed()
identity :: func(value int) int {
return value
}
make :: func() int {
return wide
return 1
}
delayed :: func() int {
return 128
}
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)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 4)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__identity__i16"))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity__i8"))
}
@(test)
eager_global_calls_root_specializations :: proc(t: ^testing.T) {
text := `make :: func() i32 {
return 7
}
unused_native :: func() i32 {
return 9
}
unused_foreign :: c func() i32
value i32 :: make()
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)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, len(hir_module.functions), 2)
testing.expect(t, strings.contains(llvm_text, "@bro__p0__make("))
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__unused_native("))
testing.expect(t, !strings.contains(llvm_text, "@unused_foreign("))
}
@(test)
malformed_generic_calls_do_not_retain_specializations :: proc(t: ^testing.T) {
text := `identity :: func(value int) int {
return value
}
bad :: identity(1, 2)
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)
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
llvm_text := llvm.emit(&ir_module, &diagnostics, &symbols)
defer delete(llvm_text)
found_arity := false
for diagnostic in diagnostics.items {
found_arity = found_arity || strings.contains(diagnostic.message, "expects 1 arguments, got 2")
}
testing.expect(t, found_arity)
testing.expect_value(t, len(hir_module.functions), 1)
testing.expect(t, !strings.contains(llvm_text, "@bro__p0__identity"))
testing.expect(t, strings.contains(llvm_text, "call void @bro.trap"))
}
@(test)
long_generic_call_chain_reaches_a_fixed_point :: proc(t: ^testing.T) {
builder := strings.builder_make()