From f171a6579da39cf1cf555222dfde23763a2a750e Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Tue, 7 Jul 2026 17:04:38 +0200 Subject: [PATCH] function literals (non-capturing) --- compiler/ast/ast.odin | 2 + compiler/checker/checker.odin | 85 ++++++++++++++++++++++++++++++++--- compiler/parser/parser.odin | 52 +++++++++++++++++++++ compiler_tests.odin | 51 ++++++++++++++++++--- std/mem/mem.bro | 16 +++---- 5 files changed, 186 insertions(+), 20 deletions(-) diff --git a/compiler/ast/ast.odin b/compiler/ast/ast.odin index e65166d..204451e 100644 --- a/compiler/ast/ast.odin +++ b/compiler/ast/ast.odin @@ -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, diff --git a/compiler/checker/checker.odin b/compiler/checker/checker.odin index 19648b9..fb40632 100644 --- a/compiler/checker/checker.odin +++ b/compiler/checker/checker.odin @@ -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)) } } diff --git a/compiler/parser/parser.odin b/compiler/parser/parser.odin index 949881e..bafa68b 100644 --- a/compiler/parser/parser.odin +++ b/compiler/parser/parser.odin @@ -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) diff --git a/compiler_tests.odin b/compiler_tests.odin index 2579acd..3bd4701 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -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 { diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 0b3d08c..0d26107 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -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 { - return ptr_cast(u8, c.malloc(size)) -} - -heap_free func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void { - c.free(memory) + alloc = func(context ?*mut anyopaque, size usize, alignment usize) ?*mut u8 { + return ptr_cast(u8, c.malloc(size)) + }, + 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 {