024 source scope capture

✓ Passing This code compiles and runs correctly.

Code

// ============================================================================
// VERIFIED REGRESSION TEST - DO NOT MODIFY WITHOUT DISCUSSION
// ============================================================================
// Test: Source Scope Capture - THE BREAKTHROUGH
// Feature: Source parameters capture continuation bindings
// Verifies: Continuation variables appear in source.scope.bindings
// ============================================================================
// This test proves that Source parameters capture lexical scope from
// continuation pipelines, enabling true template metaprogramming where
// comptime code can inspect runtime variable bindings!
// ============================================================================

~import "$std/io"

// Event that provides user data
~event getUserData { }
| data { name: []const u8, age: i32 }

~proc getUserData {
    return .{ .data = .{ .name = "Alice", .age = 42 } };
}

const std = @import("std");

// Event that takes Source[HTML] parameter for template rendering
// [comptime|transform] annotation marks this as compile-time transformation
// Transform events receive: source (user args), invocation (for type resolution), program (AST), allocator (memory)
~[comptime|transform]event renderHTML { source: Source[HTML], invocation: *const Invocation, program: *const Program, allocator: std.mem.Allocator }
| transformed { program: *const Program }
| failed { error: []const u8 }

~proc renderHTML {
    // THE BREAKTHROUGH: Access captured bindings at comptime!
    // source.scope.bindings should contain the 'u' binding from the continuation

    const ast = @import("ast");
    const ast_functional = @import("ast_functional");
    // NOTE: allocator is now passed as a parameter!

    // First, prove we can see the bindings!
    if (source.scope.bindings.len == 0) {
        return .{ .failed = .{ .error = "ERROR: No bindings captured!" } };
    }

    // THE BREAKTHROUGH: We have access to the captured binding!
    const binding = source.scope.bindings[0];

    // Parse the HTML template to find $[...] interpolations and build format string
    // THE KEY INSIGHT: Use ast_functional.resolveBindingType()!
    // This walks the AST to find the event+branch that produced this binding,
    // respecting transform execution order (upstream transforms have already run).
    const resolved = ast_functional.resolveBindingType(allocator, binding, invocation, program) catch unreachable;

    const type_event_name: []const u8 = resolved.event_name;
    const type_branch_name: []const u8 = resolved.branch_name;
    const type_fields: []const ast.Field = resolved.fields;
    const type_module: []const u8 = resolved.module;

    // Parse the HTML template to find $[...] interpolations and build format string
    const template = source.text;
    var format_buf: [1024]u8 = undefined;
    var format_len: usize = 0;
    var interpolations: [16][]const u8 = undefined;
    var interp_count: usize = 0;

    var i: usize = 0;
    while (i < template.len) {
        if (i + 1 < template.len and template[i] == '$' and template[i + 1] == '[') {
            // Found interpolation start
            const start = i + 2;
            var end = start;
            while (end < template.len and template[end] != ']') : (end += 1) {}

            if (end < template.len) {
                // Extract the expression (e.g., "u.name")
                const expr = template[start..end];
                interpolations[interp_count] = expr;
                interp_count += 1;

                // PLACEHOLDER: We'll determine format specifier after we know the field types
                // For now, just add a marker we'll replace later
                format_buf[format_len] = '{';
                format_buf[format_len + 1] = '?';
                format_buf[format_len + 2] = '}';
                format_len += 3;

                i = end + 1;
                continue;
            }
        }

        // Regular character - copy to format string
        // Normalize whitespace: convert newlines/tabs to spaces, collapse multiple spaces
        const c = template[i];
        if (c == '\n' or c == '\r' or c == '\t' or c == ' ') {
            // Only add space if we have content and last char wasn't already a space
            if (format_len > 0 and format_buf[format_len - 1] != ' ') {
                format_buf[format_len] = ' ';
                format_len += 1;
            }
        } else {
            format_buf[format_len] = c;
            format_len += 1;
        }
        i += 1;
    }

    // Trim trailing whitespace
    while (format_len > 0 and format_buf[format_len - 1] == ' ') {
        format_len -= 1;
    }

    // MOVED: Format string finalization will happen AFTER we know the field types

    // Build the interpolation expression arguments
    var args_buf: [512]u8 = undefined;
    var args_len: usize = 0;
    for (0..interp_count) |idx| {
        if (idx > 0) {
            args_buf[args_len] = ',';
            args_buf[args_len + 1] = ' ';
            args_len += 2;
        }
        const interp = interpolations[idx];
        std.mem.copyForwards(u8, args_buf[args_len..], interp);
        args_len += interp.len;
    }
    const args_str = allocator.dupe(u8, args_buf[0..args_len]) catch unreachable;

    // NOTE: proc_body will be built AFTER we know format_str (after fixing format specifiers)

    // Build DottedPath for renderHTML
    const path_segments = allocator.alloc([]const u8, 1) catch unreachable;
    path_segments[0] = allocator.dupe(u8, "renderHTML") catch unreachable;

    const renderHTML_path = ast.DottedPath{
        .module_qualifier = allocator.dupe(u8, type_module) catch unreachable,
        .segments = path_segments,
    };

    // Create runtime EventDecl with captured binding as parameter
    // We'll reference the derived type name (calculated later from event+branch)
    const runtime_fields = allocator.alloc(ast.Field, 1) catch unreachable;
    runtime_fields[0] = ast.Field{
        .name = allocator.dupe(u8, binding.name) catch unreachable,
        .type = allocator.dupe(u8, "__PLACEHOLDER__") catch unreachable,  // Will be replaced below
        .module_path = null,
        .phantom = null,
        .is_source = false,
        .is_file = false,
        .is_embed_file = false,
        .expression = null,
        .expression_str = null,
        .owns_expression = false,
    };
    var runtime_event = ast.EventDecl{
        .path = renderHTML_path,
        .input = ast.Shape{ .fields = runtime_fields },
        .branches = &[_]ast.Branch{
            ast.Branch{
                .name = "rendered",
                .payload = ast.Shape{ .fields = &[_]ast.Field{
                    ast.Field{
                        .name = "html",
                        .type = "[]const u8",
                        .module_path = null,
                        .phantom = null,
                        .is_source = false,
                        .is_file = false,
                        .is_embed_file = false,
                        .expression = null,
                        .expression_str = null,
                        .owns_expression = false,
                    },
                } },
                .is_deferred = false,
                .is_optional = false,
            },
        },
        .is_public = false,
        .is_implicit_flow = false,
        .annotations = &[_][]const u8{},
        .is_pure = false,
        .is_transitively_pure = false,
        .location = source.location,
        .module = allocator.dupe(u8, type_module) catch unreachable,
    };

    // Now that we have type_fields, fix the format specifiers based on ACTUAL field types
    // Replace {?} placeholders with correct format specifiers
    var format_idx: usize = 0;
    var interp_idx: usize = 0;
    while (format_idx < format_len) {
        if (format_idx + 2 < format_len and
            format_buf[format_idx] == '{' and
            format_buf[format_idx + 1] == '?' and
            format_buf[format_idx + 2] == '}') {
            // Found a placeholder! Get the field name from the interpolation
            const expr = interpolations[interp_idx];
            // Expression is like "u.name" - extract field name after the dot
            const field_name = if (std.mem.indexOf(u8, expr, ".")) |dot_idx|
                expr[dot_idx + 1..]
            else
                expr;

            // Look up the field type in type_fields
            var format_spec: u8 = 'a';  // Default to {any}
            for (type_fields) |field| {
                if (std.mem.eql(u8, field.name, field_name)) {
                    // Determine format spec from field type
                    if (std.mem.eql(u8, field.type, "[]const u8")) {
                        format_spec = 's';  // string
                    } else if (std.mem.eql(u8, field.type, "i32") or
                               std.mem.eql(u8, field.type, "i64") or
                               std.mem.eql(u8, field.type, "u32") or
                               std.mem.eql(u8, field.type, "u64")) {
                        format_spec = 'd';  // integer
                    }
                    break;
                }
            }

            // Replace the ? with the actual format specifier
            format_buf[format_idx + 1] = format_spec;
            interp_idx += 1;
        }
        format_idx += 1;
    }

    const format_str = allocator.dupe(u8, format_buf[0..format_len]) catch unreachable;

    // Now we can build proc_body using the finalized format_str!
    // Note: std is already imported at module level, so we don't redeclare it here
    const proc_body = std.fmt.allocPrint(allocator,
        \\const allocator = std.heap.page_allocator;
        \\const html = std.fmt.allocPrint(allocator,
        \\    "{s}",
        \\    .{{{s}}}) catch unreachable;
        \\return .{{ .rendered = .{{ .html = html }} }};
    , .{format_str, args_str}) catch unreachable;

    // Create runtime ProcDecl that implements renderHTML
    var runtime_proc = ast.ProcDecl{
        .path = renderHTML_path,
        .body = proc_body,
        .inline_flows = &[_]ast.Flow{},
        .annotations = &[_][]const u8{},
        .target = null,
        .is_impl = true,
        .is_pure = false,
        .is_transitively_pure = false,
        .location = source.location,
        .module = allocator.dupe(u8, "__PLACEHOLDER__") catch unreachable,  // Will be replaced with type_module
    };

    // Derive type name from event and branch: __GetUserDataData
    // Capitalize event name first letter
    var event_capitalized: [256]u8 = undefined;
    var event_len: usize = 0;
    for (type_event_name, 0..) |ch, idx| {
        if (idx == 0) {
            event_capitalized[event_len] = std.ascii.toUpper(ch);
        } else {
            event_capitalized[event_len] = ch;
        }
        event_len += 1;
    }

    // Capitalize branch name first letter
    var branch_capitalized: [256]u8 = undefined;
    var branch_len: usize = 0;
    for (type_branch_name, 0..) |ch, idx| {
        if (idx == 0) {
            branch_capitalized[branch_len] = std.ascii.toUpper(ch);
        } else {
            branch_capitalized[branch_len] = ch;
        }
        branch_len += 1;
    }

    // Combine: __EventNameBranchName (compiler-generated type)
    const type_alias_name = std.fmt.allocPrint(allocator,
        "__{s}{s}",
        .{event_capitalized[0..event_len], branch_capitalized[0..branch_len]}
    ) catch unreachable;

    // Now update the runtime field type and module with derived/captured values
    allocator.free(runtime_fields[0].type);
    runtime_fields[0].type = allocator.dupe(u8, type_alias_name) catch unreachable;

    allocator.free(runtime_event.module);
    runtime_event.module = allocator.dupe(u8, type_module) catch unreachable;

    allocator.free(runtime_proc.module);
    runtime_proc.module = allocator.dupe(u8, type_module) catch unreachable;

    // Build new Program: transform flows, filter out comptime event+proc, add runtime type+event+proc
    // Note: +3 because we're adding a type declaration AND event AND proc (3 new items total)
    const new_items = allocator.alloc(ast.Item, program.items.len + 3) catch unreachable;
    var idx: usize = 0;

    for (program.items) |prog_item| {
        // Filter out comptime renderHTML event_decl
        if (prog_item == .event_decl) {
            const event = prog_item.event_decl;
            if (event.path.segments.len == 1 and std.mem.eql(u8, event.path.segments[0], "renderHTML")) {
                continue; // Skip comptime event
            }

            // Transform the captured event to use derived type instead of anonymous struct
            if (event.path.segments.len == 1 and std.mem.eql(u8, event.path.segments[0], type_event_name)) {
                // Create new branches array with derived type reference
                const new_branches = allocator.alloc(ast.Branch, event.branches.len) catch unreachable;
                for (event.branches, 0..) |branch, branch_idx| {
                    if (std.mem.eql(u8, branch.name, type_branch_name)) {
                        // Replace inline struct with derived type reference
                        // Use __type_ref convention to signal a named type reference
                        const type_ref_field = allocator.alloc(ast.Field, 1) catch unreachable;
                        type_ref_field[0] = ast.Field{
                            .name = allocator.dupe(u8, "__type_ref") catch unreachable,
                            .type = allocator.dupe(u8, type_alias_name) catch unreachable,
                            .module_path = null,
                            .phantom = null,
                            .is_source = false,
                            .is_file = false,
                            .is_embed_file = false,
                            .expression = null,
                            .expression_str = null,
                            .owns_expression = false,
                        };
                        new_branches[branch_idx] = ast.Branch{
                            .name = branch.name,
                            .payload = ast.Shape{ .fields = type_ref_field },
                            .is_deferred = branch.is_deferred,
                            .is_optional = branch.is_optional,
                            .annotations = branch.annotations,
                        };
                    } else {
                        new_branches[branch_idx] = branch;
                    }
                }

                // Create transformed getUserData event
                const transformed_event = ast.EventDecl{
                    .path = event.path,
                    .input = event.input,
                    .branches = new_branches,
                    .is_public = event.is_public,
                    .is_implicit_flow = event.is_implicit_flow,
                    .annotations = event.annotations,
                    .is_pure = event.is_pure,
                    .is_transitively_pure = event.is_transitively_pure,
                    .location = event.location,
                    .module = event.module,
                };
                new_items[idx] = ast.Item{ .event_decl = transformed_event };
                idx += 1;
                continue;
            }
        }

        // Filter out comptime renderHTML proc_decl
        if (prog_item == .proc_decl) {
            const proc = prog_item.proc_decl;
            if (proc.path.segments.len == 1 and std.mem.eql(u8, proc.path.segments[0], "renderHTML")) {
                continue; // Skip comptime proc
            }
        }

        // Transform flows that have renderHTML in their continuation pipelines
        if (prog_item == .flow) {
            const flow = prog_item.flow;

            // Check if any continuation contains renderHTML
            var has_renderHTML = false;
            for (flow.continuations) |cont| {
                if (cont.node) |step| {
                    if (step == .invocation) {
                        const inv = step.invocation;
                        if (inv.path.segments.len == 1 and std.mem.eql(u8, inv.path.segments[0], "renderHTML")) {
                            has_renderHTML = true;
                            break;
                        }
                    }
                }
                if (has_renderHTML) break;
            }

            if (has_renderHTML) {
                // Transform the flow by transforming its continuations
                const new_continuations = allocator.alloc(ast.Continuation, flow.continuations.len) catch unreachable;

                for (flow.continuations, 0..) |cont, cont_idx| {
                    // Transform the step
                    var new_step: ?ast.Step = null;

                    if (cont.node) |step| {
                        if (step == .invocation) {
                            const inv = step.invocation;
                            if (inv.path.segments.len == 1 and std.mem.eql(u8, inv.path.segments[0], "renderHTML")) {
                                // Transform this invocation - remove Source arg, add binding arg
                                const transformed_path = ast.DottedPath{
                                    .module_qualifier = inv.path.module_qualifier,
                                    .segments = path_segments,
                                };

                                const new_args = allocator.alloc(ast.Arg, 1) catch unreachable;
                                new_args[0] = ast.Arg{
                                    .name = allocator.dupe(u8, binding.name) catch unreachable,
                                    .value = allocator.dupe(u8, binding.value_ref) catch unreachable,
                                    .source_value = null,
                                };

                                // Mark as already transformed to prevent infinite loops
                                const annotations = allocator.alloc([]const u8, 1) catch unreachable;
                                annotations[0] = allocator.dupe(u8, "@pass_ran(\"transform\")") catch unreachable;

                                const new_invocation = ast.Invocation{
                                    .path = transformed_path,
                                    .args = new_args,
                                    .annotations = annotations,
                                    .inserted_by_tap = false,
                                    .from_opaque_tap = false,
                                };

                                new_step = ast.Step{ .invocation = new_invocation };
                            } else {
                                // Keep step as-is
                                new_step = step;
                            }
                        } else {
                            // Keep step as-is
                            new_step = step;
                        }
                    }

                    // Create new continuation with transformed step
                    new_continuations[cont_idx] = ast.Continuation{
                        .branch = cont.branch,
                        .binding = cont.binding,
                        .binding_type = cont.binding_type,
                        .is_catchall = cont.is_catchall,
                        .catchall_metatype = cont.catchall_metatype,
                        .condition = cont.condition,
                        .condition_expr = cont.condition_expr,
                        .node = new_step,
                        .indent = cont.indent,
                        .continuations = cont.continuations,
                    };
                }

                // Create transformed flow with new continuations
                const transformed_flow = ast.Flow{
                    .invocation = flow.invocation,
                    .continuations = new_continuations,
                    .annotations = flow.annotations,
                    .pre_label = flow.pre_label,
                    .post_label = flow.post_label,
                    .super_shape = flow.super_shape,
                    .is_pure = flow.is_pure,
                    .is_transitively_pure = flow.is_transitively_pure,
                    .location = flow.location,
                    .module = flow.module,
                };

                new_items[idx] = ast.Item{ .flow = transformed_flow };
                idx += 1;
                continue;
            }
        }

        // Modules can be shared, everything else needs deep copy
        if (prog_item == .module_decl) {
            new_items[idx] = prog_item;
        } else {
            new_items[idx] = ast_functional.cloneItem(allocator, &prog_item) catch unreachable;
        }
        idx += 1;
    }

    // Create HostTypeDecl with the derived type name and fields
    const user_data_type = ast.HostTypeDecl{
        .name = type_alias_name,
        .shape = ast.Shape{ .fields = type_fields },
    };

    // Add runtime type alias, event, and proc
    new_items[idx] = ast.Item{ .host_type_decl = user_data_type };
    new_items[idx + 1] = ast.Item{ .event_decl = runtime_event };
    new_items[idx + 2] = ast.Item{ .proc_decl = runtime_proc };

    const new_program = allocator.create(ast.Program) catch unreachable;
    new_program.* = ast.Program{
        .items = new_items,
        .module_annotations = program.module_annotations,
        .main_module_name = program.main_module_name,
        .allocator = allocator,
    };

    return .{ .transformed = .{ .program = new_program } };
}

// THE BREAKTHROUGH TEST: Continuation binding + Source block
// The 'u' from | data u |> should be captured in source.scope.bindings!
~getUserData()
| data u |> renderHTML [HTML]{
        <h1>$[u.name]</h1>
        <p>Age: $[u.age]</p>
    }
    | rendered h |> std.io:println(text: h.html)
input.kz

Expected Output

<h1>Alice</h1> <p>Age: 42</p>

Test Configuration

MUST_RUN