function literals (non-capturing)

This commit is contained in:
2026-07-07 17:04:38 +02:00
parent 95c61311ca
commit f171a6579d
5 changed files with 186 additions and 20 deletions
+2
View File
@@ -104,6 +104,7 @@ Expr_Kind :: enum u8 {
Call,
Try,
Catch,
Function_Literal,
}
Expr :: struct {
@@ -209,6 +210,7 @@ Function :: struct {
file: File_Id,
c_abi: bool,
imported: bool,
generated: bool,
has_body: bool,
variadic: bool,
params: []Param,
+80 -5
View File
@@ -518,9 +518,20 @@ find_import_symbol :: proc(index: []Import_Index_Entry, scope: ast.File_Id, name
}
build_symbol_indexes :: proc(checker: ^Checker) {
checker.function_index = make([]Function_Index_Entry, len(checker.ast_module.functions), checker.allocator)
function_count := 0
for function in checker.ast_module.functions {
if !function.generated {
function_count += 1
}
}
checker.function_index = make([]Function_Index_Entry, function_count, checker.allocator)
function_index := 0
for function, id in checker.ast_module.functions {
checker.function_index[id] = Function_Index_Entry{scope=function.pkg, name=function.name, id=ast.function_id(id)}
if function.generated {
continue
}
checker.function_index[function_index] = Function_Index_Entry{scope=function.pkg, name=function.name, id=ast.function_id(id)}
function_index += 1
}
slice.sort_by(checker.function_index, function_index_less)
@@ -1038,6 +1049,12 @@ mark_expr_imports_used :: proc(checker: ^Checker, expr_id: ast.Expr_Id, file: as
append(&stack, expr.right)
}
mark_block_imports_used(checker, expr.body, file)
case .Function_Literal:
function_id := ast.Function_Id(u32(expr.integer))
if function_id != ast.INVALID_FUNCTION && int(function_id) < len(checker.ast_module.functions) {
function := checker.ast_module.functions[function_id]
mark_block_imports_used(checker, function.body, function.file)
}
case .Add, .Sub, .Mul, .Div, .Index, .Orelse, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
append(&stack, expr.left, expr.right)
case .Invalid, .Integer, .Float, .String, .Bool, .None, .Undefined, .Type, .Name:
@@ -1865,6 +1882,11 @@ infer_expr :: proc(
.Comptime, .Bool, .Not, .Eq, .Ne, .Lt, .Le, .Gt, .Ge, .And, .Or, .Range:
last = infer_compound_expr(checker, expr, locals, pkg, file, demanded, local_types)
_ = pop(&stack)
case .Function_Literal:
template := ast.Function_Id(u32(expr.integer))
pointer_type, _, ok := function_pointer_type_for_template(checker, template, demanded)
last = pointer_type if ok else types.INVALID
_ = pop(&stack)
case .Name:
last = types.INVALID
if !symbol.is_valid(expr.qualifier) {
@@ -2421,7 +2443,7 @@ infer_statements :: proc(
if statement.expr != ast.INVALID_EXPR {
returned := infer_expr(checker, statement.expr, locals^[:], pkg, file, demanded, local_types)
if is_runtime_type(checker, result_hint) {
_ = record_demand(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
_ = record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
expr := checker.ast_module.exprs[statement.expr]
if expr.kind == .Name && !symbol.is_valid(expr.qualifier) {
if local_index, ok := find_infer_local_index(locals^[:], expr.name); ok {
@@ -2433,7 +2455,7 @@ infer_statements :: proc(
returned = result_hint
} else {
// `return G` for a global const: demand the result type onto it.
record_demand(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
record_demand_shallow(checker, statement.expr, result_hint, locals^[:], local_types, pkg, file)
}
}
}
@@ -2861,6 +2883,49 @@ expr_accepts_numeric_demand :: proc(
return false
}
DEMAND_RECURSION_LIMIT :: 4096
record_demand_too_deep :: proc(checker: ^Checker, expr_id: ast.Expr_Id) -> bool {
stack: [dynamic]ast.Expr_Id
stack.allocator = checker.allocator
defer delete(stack)
append(&stack, expr_id)
seen := 0
for len(stack) > 0 {
current := pop(&stack)
if current == ast.INVALID_EXPR || int(current) >= len(checker.ast_module.exprs) {
continue
}
seen += 1
if seen > DEMAND_RECURSION_LIMIT {
return true
}
expr := checker.ast_module.exprs[current]
#partial switch expr.kind {
case .Negate:
append(&stack, expr.left)
case .Add, .Sub, .Mul, .Div:
append(&stack, expr.left, expr.right)
}
}
return false
}
record_demand_shallow :: proc(
checker: ^Checker,
expr_id: ast.Expr_Id,
demand: types.Type,
locals: []Infer_Local,
local_types: []types.Type,
pkg: ast.Package_Id,
file: ast.File_Id,
) -> bool {
if record_demand_too_deep(checker, expr_id) {
return false
}
return record_demand(checker, expr_id, demand, locals, local_types, pkg, file)
}
// record_demand pushes a concrete type demand onto open numeric slots reachable
// through bare names and numeric arithmetic. Calls remain a boundary (milestone 14.5).
record_demand :: proc(
@@ -4569,6 +4634,10 @@ build_expr :: proc(
checker, expr, locals, global_reads, calls, frame.expected, pkg, file,
)
_ = pop(&stack)
case .Function_Literal:
template := ast.Function_Id(u32(expr.integer))
last = build_function_value(checker, template, expr.span, frame.expected)
_ = pop(&stack)
case .Type:
id := source.add(checker.diagnostics, expr.span, "type is not a runtime value")
last = invalid_hir_expr(checker, expr.span, id)
@@ -5234,6 +5303,9 @@ make_link_name :: proc(checker: ^Checker, id: Spec_Id) -> string {
if function.pkg == 0 && function.name == checker.main_symbol {
return fmt.aprintf("main", allocator = checker.allocator)
}
if function.generated {
return fmt.aprintf("bro__p%d__anon%d", function.pkg, spec.template, allocator = checker.allocator)
}
if !function.has_body && function.c_abi {
if len(function.link_name) > 0 {
return strings.clone(function.link_name, checker.allocator)
@@ -8423,8 +8495,11 @@ check :: proc(
}
for function, index in ast_module.functions {
if function.generated {
continue
}
for previous in ast_module.functions[:index] {
if previous.pkg == function.pkg && previous.name == function.name {
if !previous.generated && previous.pkg == function.pkg && previous.name == function.name {
source.addf(diagnostics, function.span, "duplicate function '%s'", symbol_text(&checker, function.name))
}
}
+52
View File
@@ -736,6 +736,8 @@ parse_primary :: proc(parser: ^Parser, nesting: int) -> ast.Expr_Id {
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
case .Keyword_Func:
return parse_function_literal(parser)
case .Left_Bracket:
return parse_array_literal(parser, nesting)
case .Dot:
@@ -2111,6 +2113,56 @@ parse_function :: proc(parser: ^Parser, name: token.Token, c_abi: bool) {
})
}
parse_function_literal :: proc(parser: ^Parser) -> ast.Expr_Id {
start := advance(parser)
if _, ok := allow(parser, .Left_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected '(' after 'func'")
}
params, variadic := parse_params(parser)
if _, ok := allow(parser, .Right_Paren); !ok {
source.add(parser.diagnostics, current(parser).span, "expected ')' after parameters")
}
skip_newlines(parser)
result := parse_type(parser)
error_type := types.INVALID
if _, ok := allow(parser, .Bang); ok {
error_type = parse_error_type(parser)
}
if current(parser).kind == .Newline {
skip_newlines(parser)
}
if current(parser).kind != .Left_Brace {
delete(params, parser.module.allocator)
return invalid_expr(parser, start.span, "expected function literal body")
}
body := parse_block(parser)
end := previous(parser)
function_id := ast.function_id(len(parser.module.functions))
append(&parser.module.functions, ast.Function{
span=span_from(start.span, end.span),
name=symbol.INVALID,
pkg=parser.pkg,
file=parser.file,
c_abi=false,
generated=true,
has_body=true,
variadic=variadic,
params=params,
result=result,
error=error_type,
body=body,
diagnostic=source.INVALID_DIAGNOSTIC,
})
return add_expr(parser, ast.Expr{
kind=.Function_Literal,
span=span_from(start.span, end.span),
integer=u64(function_id),
left=ast.INVALID_EXPR,
right=ast.INVALID_EXPR,
diagnostic=source.INVALID_DIAGNOSTIC,
})
}
parse_record_field_type :: proc(parser: ^Parser, allow_anonymous_struct_payload: bool) -> types.Type {
if allow_anonymous_struct_payload && current(parser).kind == .Keyword_Struct {
return parse_inline_struct_payload_type(parser)
+46 -5
View File
@@ -2913,19 +2913,21 @@ milestone_24_rejects_invalid_forms :: proc(t: ^testing.T) {
}
@(test)
field_function_pointer_calls_lower_as_indirect_calls :: proc(t: ^testing.T) {
function_literals_lower_as_function_pointer_values :: proc(t: ^testing.T) {
text := `Callbacks :: struct {
call @func(value i32) i32
value i32
}
plus_one func(value i32) i32 {
return value + 1
}
run func(callbacks Callbacks) i32 {
return callbacks.call(callbacks.value)
}
main func() i32 {
callbacks Callbacks = Callbacks { call = plus_one, value = 41 }
callbacks Callbacks = Callbacks {
call = func(value i32) i32 {
return value + 1
},
value = 41,
}
return run(callbacks) - 42
}
`
@@ -2943,6 +2945,14 @@ main func() i32 {
ir_module := lower.lower(&hir_module)
defer ir.destroy_module(&ir_module)
generated_functions := 0
literal_exprs := 0
for function in ast_module.functions {
generated_functions += 1 if function.generated else 0
}
for expr in ast_module.exprs {
literal_exprs += 1 if expr.kind == .Function_Literal else 0
}
indirect_calls := 0
for expr in hir_module.exprs {
if expr.kind == .Call && expr.left != hir.INVALID_EXPR {
@@ -2950,10 +2960,41 @@ main func() i32 {
}
}
testing.expect_value(t, len(diagnostics.items), 0)
testing.expect_value(t, generated_functions, 1)
testing.expect_value(t, literal_exprs, 1)
testing.expect(t, len(ir_module.functions) > 0)
testing.expect(t, indirect_calls > 0)
}
@(test)
function_literals_do_not_capture_locals :: proc(t: ^testing.T) {
text := `main func() i32 {
offset i32 = 1
callback @func(value i32) i32 = func(value i32) i32 {
return value + offset
}
return callback(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)
found := false
for diagnostic in diagnostics.items {
found = found || strings.contains(diagnostic.message, "unresolved global 'offset'")
}
testing.expect(t, found)
}
@(test)
field_function_pointer_calls_reject_non_callable_fields :: proc(t: ^testing.T) {
text := `Box :: struct {
+4 -8
View File
@@ -8,16 +8,12 @@ Allocator :: struct {
heap Allocator :: Allocator {
context = none,
alloc = heap_alloc,
free = heap_free,
}
heap_alloc func(context ?*mut anyopaque, size usize, alignment usize) ?*mut u8 {
alloc = func(context ?*mut anyopaque, size usize, alignment usize) ?*mut u8 {
return ptr_cast(u8, c.malloc(size))
}
heap_free func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
},
free = func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
c.free(memory)
},
}
alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 {