prune dead specializations
This commit is contained in:
@@ -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`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
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
|
||||
}
|
||||
for arg, index in args {
|
||||
if !types.equal(spec.args[index], arg) {
|
||||
return false
|
||||
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
|
||||
}
|
||||
}
|
||||
return true
|
||||
if matches {
|
||||
return spec_id(index)
|
||||
}
|
||||
}
|
||||
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)
|
||||
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))
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user