003 glob suffix validation

✗ Failing This test is currently failing.

Failed: backend-exec

Code

// Test: Unknown suffix should error with Levenshtein-based suggestion
//
// ~log.warning("...") should fail because "warning" is not a valid level.
// The transform should:
// 1. Match log.* pattern
// 2. Extract suffix "warning"
// 3. Check against valid levels: ["error", "warn", "info", "debug"]
// 4. Find closest match via Levenshtein: "warn"
// 5. Emit compile error: "Unknown log level 'warning', did you mean 'warn'?"

~[comptime|transform]pub event log.* {
    event_name: []const u8,
    item: *const Item,
    program: *const Program,
}
| transformed { program: *const Program }
| compile_error { message: []const u8 }

~proc log.* {
    const std = @import("std");
    const allocator = std.heap.page_allocator;
    const valid_levels = [_][]const u8{ "error", "warn", "info", "debug" };

    // Levenshtein edit distance - computes minimum edits to transform a into b
    const levenshtein = struct {
        fn calc(alloc: std.mem.Allocator, a: []const u8, b: []const u8) usize {
            if (a.len == 0) return b.len;
            if (b.len == 0) return a.len;

            // Use two rows instead of full matrix for O(min(m,n)) space
            var prev_row = alloc.alloc(usize, b.len + 1) catch return std.math.maxInt(usize);
            var curr_row = alloc.alloc(usize, b.len + 1) catch return std.math.maxInt(usize);
            defer alloc.free(prev_row);
            defer alloc.free(curr_row);

            // Initialize first row
            for (prev_row, 0..) |*cell, j| {
                cell.* = j;
            }

            // Fill matrix row by row
            for (a, 0..) |ca, i| {
                curr_row[0] = i + 1;
                for (b, 0..) |cb, j| {
                    const cost: usize = if (ca == cb) 0 else 1;
                    curr_row[j + 1] = @min(
                        @min(prev_row[j + 1] + 1, curr_row[j] + 1),  // delete, insert
                        prev_row[j] + cost  // substitute
                    );
                }
                // Swap rows
                const tmp = prev_row;
                prev_row = curr_row;
                curr_row = tmp;
            }
            return prev_row[b.len];
        }
    }.calc;

    // Extract the log level from event_name
    const level = event_name[4..]; // Skip "log."

    // Check if level is valid
    var found = false;
    for (valid_levels) |valid| {
        if (std.mem.eql(u8, level, valid)) {
            found = true;
            break;
        }
    }

    if (!found) {
        // Find closest match using Levenshtein edit distance
        var closest: []const u8 = valid_levels[0];
        var min_dist: usize = std.math.maxInt(usize);

        for (valid_levels) |valid| {
            const dist = levenshtein(allocator, level, valid);
            if (dist < min_dist) {
                min_dist = dist;
                closest = valid;
            }
        }

        const msg = std.fmt.allocPrint(
            allocator,
            "Unknown log level '{s}', did you mean '{s}'?",
            .{ level, closest }
        ) catch return .{ .compile_error = .{ .message = "allocation failed" } };

        return .{ .compile_error = .{ .message = msg } };
    }

    // Valid level - emit logging code (just replace with comment for now)
    const ast = @import("ast");
    const ast_functional = @import("ast_functional");

    const flow = if (item.* == .flow) &item.flow else return .{ .transformed = .{ .program = program } };

    const inline_code_item = ast.Item{
        .inline_code = .{
            .code = "// valid log level",
            .location = flow.location,
            .module = allocator.dupe(u8, flow.module) catch return .{ .transformed = .{ .program = program } },
        },
    };

    const maybe_new_program = ast_functional.replaceFlowRecursive(allocator, program, flow, inline_code_item) catch return .{ .transformed = .{ .program = program } };
    if (maybe_new_program) |new_program| {
        const result = allocator.create(ast.Program) catch return .{ .transformed = .{ .program = program } };
        result.* = new_program;
        return .{ .transformed = .{ .program = result } };
    }
    return .{ .transformed = .{ .program = program } };
}

// This should trigger the Levenshtein suggestion
~log.warning("This should suggest 'warn'")
input.kz

Test Configuration

Expected Behavior:

BACKEND_EXEC_ERROR

Expected Error:

did you mean 'warn'