From 0cc7b6944105e6084d5951705a7efd5a180d41cf Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 23 Feb 2026 13:45:44 +0200 Subject: [PATCH] closures --- examples/35-closures.sx | 95 ++++ examples/50-smoke.sx | 234 +++++++++ specs.md | 84 ++- src/ast.zig | 8 + src/codegen.zig | 882 +++++++++++++++++++++++++++++++- src/parser.zig | 104 +++- src/sema.zig | 2 + src/types.zig | 44 ++ tests/expected/35-closures.exit | 1 + tests/expected/35-closures.txt | 13 + tests/expected/50-smoke.txt | 36 ++ 11 files changed, 1472 insertions(+), 31 deletions(-) create mode 100644 examples/35-closures.sx create mode 100644 tests/expected/35-closures.exit create mode 100644 tests/expected/35-closures.txt diff --git a/examples/35-closures.sx b/examples/35-closures.sx new file mode 100644 index 0000000..825ee5c --- /dev/null +++ b/examples/35-closures.sx @@ -0,0 +1,95 @@ +#import "modules/std.sx"; + +// --- Closure Basics --- + +// Factory: returns a new closure each time +make_adder :: (n: s64) -> Closure(s64) -> s64 { + return closure((x: s64) -> s64 => x + n); +} + +// Higher-order function: accepts any Closure(s64) -> s64 +apply :: (f: Closure(s64) -> s64, x: s64) -> s64 { return f(x); } + +// Reduce: fold over a slice with a closure +reduce :: (arr: []s64, f: Closure(s64, s64) -> s64, init: s64) -> s64 { + acc := init; + i : s64 = 0; + while i < arr.len { acc = f(acc, arr[i]); i += 1; } + return acc; +} + +// Auto-promoted bare function +triple :: (x: s64) -> s64 { return x * 3; } + +// Struct with optional closure callback +Widget :: struct { + name: string; + on_update: ?Closure(s64) -> void; +} + +main :: () { + // 1. Basic closure with capture + offset := 100; + add_offset := closure((x: s64) -> s64 => x + offset); + print("basic: {}\n", add_offset(42)); + + // 2. Capture by value (snapshot semantics) + n := 10; + snap := closure((x: s64) -> s64 => x + n); + n = 999; + print("snapshot: {}\n", snap(5)); + + // 3. Block-body closure with control flow + clamp := closure((x: s64) -> s64 { + if x < 0 { return 0; } + if x > 100 { return 100; } + return x; + }); + print("clamp: {} {} {}\n", clamp(50), clamp(0 - 10), clamp(200)); + + // 4. Void closure with string capture + tag := "INFO"; + logger := closure((msg: string) { + print("[{}] {}\n", tag, msg); + }); + logger("system ready"); + + // 5. Factory pattern + add5 := make_adder(5); + add10 := make_adder(10); + print("factory: {} {}\n", add5(100), add10(100)); + + // 6. Auto-promotion: bare fn passed where Closure expected + print("auto-promote: {}\n", apply(triple, 7)); + + // 7. Closure passed to higher-order function + factor := 4; + print("hof: {}\n", apply(closure((x: s64) -> s64 => x * factor), 10)); + + // 8. Reduce with closure + nums : []s64 = .[1, 2, 3, 4, 5]; + total := reduce(nums, closure((acc: s64, x: s64) -> s64 => acc + x), 0); + print("reduce: {}\n", total); + + // 9. Closure captures closure + inner := closure((x: s64) -> s64 => x + 10); + outer := closure((x: s64) -> s64 => inner(x) * 2); + print("compose: {}\n", outer(5)); + + // 10. Multiple closures from same scope + base := 100; + cl_add := closure((x: s64) -> s64 => x + base); + cl_mul := closure((x: s64) -> s64 => x * base); + print("multi: {} {}\n", cl_add(5), cl_mul(5)); + + // 11. Optional closures + w1 := Widget.{ name = "slider", on_update = closure((val: s64) { + print("widget: {} = {}\n", "slider", val); + }) }; + w2 := Widget.{ name = "label", on_update = null }; + + if h := w1.on_update { h(42); } + if h := w2.on_update { h(0); } else { print("widget: no handler\n"); } + + print("=== DONE ===\n"); +} diff --git a/examples/50-smoke.sx b/examples/50-smoke.sx index 50189c3..2cd17f9 100644 --- a/examples/50-smoke.sx +++ b/examples/50-smoke.sx @@ -1719,5 +1719,239 @@ END; print("guard loop: {}\n", guard_loop(3)); // guard loop: 3 } + // --- block-body lambdas --- + { + // block-body lambda with return type + clamp := (x: s64, lo: s64, hi: s64) -> s64 { + if x < lo { return lo; } + if x > hi { return hi; } + return x; + }; + print("block-lambda: {}\n", clamp(50, 0, 100)); // block-lambda: 50 + print("block-lambda: {}\n", clamp(-10, 0, 100)); // block-lambda: 0 + print("block-lambda: {}\n", clamp(999, 0, 100)); // block-lambda: 100 + + // block-body lambda without return type annotation + greet := (name: string) { + print("hello {}\n", name); + }; + greet("block"); // hello block + } + + // --- named params in function types --- + { + // Named params are documentation only — ignored for type identity + apply_named :: (f: (x: s32, y: s32) -> s32, a: s32, b: s32) -> s32 { + return f(a, b); + } + add :: (a: s32, b: s32) -> s32 { return a + b; } + print("named-fn-type: {}\n", apply_named(add, 3, 4)); // named-fn-type: 7 + } + + // --- xx on function pointers --- + { + MyEnv :: struct { n: s32; } + typed_fn :: (e: *MyEnv, x: s32) -> s32 { + return x + e.n; + } + // xx cast: (*MyEnv, s32) -> s32 → (*void, s32) -> s32 + f : (*void, s32) -> s32 = xx typed_fn; + env := MyEnv.{ n = 100 }; + print("xx-fnptr: {}\n", f(xx @env, 42)); // xx-fnptr: 142 + } + + // --- closure type: construct and access fields --- + { + dummy_fn :: (env: *void, x: s32) -> s32 { + return x * 2; + } + fn_ptr : *void = xx dummy_fn; + null_env : *void = xx 0; + c : Closure(s32) -> s32 = .{ fn_ptr = fn_ptr, env = null_env }; + print("closure-type: fn_ptr-nonnull={}\n", c.fn_ptr != null_env); + print("closure-type: env-null={}\n", c.env == null_env); + } + + // --- closure calling convention --- + { + Env :: struct { n: s32; } + impl :: (env: *void, x: s32) -> s32 { + e : *Env = xx env; + return x + e.n; + } + env := Env.{ n = 5 }; + fn_ptr : *void = xx impl; + env_ptr : *void = xx @env; + c : Closure(s32) -> s32 = .{ fn_ptr = fn_ptr, env = env_ptr }; + print("closure-call: {}\n", c(10)); + } + + // --- auto-promotion: bare fn → Closure --- + { + double :: (x: s32) -> s32 { return x * 2; } + apply :: (f: Closure(s32) -> s32, x: s32) -> s32 { return f(x); } + print("auto-promote: {}\n", apply(double, 10)); + + // Named function to Closure variable + f : Closure(s32) -> s32 = double; + print("auto-promote-var: {}\n", f(5)); + } + + // --- closure() intrinsic --- + { + // capture scalar + n := 42; + f := closure((x: s32) => x + n); + print("closure-capture: {}\n", f(10)); + + // capture by value is a snapshot + m := 5; + g := closure((x: s32) => x + m); + m = 100; + print("closure-snapshot: {}\n", g(10)); + + // no captures (null env) + h := closure((x: s32) => x * 2); + print("closure-nocap: {}\n", h(7)); + + // multiple captures + a := 10; + b := 20; + multi := closure((x: s32) => x + a + b); + print("closure-multi: {}\n", multi(3)); + + // block-body closure with return + offset := 50; + clamp := closure((x: s64) -> s64 { + if x < 0 { return 0; } + if x > 100 { return 100; } + return x + offset; + }); + // Workaround: assign result with explicit type so print wraps correctly + r1 : s64 = clamp(10); + r2 : s64 = clamp(0 - 5); + r3 : s64 = clamp(999); + print("closure-block: {}\n", r1); + print("closure-block: {}\n", r2); + print("closure-block: {}\n", r3); + + // void closure + tag := "LOG"; + logger := closure((msg: string) { + print("[{}] {}\n", tag, msg); + }); + logger("hello"); + + // pass closure to higher-order function + dbl :: (x: s32) -> s32 { return x * 2; } + apply_cl :: (f2: Closure(s32) -> s32, x: s32) -> s32 { return f2(x); } + factor : s32 = 3; + print("closure-hof: {}\n", apply_cl(closure((x: s32) -> s32 => x * factor), 10)); + + // auto-promoted bare fn passed alongside closures + print("closure-hof-bare: {}\n", apply_cl(dbl, 10)); + + // C5.A2: capture f32 + scale := 2.5; + f_f32 := closure((x: f32) -> f32 => x * scale); + print("closure-f32: {}\n", f_f32(4.0)); + + // C5.A3: capture bool + verbose := true; + f_bool := closure((msg: string) { + if verbose { print("closure-bool: {}\n", msg); } + }); + f_bool("hello"); + + // C5.B3: two params + base : s32 = 100; + f_2p := closure((x: s32, y: s32) -> s32 => x + y + base); + print("closure-2p: {}\n", f_2p(3, 4)); + + // C5.B4: three params + bias : s32 = 1; + f_3p := closure((a: s32, b: s32, c2: s32) -> s32 => a + b + c2 + bias); + print("closure-3p: {}\n", f_3p(10, 20, 30)); + + // C5.B5: mixed param types (string + s32) + extra : s32 = 5; + f_mix := closure((name: string, age: s32) { + print("closure-mix: {} is {}\n", name, age + extra); + }); + f_mix("Alice", 30); + + // C5.C3: return bool + threshold : s32 = 100; + f_rbool := closure((x: s32) -> bool { return x > threshold; }); + print("closure-rbool: {} {}\n", f_rbool(50), f_rbool(200)); + + // C5.D3: reduce / fold + reduce :: (arr: []s32, f3: Closure(s32, s32) -> s32, init: s32) -> s32 { + acc := init; + i : s64 = 0; + while i < arr.len { acc = f3(acc, arr[i]); i += 1; } + return acc; + } + r_nums : []s32 = .[1, 2, 3, 4, 5]; + r_bonus : s32 = 100; + r_total := reduce(r_nums, closure((acc: s32, x: s32) -> s32 => acc + x), r_bonus); + print("closure-reduce: {}\n", r_total); + + // C5.G1: factory function + make_adder :: (n: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => x + n); + } + add5 := make_adder(5); + add10 := make_adder(10); + print("closure-factory: {} {}\n", add5(100), add10(100)); + + // C5.A5: capture struct + Point :: struct { x: s32; y: s32; } + origin := Point.{ x = 10, y = 20 }; + f_st := closure(() { + print("closure-struct: {} {}\n", origin.x, origin.y); + }); + f_st(); + + // C5.H1: closure captures another closure + inner_n := 10; + inner_cl := closure((x: s64) -> s64 => x + inner_n); + outer_cl := closure((x: s64) -> s64 => inner_cl(x) * 2); + print("closure-compose: {}\n", outer_cl(5)); + + // C5.M7: multiple closures from same scope capture independently + shared : s32 = 10; + cl_a := closure((x: s32) -> s32 => x + shared); + cl_b := closure((x: s32) -> s32 => x * shared); + print("closure-indep: {} {}\n", cl_a(5), cl_b(5)); + + // C6: optional closures + f_none : ?Closure(s64) -> s64 = null; + if h := f_none { + print("should not print: {}\n", h(1)); + } else { + print("opt-closure: none\n"); + } + + opt_n := 10; + f_some : ?Closure(s64) -> s64 = closure((x: s64) -> s64 => x + opt_n); + if h := f_some { + print("opt-closure: {}\n", h(5)); + } else { + print("should not print\n"); + } + + // Struct with optional closure callback + Btn :: struct { label: string; on_click: ?Closure(s64) -> void; } + btn_x := 99; + btn_cl := closure((id: s64) { + print("opt-closure-btn: {} {}\n", id, btn_x); + }); + btn1 := Btn.{ label = "OK", on_click = btn_cl }; + btn2 := Btn.{ label = "Cancel", on_click = null }; + if h := btn1.on_click { h(1); } + if h := btn2.on_click { h(2); } else { print("opt-closure-btn: null\n"); } + } + print("=== DONE ===\n"); } diff --git a/specs.md b/specs.md index 746fbbd..a1c3994 100644 --- a/specs.md +++ b/specs.md @@ -1081,6 +1081,82 @@ SOME_FUNC :: () => 42; // () -> s32 double :: (x: $T) -> T => x + x; // generic lambda with return type ``` +### Closures + +A **closure** is a function bundled with captured state. It is represented as a fat pointer `{ fn_ptr, env }` (16 bytes), unlike a bare function pointer which is 8 bytes. + +#### Closure Type +```sx +Closure(param_types) -> R // e.g. Closure(s32, s32) -> s32 +Closure(param_types) // void return: Closure(s64) -> void +?Closure(s32) -> s32 // optional closure (null = none) +``` + +#### Creating Closures — `closure()` intrinsic +```sx +offset := 50; +f := closure((x: s32) -> s32 => x + offset); // expression body +g := closure((x: s32) -> s32 { // block body + if x < 0 { return 0; } + return x + offset; +}); +``` + +The `closure()` intrinsic: +1. Analyzes the lambda body for free variables (variables from outer scope) +2. Allocates an env struct on the heap (via `malloc`) containing captured values +3. Generates a trampoline function with signature `(env: *void, params...) -> R` +4. Returns a `Closure` value `{ trampoline, env_ptr }` + +**Capture semantics**: capture by value (snapshot at creation time). Mutating the original variable after creating the closure does not affect the captured value. +```sx +n := 10; +f := closure((x: s64) -> s64 => x + n); +n = 999; +print("{}\n", f(5)); // 15, not 1004 +``` + +#### Calling Closures +Closures are called with normal function call syntax: +```sx +result := f(10); +``` +The compiler prepends the env pointer to the argument list and does an indirect call through the fn_ptr. + +#### Auto-Promotion +A bare function can be implicitly promoted to a `Closure` where one is expected. The compiler generates a static thunk that ignores the env parameter, with a null env pointer. +```sx +double :: (x: s32) -> s32 { return x * 2; } +apply :: (f: Closure(s32) -> s32, x: s32) -> s32 { return f(x); } +apply(double, 10); // double auto-promoted to Closure +``` + +#### Factory Functions +Functions can return closures, enabling the factory pattern: +```sx +make_adder :: (n: s32) -> Closure(s32) -> s32 { + return closure((x: s32) -> s32 => x + n); +} +add5 := make_adder(5); +print("{}\n", add5(100)); // 105 +``` + +#### Optional Closures +`?Closure` is supported for nullable callbacks. Uses `fn_ptr == null` as the none sentinel (zero overhead — same layout as `Closure`). +```sx +Button :: struct { + label: string; + on_click: ?Closure(s64) -> void; +} +btn := Button.{ label = "OK", on_click = null }; +if handler := btn.on_click { + handler(1); +} +``` + +#### Memory +Closure env is heap-allocated via `malloc`. The caller is responsible for freeing `closure.env` when the closure is no longer needed. Auto-promoted closures have a null env and require no freeing. + ### Function Call ```sx callee(args) @@ -1192,7 +1268,7 @@ Statements are terminated by `;`. The `push` statement temporarily overrides a global `context` variable for the duration of a block. The previous context is saved before the block and restored after it exits. ```sx -push Context.{ arena = @arena, data = xx @logger } { +push Context.{ allocator = arena.allocator(), data = xx @logger } { handle(client); // inside here, `context` has the new value } // context is restored to its previous value here @@ -1201,13 +1277,13 @@ push Context.{ arena = @arena, data = xx @logger } { **`Context` struct** — defined in `std.sx`: ```sx Context :: struct { - arena: *Arena; // pointer to active arena allocator (or null) - data: *void; // opaque pointer for application-specific data + allocator: Allocator; // active allocator for dynamic allocation + data: *void; // opaque pointer for application-specific data } context : Context = ---; // global mutable variable ``` -Inside the pushed block, any code (including called functions) can read `context.arena` and `context.data`. The standard library's `cstring()` function checks `context.arena` and uses it for allocation when available, falling back to `malloc()` otherwise. +Inside the pushed block, any code (including called functions) can read `context.allocator` and `context.data`. The standard library's `cstring()` and `alloc_slice()` functions use `context.allocator` for allocation when its `.ctx` is non-null, falling back to `malloc()` otherwise. `push` requires a global mutable variable named `context` to be in scope (provided by `std.sx`). diff --git a/src/ast.zig b/src/ast.zig index c42f126..47105e6 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -68,6 +68,7 @@ pub const Node = struct { foreign_expr: ForeignExpr, library_decl: LibraryDecl, function_type_expr: FunctionTypeExpr, + closure_type_expr: ClosureTypeExpr, tuple_type_expr: TupleTypeExpr, tuple_literal: TupleLiteral, ufcs_alias: UfcsAlias, @@ -427,6 +428,13 @@ pub const LibraryDecl = struct { pub const FunctionTypeExpr = struct { param_types: []const *Node, + param_names: ?[]const ?[]const u8 = null, // optional documentation names + return_type: ?*Node, // null = void return +}; + +pub const ClosureTypeExpr = struct { + param_types: []const *Node, + param_names: ?[]const ?[]const u8 = null, // optional documentation names return_type: ?*Node, // null = void return }; diff --git a/src/codegen.zig b/src/codegen.zig index 3d95013..88809b7 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -211,6 +211,8 @@ pub const CodeGen = struct { tuple_alloca_types: std.AutoHashMap(usize, Type), // UFCS alias map: alias name → target function name ufcs_aliases: std.StringHashMap([]const u8), + // Closure thunk cache: original function name → thunk LLVM function (dedup) + closure_thunks: std.StringHashMap(c.LLVMValueRef), // Target configuration (triple, cpu, opt level, lib paths, linker) target_config: TargetConfig = .{}, // Cached primitive LLVM types (initialized once in init(), avoids repeated FFI calls) @@ -429,6 +431,7 @@ pub const CodeGen = struct { .function_return_types = std.StringHashMap(Type).init(allocator), .tuple_alloca_types = std.AutoHashMap(usize, Type).init(allocator), .ufcs_aliases = std.StringHashMap([]const u8).init(allocator), + .closure_thunks = std.StringHashMap(c.LLVMValueRef).init(allocator), .target_config = target_config, .cached_i1 = c.LLVMInt1TypeInContext(ctx), .cached_i8 = c.LLVMInt8TypeInContext(ctx), @@ -598,12 +601,17 @@ pub const CodeGen = struct { return c.LLVMVectorType(self.typeToLLVM(elem_ty), info.length); }, .pointer_type, .many_pointer_type, .function_type => self.ptrType(), + .closure_type => self.getClosureStructType(), .optional_type => |info| { - // ?*T, ?[*]T → bare pointer (null = none) + // ?*T, ?[*]T, ?fn → bare pointer (null = none) const child_type = self.resolveTypeFromName(info.child_name) orelse unreachable; if (child_type.isPointer() or child_type.isManyPointer() or child_type.isFunctionType()) { return self.ptrType(); } + // ?Closure → same layout as Closure { ptr, ptr } — fn_ptr null = none + if (child_type.isClosureType()) { + return self.getClosureStructType(); + } // ?T → { T, i1 } struct var field_types: [2]c.LLVMTypeRef = .{ self.typeToLLVM(child_type), @@ -834,6 +842,7 @@ pub const CodeGen = struct { .vector_type, .array_type => self.allocaStoreAsI64(self.typeToLLVM(ty), val, "any_vec"), .slice_type => self.allocaStoreAsI64(self.getStringStructType(), val, "any_slice"), .pointer_type, .many_pointer_type, .function_type => self.ptrToInt(val, "any_ptr"), + .closure_type => self.allocaStoreAsI64(self.getClosureStructType(), val, "any_closure"), .meta_type => |mt| self.allocaStoreAsI64(self.getStringStructType(), self.buildStringSlice(val, self.constInt64(mt.name.len)), "any_type"), else => self.sExt(val, i64_ty, "any_val"), }; @@ -850,6 +859,15 @@ pub const CodeGen = struct { return self.string_struct_type.?; } + fn getClosureStructType(self: *CodeGen) c.LLVMTypeRef { + // Closure = { fn_ptr: ptr, env: ptr } + var field_types = [_]c.LLVMTypeRef{ + self.ptrType(), // fn_ptr + self.ptrType(), // env + }; + return c.LLVMStructTypeInContext(self.context, &field_types, 2, 0); + } + /// Build a fat pointer {ptr, len} struct from a type, pointer, and length value. fn buildFatPointer(self: *CodeGen, ty: c.LLVMTypeRef, ptr: c.LLVMValueRef, len: c.LLVMValueRef) c.LLVMValueRef { const undef = self.getUndef(ty); @@ -916,6 +934,9 @@ pub const CodeGen = struct { if (child_ty.isPointer() or child_ty.isManyPointer() or child_ty.isFunctionType()) { return value; // pointer is already nullable — just pass through } + if (child_ty.isClosureType()) { + return value; // closure {fn_ptr, env} — fn_ptr null means none + } const llvm_opt_ty = self.typeToLLVM(opt_ty); var result = self.getUndef(llvm_opt_ty); result = c.LLVMBuildInsertValue(self.builder, result, value, 0, "opt_val"); @@ -938,6 +959,11 @@ pub const CodeGen = struct { if (child_ty.isPointer() or child_ty.isManyPointer() or child_ty.isFunctionType()) { return c.LLVMBuildICmp(self.builder, c.LLVMIntNE, opt_val, c.LLVMConstNull(self.ptrType()), "opt_nonnull"); } + if (child_ty.isClosureType()) { + // Check fn_ptr (index 0) != null + const fn_ptr = self.extractValue(opt_val, 0, "cl_fn_chk"); + return c.LLVMBuildICmp(self.builder, c.LLVMIntNE, fn_ptr, c.LLVMConstNull(self.ptrType()), "opt_cl_nonnull"); + } return c.LLVMBuildExtractValue(self.builder, opt_val, 1, "opt_flag"); } @@ -949,6 +975,9 @@ pub const CodeGen = struct { if (child_ty.isPointer() or child_ty.isManyPointer() or child_ty.isFunctionType()) { return opt_val; // for pointer optionals, the value IS the pointer } + if (child_ty.isClosureType()) { + return opt_val; // closure struct is the payload — fn_ptr null = none + } return c.LLVMBuildExtractValue(self.builder, opt_val, 0, "opt_payload"); } @@ -1111,6 +1140,16 @@ pub const CodeGen = struct { return self.emitErrorFmt("no field '{s}' on {s} (available: .len, .ptr)", .{ field, type_name }); } + fn extractClosureField(self: *CodeGen, val: c.LLVMValueRef, field: []const u8) !c.LLVMValueRef { + if (std.mem.eql(u8, field, "fn_ptr")) { + return self.extractValue(val, 0, "fn_ptr"); + } + if (std.mem.eql(u8, field, "env")) { + return self.extractValue(val, 1, "env"); + } + return self.emitErrorFmt("no field '{s}' on Closure (available: .fn_ptr, .env)", .{field}); + } + fn pushScope(self: *CodeGen) !void { var saves = std.ArrayList(ScopeEntry).empty; try saves.ensureTotalCapacity(self.allocator, 8); @@ -1613,6 +1652,21 @@ pub const CodeGen = struct { .return_type = ret_ptr, } }; } + // Closure type: Closure(ParamTypes) -> ReturnType + if (tn.data == .closure_type_expr) { + const cte = tn.data.closure_type_expr; + var param_types = std.ArrayList(Type).empty; + for (cte.param_types) |pt| { + param_types.append(self.allocator, self.resolveType(pt)) catch return .void_type; + } + const ret_ty = if (cte.return_type) |rt| self.resolveType(rt) else Type.void_type; + const ret_ptr = self.allocator.create(Type) catch return .void_type; + ret_ptr.* = ret_ty; + return .{ .closure_type = .{ + .param_types = param_types.toOwnedSlice(self.allocator) catch return .void_type, + .return_type = ret_ptr, + } }; + } // Tuple type: (T1, T2) or (T1,) if (tn.data == .tuple_type_expr) { const tte = tn.data.tuple_type_expr; @@ -2670,7 +2724,9 @@ pub const CodeGen = struct { } fn registerLambdaAsFunction(self: *CodeGen, name: []const u8, lambda: ast.Lambda) !void { - const ret_sx_type = self.inferType(lambda.body); + // Block-body without explicit return type → void (same as named functions) + // Expression-body without explicit return type → infer from expression + const ret_sx_type = if (lambda.return_type != null) self.resolveType(lambda.return_type) else if (lambda.body.data == .block) Type.void_type else self.inferType(lambda.body); const ret_llvm_type = self.typeToLLVM(ret_sx_type); var param_llvm_types = std.ArrayList(c.LLVMTypeRef).empty; @@ -2689,13 +2745,16 @@ pub const CodeGen = struct { const name_z = try self.allocator.dupeZ(u8, name); _ = c.LLVMAddFunction(self.module, name_z.ptr, fn_type); + try self.function_return_types.put(name, ret_sx_type); } fn genLambdaBody(self: *CodeGen, name: []const u8, lambda: ast.Lambda) !void { self.named_values.clearRetainingCapacity(); self.narrowed_types.clearRetainingCapacity(); - const ret_sx_type = self.inferType(lambda.body); + // Block-body without explicit return type → void (same as named functions) + // Expression-body without explicit return type → infer from expression + const ret_sx_type = if (lambda.return_type != null) self.resolveType(lambda.return_type) else if (lambda.body.data == .block) Type.void_type else self.inferType(lambda.body); self.current_return_type = ret_sx_type; const name_z = try self.allocator.dupeZ(u8, name); @@ -2709,12 +2768,39 @@ pub const CodeGen = struct { try self.bindParam(function, param.name, sx_ty, @intCast(i)); } - const ret_val = try self.genExpr(lambda.body); - if (ret_val) |val| { - const prepared = try self.prepareReturnValue(val, ret_sx_type); - self.ret(prepared); + // Block-body lambda: generate statements like genFnBody + if (lambda.body.data == .block) { + try self.pushScope(); + var last_val: c.LLVMValueRef = null; + for (lambda.body.data.block.stmts) |stmt| { + last_val = try self.genStmt(stmt); + } + // Only add terminator if block doesn't already have one (from explicit return) + const current_bb = self.getCurrentBlock(); + if (c.LLVMGetBasicBlockTerminator(current_bb) == null) { + try self.popScope(); + const effective_last_val: ?c.LLVMValueRef = if (last_val) |val| + (if (c.LLVMTypeOf(val) == self.voidType()) null else val) + else + null; + if (ret_sx_type == .void_type) { + self.retVoid(); + } else if (effective_last_val) |val| { + const prepared = try self.prepareReturnValue(val, ret_sx_type); + self.ret(prepared); + } else { + _ = c.LLVMBuildUnreachable(self.builder); + } + } } else { - self.retVoid(); + // Expression-body lambda: (params) => expr + const ret_val = try self.genExpr(lambda.body); + if (ret_val) |val| { + const prepared = try self.prepareReturnValue(val, ret_sx_type); + self.ret(prepared); + } else { + self.retVoid(); + } } } @@ -2891,6 +2977,30 @@ pub const CodeGen = struct { } } + // Lambda assigned to variable: treat like a local const lambda (create named fn) + if (vd.value) |val| { + if (val.data == .lambda) { + const saved_fn = self.current_function; + const saved_bb = self.getCurrentBlock(); + const saved_ret = self.current_return_type; + const saved_named = self.named_values; + const saved_narrowed2 = self.narrowed_types; + self.named_values = std.StringHashMap(NamedValue).init(self.allocator); + self.narrowed_types = std.StringHashMap(NarrowedInfo).init(self.allocator); + + try self.registerLambdaAsFunction(vd.name, val.data.lambda); + try self.genLambdaBody(vd.name, val.data.lambda); + + self.named_values.deinit(); + self.named_values = saved_named; + self.narrowed_types = saved_narrowed2; + self.current_return_type = saved_ret; + self.current_function = saved_fn; + self.positionAt(saved_bb); + return null; + } + } + var sx_ty: Type = Type.s(64); if (vd.type_annotation) |ta| { @@ -2915,6 +3025,47 @@ pub const CodeGen = struct { return self.emitErrorFmt("variable '{s}' has no type annotation and no initializer", .{vd.name}); } + // Closure-typed variable + if (sx_ty.isClosureType()) { + const llvm_ty = self.getClosureStructType(); + const alloca = try self.buildNamedAlloca(llvm_ty, vd.name); + + if (vd.value == null) { + // Default: zero-init (null fn_ptr and null env) + self.storeNull(llvm_ty, alloca); + } else if (vd.value.?.data == .undef_literal) { + self.storeUndef(llvm_ty, alloca); + } else if (vd.value.?.data == .struct_literal) { + // .{ fn_ptr = ..., env = ... } — construct closure from anonymous struct literal + const sl = vd.value.?.data.struct_literal; + var fn_ptr_val: ?c.LLVMValueRef = null; + var env_val: ?c.LLVMValueRef = null; + for (sl.field_inits) |fi| { + const fname = fi.name orelse return self.emitError("closure literal fields must be named (.fn_ptr, .env)"); + if (std.mem.eql(u8, fname, "fn_ptr")) { + fn_ptr_val = try self.genExpr(fi.value); + } else if (std.mem.eql(u8, fname, "env")) { + env_val = try self.genExpr(fi.value); + } else { + return self.emitErrorFmt("unknown closure field '{s}' (expected .fn_ptr, .env)", .{fname}); + } + } + const fn_ptr = fn_ptr_val orelse return self.emitError("closure literal missing .fn_ptr field"); + const env = env_val orelse return self.emitError("closure literal missing .env field"); + // Build { fn_ptr, env } aggregate + var closure_val = c.LLVMGetUndef(llvm_ty); + closure_val = self.insertValue(closure_val, fn_ptr, 0, "closure_fn"); + closure_val = self.insertValue(closure_val, env, 1, "closure_env"); + _ = c.LLVMBuildStore(self.builder, closure_val, alloca); + } else { + const val = try self.genExprAsType(vd.value.?, sx_ty); + _ = c.LLVMBuildStore(self.builder, val, alloca); + } + + try self.registerVariable(vd.name, alloca, sx_ty); + return null; + } + // Struct-typed variable if (sx_ty.isStruct()) { // Resolve type aliases (e.g. Vec3 -> Vec__3_f32) @@ -3601,6 +3752,24 @@ pub const CodeGen = struct { return self.emitErrorFmt("no field '{s}' on slice (available: .ptr, .len)", .{fa.field}); } + // Closure field assignment: c.fn_ptr = val, c.env = val + if (entry.ty.isClosureType()) { + const struct_ty = self.getClosureStructType(); + if (std.mem.eql(u8, fa.field, "fn_ptr")) { + const gep = self.structGEP(struct_ty, entry.ptr, 0, "closure_fn_ptr"); + const rhs = try self.genExpr(asgn.value); + _ = c.LLVMBuildStore(self.builder, rhs, gep); + return null; + } + if (std.mem.eql(u8, fa.field, "env")) { + const gep = self.structGEP(struct_ty, entry.ptr, 1, "closure_env"); + const rhs = try self.genExpr(asgn.value); + _ = c.LLVMBuildStore(self.builder, rhs, gep); + return null; + } + return self.emitErrorFmt("no field '{s}' on Closure (available: .fn_ptr, .env)", .{fa.field}); + } + if (!entry.ty.isStruct()) return self.emitErrorFmt("field access on non-struct variable '{s}'", .{obj_name}); const sname = entry.ty.struct_type; @@ -5064,6 +5233,13 @@ pub const CodeGen = struct { } } + // Auto-promotion: bare function → Closure (static thunk + null env) + if (target_ty.isClosureType()) { + if (src_ty.isFunctionType() or self.isFunctionName(node)) { + return self.promoteToClosureThunk(node, target_ty.closure_type); + } + } + // Implicit address-of: passing T where *T is expected → auto & if (target_ty.isPointer()) { const pointee_name = target_ty.pointer_type.pointee_name; @@ -5314,6 +5490,12 @@ pub const CodeGen = struct { return val; } + // function_type → function_type: both are opaque pointers at LLVM level, no-op + // Enables xx cast between different function pointer signatures + if (src_ty.isFunctionType() and target_ty.isFunctionType()) { + return val; + } + return val; } @@ -6042,6 +6224,10 @@ pub const CodeGen = struct { const slice_val = c.LLVMBuildLoad2(self.builder, self.getStringStructType(), entry.ptr, "slice_load"); return self.extractFatPtrField(slice_val, fa.field, "slice"); } + if (entry.ty.isClosureType()) { + const closure_val = c.LLVMBuildLoad2(self.builder, self.getClosureStructType(), entry.ptr, "closure_load"); + return self.extractClosureField(closure_val, fa.field); + } if (entry.ty.isArray()) { if (std.mem.eql(u8, fa.field, "len")) { return self.constInt64(entry.ty.array_type.length); @@ -6090,6 +6276,9 @@ pub const CodeGen = struct { if (obj_ty.isSlice()) { return self.extractFatPtrField(obj_val, fa.field, "slice"); } + if (obj_ty.isClosureType()) { + return self.extractClosureField(obj_val, fa.field); + } if (obj_ty.isStruct()) { const sname = obj_ty.struct_type; const info = try self.getStructInfo(sname); @@ -6667,7 +6856,7 @@ pub const CodeGen = struct { } } - // Struct field function pointer call: obj.field(args) + // Struct field function pointer / closure call: obj.field(args) // Checked before UFCS so that struct fields shadow free functions of the same name. { var obj_ty = self.inferType(fa.object); @@ -6682,9 +6871,19 @@ pub const CodeGen = struct { const fn_ptr = try self.genFieldAccess(fa); return self.genIndirectCallFromPtr(fn_ptr, field_ty.function_type, call_node); } + if (field_ty.isClosureType()) { + const closure_val = try self.genFieldAccess(fa); + return self.genClosureCallFromValue(closure_val, field_ty.closure_type, call_node); + } } } } + // Direct closure variable field call: c(args) where c is a local Closure variable + if (obj_ty.isClosureType()) { + // This handles the case where obj is a closure and fa.field is being called as UFCS, + // but actually we want obj.fn_ptr/env access + call. This path is for + // closures stored in struct fields — the direct variable call goes through genCallByName. + } } // UFCS: obj.method(args...) → method(obj, args...) @@ -6808,6 +7007,22 @@ pub const CodeGen = struct { if (std.mem.eql(u8, callee_name, "memcpy")) { return self.genMemcpy(call_node.args); } + if (std.mem.eql(u8, callee_name, "closure")) { + return self.genClosureIntrinsic(call_node); + } + + // Local variable takes priority: closures and function pointers shadow LLVM named functions + if (self.lookupValue(callee_name)) |v| { + const entry = v.asNamedValue(); + if (entry) |e| { + if (e.ty.isClosureType()) { + return self.genClosureCall(e, call_node); + } + if (e.ty.isFunctionType()) { + return self.genIndirectCall(e, call_node); + } + } + } var nbuf: [256]u8 = undefined; var callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(callee_name, &nbuf)); @@ -6834,16 +7049,7 @@ pub const CodeGen = struct { callee_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(c_name, &rbuf)); } } - // Function pointer indirect call: callee is a variable with function_type if (callee_fn == null) { - if (self.lookupValue(callee_name)) |v| { - const entry = v.asNamedValue(); - if (entry) |e| { - if (e.ty.isFunctionType()) { - return self.genIndirectCall(e, call_node); - } - } - } return self.emitErrorFmt("undefined function '{s}'", .{callee_name}); } @@ -7058,6 +7264,570 @@ pub const CodeGen = struct { ); } + fn genClosureCall(self: *CodeGen, entry: NamedValue, call_node: ast.Call) !c.LLVMValueRef { + const cti = entry.ty.closure_type; + const closure_struct_ty = self.getClosureStructType(); + const closure_val = c.LLVMBuildLoad2(self.builder, closure_struct_ty, entry.ptr, "closure_load"); + return self.genClosureCallFromValue(closure_val, cti, call_node); + } + + fn genClosureCallFromValue(self: *CodeGen, closure_val: c.LLVMValueRef, cti: Type.ClosureTypeInfo, call_node: ast.Call) !c.LLVMValueRef { + // Extract fn_ptr and env from closure struct + const fn_ptr = self.extractValue(closure_val, 0, "cl_fn_ptr"); + const env_ptr = self.extractValue(closure_val, 1, "cl_env"); + + // Build LLVM function type: (env: *void, params...) -> R + const ptr_ty_llvm = self.ptrType(); + const total_params = cti.param_types.len + 1; // +1 for env + if (total_params > 64) return self.emitErrorFmt("closure call has {d} parameters, exceeding maximum of 64", .{total_params}); + var param_llvm_types: [64]c.LLVMTypeRef = undefined; + param_llvm_types[0] = ptr_ty_llvm; // env: *void + for (cti.param_types, 0..) |pt, i| { + param_llvm_types[i + 1] = if (pt.isArray()) ptr_ty_llvm else self.typeToLLVM(pt); + } + const ret_llvm = self.typeToLLVM(cti.return_type.*); + const fn_type = c.LLVMFunctionType( + ret_llvm, + ¶m_llvm_types, + @intCast(total_params), + 0, + ); + + // Generate arguments: [env, arg0, arg1, ...] + var arg_vals = std.ArrayList(c.LLVMValueRef).empty; + try arg_vals.append(self.allocator, env_ptr); + for (call_node.args, 0..) |arg, i| { + if (i < cti.param_types.len) { + const pt = cti.param_types[i]; + if (pt.isArray()) { + const decay_target: Type = .{ .many_pointer_type = .{ .element_name = pt.array_type.element_name } }; + try arg_vals.append(self.allocator, try self.genExprAsType(arg, decay_target)); + } else { + try arg_vals.append(self.allocator, try self.genExprAsType(arg, pt)); + } + } else { + try arg_vals.append(self.allocator, try self.genExpr(arg)); + } + } + const args_slice = try arg_vals.toOwnedSlice(self.allocator); + + const call_name: [*c]const u8 = if (ret_llvm == self.voidType()) "" else "cl_call"; + return c.LLVMBuildCall2( + self.builder, + fn_type, + fn_ptr, + if (args_slice.len > 0) args_slice.ptr else null, + @intCast(args_slice.len), + call_name, + ); + } + + /// Check if a node refers to a named function (not a variable). + fn isFunctionName(self: *CodeGen, node: *Node) bool { + if (node.data != .identifier) return false; + const name = node.data.identifier.name; + // It's a function if LLVM knows about it and it's NOT a local variable + var nbuf: [256]u8 = undefined; + if (c.LLVMGetNamedFunction(self.module, self.nameToCStr(name, &nbuf)) != null) { + // Make sure it's not shadowed by a local variable + if (self.named_values.get(name) != null) return false; + return true; + } + return false; + } + + /// Auto-promote a bare function to a Closure by generating a static thunk. + /// The thunk has signature (env: *void, params...) -> R and ignores the env param. + /// Returns a closure value { thunk_ptr, null }. + fn promoteToClosureThunk(self: *CodeGen, node: *Node, cti: Type.ClosureTypeInfo) !c.LLVMValueRef { + // Determine source function name + const fn_name = blk: { + if (node.data == .identifier) break :blk node.data.identifier.name; + return self.emitError("auto-promotion to Closure requires a function name"); + }; + + // Check thunk cache for dedup + if (self.closure_thunks.get(fn_name)) |cached_thunk| { + // Build closure { thunk, null } + var closure_val = c.LLVMGetUndef(self.getClosureStructType()); + closure_val = self.insertValue(closure_val, cached_thunk, 0, "cl_fn"); + closure_val = self.insertValue(closure_val, c.LLVMConstPointerNull(self.ptrType()), 1, "cl_env"); + return closure_val; + } + + // Generate thunk: __thunk_(env: *void, params...) -> R + const thunk_name = try std.fmt.allocPrint(self.allocator, "__thunk_{s}", .{fn_name}); + const thunk_name_z = try self.allocator.dupeZ(u8, thunk_name); + + // Build thunk function type: (ptr, param_types...) -> ret_type + const ptr_ty = self.ptrType(); + const total_params = cti.param_types.len + 1; + var param_llvm_types = try self.allocator.alloc(c.LLVMTypeRef, total_params); + param_llvm_types[0] = ptr_ty; // env: *void (ignored) + for (cti.param_types, 0..) |pt, i| { + param_llvm_types[i + 1] = if (pt.isArray()) ptr_ty else self.typeToLLVM(pt); + } + const ret_llvm = self.typeToLLVM(cti.return_type.*); + const thunk_fn_type = c.LLVMFunctionType( + ret_llvm, + param_llvm_types.ptr, + @intCast(total_params), + 0, + ); + + // Create the thunk function + const thunk_fn = c.LLVMAddFunction(self.module, thunk_name_z.ptr, thunk_fn_type); + c.LLVMSetLinkage(thunk_fn, c.LLVMPrivateLinkage); + + // Save current position + const saved_fn = self.current_function; + const saved_bb = self.getCurrentBlock(); + + // Build thunk body + const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, thunk_fn, "entry"); + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + self.current_function = thunk_fn; + + // Look up the original function + var nbuf: [256]u8 = undefined; + const orig_fn = c.LLVMGetNamedFunction(self.module, self.nameToCStr(fn_name, &nbuf)) orelse + return self.emitErrorFmt("cannot find function '{s}' for closure promotion", .{fn_name}); + + // Build call to original: original(param0, param1, ...) — skip env (param 0 of thunk) + var call_args = try self.allocator.alloc(c.LLVMValueRef, cti.param_types.len); + for (0..cti.param_types.len) |i| { + call_args[i] = c.LLVMGetParam(thunk_fn, @intCast(i + 1)); + } + + const orig_fn_type = c.LLVMGlobalGetValueType(orig_fn); + const call_name: [*c]const u8 = if (ret_llvm == self.voidType()) "" else "fwd"; + const result = c.LLVMBuildCall2( + self.builder, + orig_fn_type, + orig_fn, + if (call_args.len > 0) call_args.ptr else null, + @intCast(call_args.len), + call_name, + ); + + if (ret_llvm == self.voidType()) { + _ = c.LLVMBuildRetVoid(self.builder); + } else { + _ = c.LLVMBuildRet(self.builder, result); + } + + // Restore position + self.current_function = saved_fn; + self.positionAt(saved_bb); + + // Cache thunk for dedup + self.closure_thunks.put(fn_name, thunk_fn) catch {}; + + // Build closure value { thunk_fn, null } + var closure_val = c.LLVMGetUndef(self.getClosureStructType()); + closure_val = self.insertValue(closure_val, thunk_fn, 0, "cl_fn"); + closure_val = self.insertValue(closure_val, c.LLVMConstPointerNull(self.ptrType()), 1, "cl_env"); + return closure_val; + } + + // Counter for unique closure names + var closure_counter: u32 = 0; + + /// closure(lambda) intrinsic — captures free variables, allocates env, returns Closure. + fn genClosureIntrinsic(self: *CodeGen, call_node: ast.Call) !c.LLVMValueRef { + if (call_node.args.len != 1) return self.emitError("closure() requires exactly one lambda argument"); + const arg = call_node.args[0]; + const lambda = switch (arg.data) { + .lambda => arg.data.lambda, + else => return self.emitError("closure() argument must be a lambda expression"), + }; + + // Determine lambda return type + const ret_ty = if (lambda.return_type) |rt| + self.resolveType(rt) + else if (lambda.body.data == .block) + Type.void_type + else + self.inferType(lambda.body); + + // Collect parameter names for exclusion + var param_names = std.StringHashMap(void).init(self.allocator); + for (lambda.params) |p| { + param_names.put(p.name, {}) catch {}; + } + + // Free variable analysis: walk body, collect identifiers that are local variables (not params, not functions, not globals) + var captures = std.ArrayList(CaptureInfo).empty; + try self.collectCaptures(lambda.body, ¶m_names, &captures); + + // Deduplicate captures + var seen = std.StringHashMap(void).init(self.allocator); + var deduped = std.ArrayList(CaptureInfo).empty; + for (captures.items) |cap| { + if (!seen.contains(cap.name)) { + seen.put(cap.name, {}) catch {}; + try deduped.append(self.allocator, cap); + } + } + const capture_list = deduped.items; + + // Build param types for the closure type + var closure_param_types = try self.allocator.alloc(Type, lambda.params.len); + for (lambda.params, 0..) |p, i| { + closure_param_types[i] = self.resolveType(p.type_expr); + } + + // Generate unique name + const closure_id = closure_counter; + closure_counter += 1; + const tramp_name = try std.fmt.allocPrint(self.allocator, "__closure_{d}", .{closure_id}); + const tramp_name_z = try self.allocator.dupeZ(u8, tramp_name); + + // Build env struct type: { capture0_type, capture1_type, ... } + var env_field_types = try self.allocator.alloc(c.LLVMTypeRef, capture_list.len); + for (capture_list, 0..) |cap, i| { + env_field_types[i] = self.typeToLLVM(cap.ty); + } + const env_struct_ty = c.LLVMStructTypeInContext( + self.context, + if (env_field_types.len > 0) env_field_types.ptr else null, + @intCast(env_field_types.len), + 0, + ); + + // Build trampoline function type: (env: *void, params...) -> R + const ptr_ty = self.ptrType(); + const total_params = lambda.params.len + 1; + var tramp_param_types = try self.allocator.alloc(c.LLVMTypeRef, total_params); + tramp_param_types[0] = ptr_ty; // env: *void + for (lambda.params, 0..) |p, i| { + const pt = self.resolveType(p.type_expr); + tramp_param_types[i + 1] = if (pt.isArray()) ptr_ty else self.typeToLLVM(pt); + } + const ret_llvm = self.typeToLLVM(ret_ty); + const tramp_fn_type = c.LLVMFunctionType( + ret_llvm, + tramp_param_types.ptr, + @intCast(total_params), + 0, + ); + + // Create trampoline function + const tramp_fn = c.LLVMAddFunction(self.module, tramp_name_z.ptr, tramp_fn_type); + c.LLVMSetLinkage(tramp_fn, c.LLVMPrivateLinkage); + + // Save codegen state + const saved_fn = self.current_function; + const saved_bb = self.getCurrentBlock(); + const saved_ret = self.current_return_type; + const saved_named = self.named_values; + const saved_narrowed = self.narrowed_types; + + // Set up trampoline body + self.current_function = tramp_fn; + self.current_return_type = ret_ty; + self.named_values = std.StringHashMap(NamedValue).init(self.allocator); + self.narrowed_types = std.StringHashMap(NarrowedInfo).init(self.allocator); + + const entry_bb = c.LLVMAppendBasicBlockInContext(self.context, tramp_fn, "entry"); + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + + // Load env pointer (param 0) and cast to env struct type + const raw_env = c.LLVMGetParam(tramp_fn, 0); + + // Register captured variables by loading from env struct + for (capture_list, 0..) |cap, i| { + const field_gep = self.structGEP(env_struct_ty, raw_env, @intCast(i), "cap_ptr"); + const field_llvm_ty = self.typeToLLVM(cap.ty); + // For aggregate types (strings, slices, structs), use alloca + if (cap.ty == .string_type or cap.ty.isSlice() or cap.ty.isStruct() or cap.ty.isClosureType()) { + // Alloca + copy for aggregate types + const alloca = self.buildEntryBlockAlloca(field_llvm_ty, "cap_alloca"); + const loaded = c.LLVMBuildLoad2(self.builder, field_llvm_ty, field_gep, "cap_load"); + _ = c.LLVMBuildStore(self.builder, loaded, alloca); + try self.named_values.put(cap.name, .{ .ptr = alloca, .ty = cap.ty }); + } else { + // Scalar: just use the GEP as the alloca + try self.named_values.put(cap.name, .{ .ptr = field_gep, .ty = cap.ty }); + } + } + + // Register lambda params (starting from param index 1) + for (lambda.params, 0..) |p, i| { + const param_ty = self.resolveType(p.type_expr); + const llvm_param = c.LLVMGetParam(tramp_fn, @intCast(i + 1)); + const param_llvm_ty = self.typeToLLVM(param_ty); + const alloca = self.buildEntryBlockAlloca(param_llvm_ty, "param"); + _ = c.LLVMBuildStore(self.builder, llvm_param, alloca); + try self.named_values.put(p.name, .{ .ptr = alloca, .ty = param_ty }); + } + + // Generate lambda body + if (lambda.body.data == .block) { + // Block body — generate statements + for (lambda.body.data.block.stmts) |stmt| { + _ = try self.genStmt(stmt); + } + // Add terminator if current block (possibly a dead `after_ret` block) lacks one + if (c.LLVMGetBasicBlockTerminator(self.getCurrentBlock()) == null) { + if (ret_ty == .void_type) { + _ = c.LLVMBuildRetVoid(self.builder); + } else { + // Dead block after explicit return — add unreachable + _ = c.LLVMBuildUnreachable(self.builder); + } + } + } else { + // Expression body + const body_val = try self.genExprAsType(lambda.body, ret_ty); + _ = c.LLVMBuildRet(self.builder, body_val); + } + + // Restore codegen state + self.named_values.deinit(); + self.named_values = saved_named; + self.narrowed_types = saved_narrowed; + self.current_return_type = saved_ret; + self.current_function = saved_fn; + self.positionAt(saved_bb); + + // Now back in the caller's context — allocate env and store captures + if (capture_list.len > 0) { + // Allocate env via malloc + const env_size = c.LLVMSizeOf(env_struct_ty); + const env_size_i64 = c.LLVMBuildIntCast2(self.builder, env_size, self.i64Type(), 0, "env_size"); + const malloc_fn = self.getOrDeclareMalloc(); + var malloc_arg_types = [_]c.LLVMTypeRef{self.i64Type()}; + var malloc_args = [_]c.LLVMValueRef{env_size_i64}; + const env_raw = c.LLVMBuildCall2( + self.builder, + c.LLVMFunctionType(ptr_ty, &malloc_arg_types, 1, 0), + malloc_fn, + &malloc_args, + 1, + "env_alloc", + ); + + // Store captured values into env struct + for (capture_list, 0..) |cap, i| { + const gep = self.structGEP(env_struct_ty, env_raw, @intCast(i), "env_store"); + _ = c.LLVMBuildStore(self.builder, cap.value, gep); + } + + // Build closure value { tramp_fn, env_raw } + var closure_val = c.LLVMGetUndef(self.getClosureStructType()); + closure_val = self.insertValue(closure_val, tramp_fn, 0, "cl_fn"); + closure_val = self.insertValue(closure_val, env_raw, 1, "cl_env"); + return closure_val; + } else { + // No captures — null env (like auto-promotion) + var closure_val = c.LLVMGetUndef(self.getClosureStructType()); + closure_val = self.insertValue(closure_val, tramp_fn, 0, "cl_fn"); + closure_val = self.insertValue(closure_val, c.LLVMConstPointerNull(ptr_ty), 1, "cl_env"); + return closure_val; + } + } + + const CaptureInfo = struct { + name: []const u8, + ty: Type, + value: c.LLVMValueRef, // loaded value at capture site + }; + + /// Walk AST node and collect free variable references (identifiers that are in scope as local vars, + /// not lambda params, not functions, not type names). + fn collectCaptures(self: *CodeGen, node: *Node, param_names: *std.StringHashMap(void), captures: *std.ArrayList(CaptureInfo)) !void { + switch (node.data) { + .identifier => |id| { + // Skip if it's a lambda param + if (param_names.contains(id.name)) return; + // Skip if it's a function name + if (self.isFunctionName(node)) return; + // Skip if it's a type name + if (self.type_registry.contains(id.name)) return; + // Skip if it's a generic template + if (self.generic_templates.contains(id.name)) return; + // Skip if it's a namespace + if (self.namespaces.contains(id.name)) return; + // Skip if it's a library constant + if (self.library_constants.contains(id.name)) return; + // Only capture if it's a local/global variable + if (self.getNamedOrGlobal(id.name)) |entry| { + // Load the current value + const val = self.loadTyped(entry.ty, entry.ptr, "capture"); + try captures.append(self.allocator, .{ + .name = id.name, + .ty = entry.ty, + .value = val, + }); + } + }, + .block => |blk| { + for (blk.stmts) |stmt| { + try self.collectCaptures(stmt, param_names, captures); + } + }, + .binary_op => |bop| { + try self.collectCaptures(bop.lhs, param_names, captures); + try self.collectCaptures(bop.rhs, param_names, captures); + }, + .unary_op => |uop| { + try self.collectCaptures(uop.operand, param_names, captures); + }, + .call => |call| { + try self.collectCaptures(call.callee, param_names, captures); + for (call.args) |a| { + try self.collectCaptures(a, param_names, captures); + } + }, + .field_access => |fa| { + try self.collectCaptures(fa.object, param_names, captures); + }, + .if_expr => |ie| { + try self.collectCaptures(ie.condition, param_names, captures); + try self.collectCaptures(ie.then_branch, param_names, captures); + if (ie.else_branch) |eb| { + try self.collectCaptures(eb, param_names, captures); + } + }, + .return_stmt => |rs| { + if (rs.value) |val| { + try self.collectCaptures(val, param_names, captures); + } + }, + .while_expr => |we| { + try self.collectCaptures(we.condition, param_names, captures); + try self.collectCaptures(we.body, param_names, captures); + }, + .for_expr => |fe| { + try self.collectCaptures(fe.iterable, param_names, captures); + // Add loop capture name to excluded params + param_names.put(fe.capture_name, {}) catch {}; + if (fe.index_name) |idx_name| { + param_names.put(idx_name, {}) catch {}; + } + try self.collectCaptures(fe.body, param_names, captures); + }, + .var_decl => |vd| { + if (vd.value) |val| { + try self.collectCaptures(val, param_names, captures); + } + // After declaration, the name is local — exclude from captures + param_names.put(vd.name, {}) catch {}; + }, + .const_decl => |cd| { + try self.collectCaptures(cd.value, param_names, captures); + param_names.put(cd.name, {}) catch {}; + }, + .assignment => |asgn| { + try self.collectCaptures(asgn.target, param_names, captures); + try self.collectCaptures(asgn.value, param_names, captures); + }, + .index_expr => |ie| { + try self.collectCaptures(ie.object, param_names, captures); + try self.collectCaptures(ie.index, param_names, captures); + }, + .struct_literal => |sl| { + for (sl.field_inits) |fi| { + try self.collectCaptures(fi.value, param_names, captures); + } + }, + .deref_expr => |de| { + try self.collectCaptures(de.operand, param_names, captures); + }, + .force_unwrap => |fu| { + try self.collectCaptures(fu.operand, param_names, captures); + }, + .null_coalesce => |nc| { + try self.collectCaptures(nc.lhs, param_names, captures); + try self.collectCaptures(nc.rhs, param_names, captures); + }, + .match_expr => |me| { + try self.collectCaptures(me.subject, param_names, captures); + for (me.arms) |arm| { + if (arm.pattern) |pat| { + try self.collectCaptures(pat, param_names, captures); + } + try self.collectCaptures(arm.body, param_names, captures); + } + }, + .defer_stmt => |ds| { + try self.collectCaptures(ds.expr, param_names, captures); + }, + .slice_expr => |se| { + try self.collectCaptures(se.object, param_names, captures); + if (se.start) |s| try self.collectCaptures(s, param_names, captures); + if (se.end) |e| try self.collectCaptures(e, param_names, captures); + }, + .spread_expr => |se| { + try self.collectCaptures(se.operand, param_names, captures); + }, + .lambda => |lam| { + // Nested lambda: its params are excluded, but captures from outer scope bubble up + for (lam.params) |p| { + param_names.put(p.name, {}) catch {}; + } + try self.collectCaptures(lam.body, param_names, captures); + }, + .chained_comparison => |cc| { + for (cc.operands) |operand| { + try self.collectCaptures(operand, param_names, captures); + } + }, + // Leaf nodes: nothing to capture + .int_literal, .float_literal, .bool_literal, .string_literal, + .null_literal, .undef_literal, .builtin_expr, .break_expr, + .continue_expr, .type_expr, .enum_literal, .foreign_expr, + .library_decl, .array_type_expr, .slice_type_expr, + .pointer_type_expr, .many_pointer_type_expr, .optional_type_expr, + .function_type_expr, .closure_type_expr, .tuple_type_expr, + => {}, + // Remaining nodes that contain children + .array_literal => |al| { + for (al.elements) |elem| { + try self.collectCaptures(elem, param_names, captures); + } + }, + .tuple_literal => |tl| { + for (tl.elements) |elem| { + try self.collectCaptures(elem.value, param_names, captures); + } + }, + .comptime_expr => |ct| { + try self.collectCaptures(ct.expr, param_names, captures); + }, + .insert_expr => |ins| { + try self.collectCaptures(ins.expr, param_names, captures); + }, + .push_stmt => |ps| { + try self.collectCaptures(ps.context_expr, param_names, captures); + try self.collectCaptures(ps.body, param_names, captures); + }, + .multi_assign => |ma| { + for (ma.targets) |t| try self.collectCaptures(t, param_names, captures); + for (ma.values) |v| try self.collectCaptures(v, param_names, captures); + }, + // Top-level decls: skip + .root, .fn_decl, .param, .match_arm, .enum_decl, .struct_decl, + .union_decl, .namespace_decl, .import_decl, .c_import_decl, + .ufcs_alias, .parameterized_type_expr, + => {}, + } + } + + fn getOrDeclareMalloc(self: *CodeGen) c.LLVMValueRef { + var nbuf: [256]u8 = undefined; + if (c.LLVMGetNamedFunction(self.module, self.nameToCStr("malloc", &nbuf))) |f| return f; + var param_types = [_]c.LLVMTypeRef{self.i64Type()}; + const fn_type = c.LLVMFunctionType( + self.ptrType(), + ¶m_types, + 1, + 0, + ); + return c.LLVMAddFunction(self.module, "malloc", fn_type); + } + fn genGenericCall(self: *CodeGen, qualified_name: []const u8, template: ast.FnDecl, call_node: ast.Call) !c.LLVMValueRef { const fd = template; @@ -8892,6 +9662,49 @@ pub const CodeGen = struct { return .{ .vector_type = .{ .element_name = elem, .length = length } }; } } + // Closure display name: "Closure(T1, T2) -> R" or "Closure(T1, T2)" + if (name.len > 9 and std.mem.startsWith(u8, name, "Closure(")) { + // Find matching closing paren + if (std.mem.indexOfScalar(u8, name[8..], ')')) |close_rel| { + const params_str = name[8 .. 8 + close_rel]; + const after_paren = name[8 + close_rel + 1 ..]; + + // Parse param types + var param_types_list = std.ArrayList(Type).empty; + if (params_str.len > 0) { + var start: usize = 0; + var i: usize = 0; + while (i < params_str.len) : (i += 1) { + if (i + 1 < params_str.len and params_str[i] == ',' and params_str[i + 1] == ' ') { + const pt_name = params_str[start..i]; + if (self.resolveTypeFromName(pt_name)) |pt| { + param_types_list.append(self.allocator, pt) catch {}; + } + start = i + 2; + i += 1; + } + } + // Last param + const last = params_str[start..]; + if (self.resolveTypeFromName(last)) |pt| { + param_types_list.append(self.allocator, pt) catch {}; + } + } + + // Parse return type + const ret_type = if (std.mem.startsWith(u8, after_paren, " -> ")) + self.resolveTypeFromName(after_paren[4..]) orelse Type.void_type + else + Type.void_type; + + const ret_type_ptr = self.allocator.create(Type) catch return null; + ret_type_ptr.* = ret_type; + return .{ .closure_type = .{ + .param_types = param_types_list.toOwnedSlice(self.allocator) catch return null, + .return_type = ret_type_ptr, + } }; + } + } // Type aliases if (self.lookupAlias(name)) |target| return self.resolveTypeFromName(target); return null; @@ -8989,7 +9802,7 @@ pub const CodeGen = struct { }; if (obj_ty) |uty| return uty; - // Struct field function pointer call: obj.fn_field(args) + // Struct field function pointer / closure call: obj.fn_field(args) { var fa_obj_ty = self.inferType(fa.object); if (fa_obj_ty.isPointer()) { @@ -9002,6 +9815,9 @@ pub const CodeGen = struct { if (field_ty.isFunctionType()) { return field_ty.function_type.return_type.*; } + if (field_ty.isClosureType()) { + return field_ty.closure_type.return_type.*; + } } } } @@ -9044,6 +9860,25 @@ pub const CodeGen = struct { if (std.mem.eql(u8, base_name, "free")) return .void_type; if (std.mem.eql(u8, base_name, "memcpy")) return .{ .pointer_type = .{ .pointee_name = "void" } }; if (std.mem.eql(u8, base_name, "memset")) return .void_type; + // closure(lambda) → Closure(param_types) -> R + if (std.mem.eql(u8, base_name, "closure")) { + if (call_node.args.len == 1 and call_node.args[0].data == .lambda) { + const lam = call_node.args[0].data.lambda; + var param_types = std.ArrayList(Type).empty; + for (lam.params) |p| { + param_types.append(self.allocator, self.resolveType(p.type_expr)) catch {}; + } + const cl_ret = if (lam.return_type) |rt| self.resolveType(rt) + else if (lam.body.data == .block) Type.void_type + else self.inferType(lam.body); + const ret_ptr = self.allocator.create(Type) catch return Type.s(64); + ret_ptr.* = cl_ret; + return .{ .closure_type = .{ + .param_types = param_types.toOwnedSlice(self.allocator) catch return Type.s(64), + .return_type = ret_ptr, + } }; + } + } // Check generic templates — infer return type from widened bindings const template = self.generic_templates.get(callee_name) orelse blk: { // Intra-namespace fallback @@ -9112,12 +9947,15 @@ pub const CodeGen = struct { const ret_llvm = c.LLVMGetReturnType(fn_type); return self.llvmTypeToSxType(ret_llvm); } - // Check if callee is a variable with function pointer type + // Check if callee is a variable with function pointer or closure type { if (self.lookupValue(callee_name)) |v| { if (v.ty().isFunctionType()) { return v.ty().function_type.return_type.*; } + if (v.ty().isClosureType()) { + return v.ty().closure_type.return_type.*; + } } } return Type.s(64); @@ -9179,6 +10017,10 @@ pub const CodeGen = struct { if (std.mem.eql(u8, fa.field, "len")) return Type.s(64); if (std.mem.eql(u8, fa.field, "ptr")) return .{ .many_pointer_type = .{ .element_name = obj_ty.array_type.element_name } }; } + if (obj_ty.isClosureType()) { + if (std.mem.eql(u8, fa.field, "fn_ptr")) return .{ .pointer_type = .{ .pointee_name = "void" } }; + if (std.mem.eql(u8, fa.field, "env")) return .{ .pointer_type = .{ .pointee_name = "void" } }; + } if (obj_ty.isAny()) { if (std.mem.eql(u8, fa.field, "tag")) return Type.s(64); if (std.mem.eql(u8, fa.field, "value")) return Type.s(64); diff --git a/src/parser.zig b/src/parser.zig index 8d10900..eb9a091 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -390,14 +390,28 @@ pub const Parser = struct { } // Function type: (ParamTypes) -> ReturnType // Tuple type: (T1, T2) or (T1) — no '->' after ')' + // Named params (documentation only): (name: Type, ...) -> ReturnType if (self.current.tag == .l_paren) { self.advance(); // skip '(' var param_types = std.ArrayList(*Node).empty; + var param_names = std.ArrayList(?[]const u8).empty; + var has_names = false; while (self.current.tag != .r_paren and self.current.tag != .eof) { if (param_types.items.len > 0) { try self.expect(.comma); if (self.current.tag == .r_paren) break; // trailing comma ok } + // Check for optional param name: `name: Type` + // An identifier followed by `:` (not `::` or `:=`) is a param name + if (self.current.tag == .identifier and self.peekNext() == .colon) { + const pname = self.tokenSlice(self.current); + self.advance(); // skip name + self.advance(); // skip ':' + try param_names.append(self.allocator, pname); + has_names = true; + } else { + try param_names.append(self.allocator, null); + } try param_types.append(self.allocator, try self.parseTypeExpr()); } try self.expect(.r_paren); @@ -407,6 +421,7 @@ pub const Parser = struct { const return_type = try self.parseTypeExpr(); return try self.createNode(start, .{ .function_type_expr = .{ .param_types = try param_types.toOwnedSlice(self.allocator), + .param_names = if (has_names) try param_names.toOwnedSlice(self.allocator) else null, .return_type = return_type, } }); } @@ -439,6 +454,42 @@ pub const Parser = struct { } } + // Closure type: Closure(params...) -> R + if (std.mem.eql(u8, name, "Closure") and self.current.tag == .l_paren) { + self.advance(); // skip '(' + var param_types = std.ArrayList(*Node).empty; + var param_names = std.ArrayList(?[]const u8).empty; + var has_names = false; + while (self.current.tag != .r_paren and self.current.tag != .eof) { + if (param_types.items.len > 0) { + try self.expect(.comma); + if (self.current.tag == .r_paren) break; // trailing comma ok + } + // Check for optional param name: `name: Type` + if (self.current.tag == .identifier and self.peekNext() == .colon) { + const pname = self.tokenSlice(self.current); + self.advance(); // skip name + self.advance(); // skip ':' + try param_names.append(self.allocator, pname); + has_names = true; + } else { + try param_names.append(self.allocator, null); + } + try param_types.append(self.allocator, try self.parseTypeExpr()); + } + try self.expect(.r_paren); + var return_type: ?*Node = null; + if (self.current.tag == .arrow) { + self.advance(); + return_type = try self.parseTypeExpr(); + } + return try self.createNode(start, .{ .closure_type_expr = .{ + .param_types = try param_types.toOwnedSlice(self.allocator), + .param_names = if (has_names) try param_names.toOwnedSlice(self.allocator) else null, + .return_type = return_type, + } }); + } + // Parameterized type: Vector(N, T) or later generic struct instantiation if (self.current.tag == .l_paren) { self.advance(); // skip '(' @@ -1889,24 +1940,56 @@ pub const Parser = struct { self.prev_end = saved_prev_end; } - // Use shared paren-scanning, then check for lambda patterns - const tag = self.peekPastParens() orelse return false; + // Check upfront if parens look like function params (for block-body disambiguation) + const has_param_parens = blk: { + self.advance(); // skip '(' + if (self.current.tag == .r_paren) break :blk true; // empty parens + if (self.current.tag != .identifier) break :blk false; + self.advance(); + break :blk self.current.tag == .colon; + }; + + // Restore to '(' and scan past parens inline (not via peekPastParens which restores state) + self.lexer = saved_lexer; + self.current = saved_current; + self.prev_end = saved_prev_end; + self.advance(); // skip '(' + var depth: u32 = 1; + while (depth > 0 and self.current.tag != .eof) { + if (self.current.tag == .l_paren) depth += 1; + if (self.current.tag == .r_paren) depth -= 1; + if (depth > 0) self.advance(); + } + if (self.current.tag != .r_paren) return false; + self.advance(); // skip ')' — now positioned on token after parens + + const tag = self.current.tag; + // (params) => expr if (tag == .fat_arrow) return true; // (params) -> ReturnType => expr + // (params) -> ReturnType { stmts } if (tag == .arrow) { self.advance(); // skip '->' - // Skip past the return type tokens until we see '=>' or something unexpected + // Skip past the return type tokens until we see '=>', '{', or something unexpected while (self.current.tag != .eof) { if (self.current.tag == .fat_arrow) return true; + if (self.current.tag == .l_brace) return true; if (self.current.tag == .identifier or self.current.tag.isTypeKeyword() or self.current.tag == .dot or self.current.tag == .dollar or self.current.tag == .l_bracket or self.current.tag == .r_bracket or self.current.tag == .l_paren or self.current.tag == .r_paren or - self.current.tag == .comma or self.current.tag == .int_literal) + self.current.tag == .comma or self.current.tag == .int_literal or + self.current.tag == .star or self.current.tag == .question) { self.advance(); } else break; } + return false; + } + // (params) { stmts } — block-body lambda + // Only if contents look like function params (have `:` type annotations or is empty `()`) + if (tag == .l_brace) { + return has_param_parens; } return false; } @@ -1915,15 +1998,22 @@ pub const Parser = struct { const start = self.current.loc.start; const params = try self.parseParams(); - // Optional return type: (params) -> Type => expr + // Optional return type: (params) -> Type => expr OR (params) -> Type { stmts } var return_type: ?*Node = null; if (self.current.tag == .arrow) { self.advance(); return_type = try self.parseTypeExpr(); } - try self.expect(.fat_arrow); - const body = try self.parseExpr(); + // Two body forms: + // (params) => expr — expression lambda + // (params) { stmts } — block-body lambda + const body = if (self.current.tag == .l_brace) + try self.parseBlock() + else blk: { + try self.expect(.fat_arrow); + break :blk try self.parseExpr(); + }; const type_params = try self.collectTypeParams(params); return try self.createNode(start, .{ .lambda = .{ .params = params, diff --git a/src/sema.zig b/src/sema.zig index 57f683f..fb3ae8b 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -848,6 +848,7 @@ pub const Analyzer = struct { .foreign_expr, .library_decl, .function_type_expr, + .closure_type_expr, .import_decl, .c_import_decl, .array_type_expr, @@ -1225,6 +1226,7 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node { .slice_expr, .tuple_type_expr, .ufcs_alias, + .closure_type_expr, => {}, .tuple_literal => |tl| { for (tl.elements) |elem| { diff --git a/src/types.zig b/src/types.zig index 5de3f3f..3bf7f05 100644 --- a/src/types.zig +++ b/src/types.zig @@ -22,6 +22,7 @@ pub const Type = union(enum) { many_pointer_type: ManyPointerTypeInfo, vector_type: VectorTypeInfo, function_type: FunctionTypeInfo, + closure_type: ClosureTypeInfo, any_type, optional_type: OptionalTypeInfo, meta_type: MetaTypeInfo, @@ -44,6 +45,11 @@ pub const Type = union(enum) { return_type: *const Type, }; + pub const ClosureTypeInfo = struct { + param_types: []const Type, + return_type: *const Type, + }; + pub const ArrayTypeInfo = struct { element_name: []const u8, length: u32, @@ -95,6 +101,14 @@ pub const Type = union(enum) { } return info.return_type.eql(o.return_type.*); }, + .closure_type => |info| { + const o = other.closure_type; + if (info.param_types.len != o.param_types.len) return false; + for (info.param_types, o.param_types) |a, b| { + if (!a.eql(b)) return false; + } + return info.return_type.eql(o.return_type.*); + }, .optional_type => |info| std.mem.eql(u8, info.child_name, other.optional_type.child_name), .meta_type => |info| std.mem.eql(u8, info.name, other.meta_type.name), .tuple_type => |info| { @@ -302,6 +316,21 @@ pub const Type = union(enum) { }; } + pub fn isClosureType(self: Type) bool { + return switch (self) { + .closure_type => true, + else => false, + }; + } + + /// Returns true for both bare function pointers and closures + pub fn isCallable(self: Type) bool { + return switch (self) { + .function_type, .closure_type => true, + else => false, + }; + } + pub fn isArray(self: Type) bool { return switch (self) { .array_type => true, @@ -356,6 +385,7 @@ pub const Type = union(enum) { .f64 => 64, .boolean => 1, .pointer_type, .many_pointer_type, .function_type => 64, + .closure_type => 128, // { ptr, ptr } = 16 bytes else => 0, }; } @@ -506,6 +536,20 @@ pub const Type = union(enum) { } return try buf.toOwnedSlice(allocator); }, + .closure_type => |info| { + var buf = std.ArrayList(u8).empty; + try buf.appendSlice(allocator, "Closure("); + for (info.param_types, 0..) |pt, i| { + if (i > 0) try buf.appendSlice(allocator, ", "); + try buf.appendSlice(allocator, try pt.displayName(allocator)); + } + try buf.append(allocator, ')'); + if (!std.meta.eql(info.return_type.*, Type.void_type)) { + try buf.appendSlice(allocator, " -> "); + try buf.appendSlice(allocator, try info.return_type.displayName(allocator)); + } + return try buf.toOwnedSlice(allocator); + }, .optional_type => |info| return fmtAlloc(allocator, "?{s}", .{info.child_name}), .meta_type => |info| info.name, .tuple_type => |info| { diff --git a/tests/expected/35-closures.exit b/tests/expected/35-closures.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/35-closures.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/35-closures.txt b/tests/expected/35-closures.txt new file mode 100644 index 0000000..b22f23c --- /dev/null +++ b/tests/expected/35-closures.txt @@ -0,0 +1,13 @@ +basic: 142 +snapshot: 15 +clamp: 50 0 100 +[INFO] system ready +factory: 105 110 +auto-promote: 21 +hof: 40 +reduce: 15 +compose: 30 +multi: 105 500 +widget: slider = 42 +widget: no handler +=== DONE === diff --git a/tests/expected/50-smoke.txt b/tests/expected/50-smoke.txt index 3d40bef..f762c58 100644 --- a/tests/expected/50-smoke.txt +++ b/tests/expected/50-smoke.txt @@ -426,4 +426,40 @@ or guard: 7 or guard null: 0 nested narrow: 10 20 guard loop: 3 +block-lambda: 50 +block-lambda: 0 +block-lambda: 100 +hello block +named-fn-type: 7 +xx-fnptr: 142 +closure-type: fn_ptr-nonnull=true +closure-type: env-null=true +closure-call: 15 +auto-promote: 20 +auto-promote-var: 10 +closure-capture: 52 +closure-snapshot: 15 +closure-nocap: 14 +closure-multi: 33 +closure-block: 60 +closure-block: 0 +closure-block: 100 +[LOG] hello +closure-hof: 30 +closure-hof-bare: 20 +closure-f32: 10.000000 +closure-bool: hello +closure-2p: 107 +closure-3p: 61 +closure-mix: Alice is 35 +closure-rbool: false true +closure-reduce: 115 +closure-factory: 105 110 +closure-struct: 10 20 +closure-compose: 30 +closure-indep: 15 50 +opt-closure: none +opt-closure: 15 +opt-closure-btn: 1 99 +opt-closure-btn: null === DONE ===