mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
WIP
This commit is contained in:
parent
d711c5c452
commit
8bbb6d3bb8
5 changed files with 1895 additions and 1 deletions
|
|
@ -32,4 +32,6 @@ test "compile tests" {
|
|||
std.testing.refAllDecls(@import("test/int_test.zig"));
|
||||
std.testing.refAllDecls(@import("test/node_store_test.zig"));
|
||||
std.testing.refAllDecls(@import("test/import_store_test.zig"));
|
||||
std.testing.refAllDecls(@import("test/scope_test.zig"));
|
||||
std.testing.refAllDecls(@import("test/record_test.zig"));
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
289
src/canonicalize/test/record_test.zig
Normal file
289
src/canonicalize/test/record_test.zig
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
// test "record literal uses record_unbound" {
|
||||
// const gpa = std.testing.allocator;
|
||||
|
||||
// // Test a simple record literal
|
||||
// {
|
||||
// const source1 = "{ x: 42, y: \"hello\" }";
|
||||
|
||||
// var common_env = try base.CommonEnv.init(gpa, source1);
|
||||
// // Module env takes ownership of Common env -- no need to deinit here
|
||||
|
||||
// var env = try ModuleEnv.init(gpa, &common_env);
|
||||
// defer env.deinit();
|
||||
|
||||
// try env.initCIRFields(gpa, "test");
|
||||
|
||||
// var ast = try parse.parseExpr(&env, gpa);
|
||||
// defer ast.deinit(gpa);
|
||||
|
||||
// var can = try Self.init(&env, &ast, null);
|
||||
// defer can.deinit();
|
||||
|
||||
// const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx);
|
||||
// const canonical_expr_idx = try can.canonicalizeExpr(expr_idx) orelse {
|
||||
// return error.CanonicalizeError;
|
||||
// };
|
||||
|
||||
// // Get the type of the expression
|
||||
// const expr_var = @as(types.Var, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())));
|
||||
// const resolved = env.types.resolveVar(expr_var);
|
||||
|
||||
// // Check that it's a record_unbound
|
||||
// switch (resolved.desc.content) {
|
||||
// .structure => |structure| switch (structure) {
|
||||
// .record_unbound => |fields| {
|
||||
// // Success! The record literal created a record_unbound type
|
||||
// try testing.expect(fields.len() == 2);
|
||||
// },
|
||||
// else => return error.ExpectedRecordUnbound,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Test an empty record literal
|
||||
// {
|
||||
// const source2 = "{}";
|
||||
|
||||
// var env = try ModuleEnv.init(gpa, source2);
|
||||
// defer env.deinit();
|
||||
|
||||
// try env.initCIRFields(gpa, "test");
|
||||
|
||||
// var ast = try parse.parseExpr(&env, gpa);
|
||||
// defer ast.deinit(gpa);
|
||||
|
||||
// var can = try Self.init(&env, &ast, null);
|
||||
// defer can.deinit();
|
||||
|
||||
// const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx);
|
||||
// const canonical_expr_idx = try can.canonicalizeExpr(expr_idx) orelse {
|
||||
// return error.CanonicalizeError;
|
||||
// };
|
||||
|
||||
// // Get the type of the expression
|
||||
// const expr_var = @as(types.Var, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())));
|
||||
// const resolved = env.types.resolveVar(expr_var);
|
||||
|
||||
// // Check that it's an empty_record
|
||||
// switch (resolved.desc.content) {
|
||||
// .structure => |structure| switch (structure) {
|
||||
// .empty_record => {
|
||||
// // Success! Empty record literal created empty_record type
|
||||
// },
|
||||
// else => return error.ExpectedEmptyRecord,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Test a record with a single field
|
||||
// // Test a nested record literal
|
||||
// {
|
||||
// const source3 = "{ value: 123 }";
|
||||
|
||||
// var env = try ModuleEnv.init(gpa, source3);
|
||||
// defer env.deinit();
|
||||
|
||||
// try env.initCIRFields(gpa, "test");
|
||||
|
||||
// var ast = try parse.parseExpr(&env, gpa, gpa);
|
||||
// defer ast.deinit(gpa);
|
||||
|
||||
// var can = try Self.init(&env, &ast, null);
|
||||
// defer can.deinit();
|
||||
|
||||
// const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx);
|
||||
// const canonical_expr_idx = try can.canonicalizeExpr(expr_idx) orelse {
|
||||
// return error.CanonicalizeError;
|
||||
// };
|
||||
|
||||
// // Get the type of the expression
|
||||
// const expr_var = @as(types.Var, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())));
|
||||
// const resolved = env.types.resolveVar(expr_var);
|
||||
|
||||
// // Check that it's a record_unbound
|
||||
// switch (resolved.desc.content) {
|
||||
// .structure => |structure| switch (structure) {
|
||||
// .record_unbound => |fields| {
|
||||
// // Success! The record literal created a record_unbound type
|
||||
// try testing.expect(fields.len() == 1);
|
||||
|
||||
// // Check the field
|
||||
// const fields_slice = env.types.getRecordFieldsSlice(fields);
|
||||
// const field_name = env.getIdent(fields_slice.get(0).name);
|
||||
// try testing.expectEqualStrings("value", field_name);
|
||||
// },
|
||||
// else => return error.ExpectedRecordUnbound,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// test "record_unbound basic functionality" {
|
||||
// const gpa = std.testing.allocator;
|
||||
// const source = "{ x: 42, y: 99 }";
|
||||
|
||||
// var common_env = try base.CommonEnv.init(gpa, source);
|
||||
// // Module env takes ownership of Common env -- no need to deinit here
|
||||
|
||||
// // Test that record literals create record_unbound types
|
||||
// var env = try ModuleEnv.init(gpa, &common_env);
|
||||
// defer env.deinit();
|
||||
|
||||
// try env.initCIRFields(gpa, "test");
|
||||
|
||||
// var ast = try parse.parseExpr(&common_env, gpa);
|
||||
// defer ast.deinit(gpa);
|
||||
|
||||
// var can = try Self.init(&env, &ast, null);
|
||||
// defer can.deinit();
|
||||
|
||||
// const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx);
|
||||
// const canonical_expr_idx = try can.canonicalizeExpr(expr_idx) orelse {
|
||||
// return error.CanonicalizeError;
|
||||
// };
|
||||
|
||||
// // Get the type of the expression
|
||||
// const expr_var = @as(types.Var, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())));
|
||||
// const resolved = env.types.resolveVar(expr_var);
|
||||
|
||||
// // Verify it starts as record_unbound
|
||||
// switch (resolved.desc.content) {
|
||||
// .structure => |structure| switch (structure) {
|
||||
// .record_unbound => |fields| {
|
||||
// // Success! Record literal created record_unbound type
|
||||
// try testing.expect(fields.len() == 2);
|
||||
|
||||
// // Check field names
|
||||
// const field_slice = env.types.getRecordFieldsSlice(fields);
|
||||
// try testing.expectEqualStrings("x", env.getIdent(field_slice.get(0).name));
|
||||
// try testing.expectEqualStrings("y", env.getIdent(field_slice.get(1).name));
|
||||
// },
|
||||
// else => return error.ExpectedRecordUnbound,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// }
|
||||
|
||||
// test "record_unbound with multiple fields" {
|
||||
// const gpa = std.testing.allocator;
|
||||
// const source = "{ a: 123, b: 456, c: 789 }";
|
||||
|
||||
// var common_env = try base.CommonEnv.init(gpa, source);
|
||||
// // Module env takes ownership of Common env -- no need to deinit here
|
||||
|
||||
// var env = try ModuleEnv.init(gpa, &common_env);
|
||||
// defer env.deinit();
|
||||
|
||||
// try env.initCIRFields(gpa, "test");
|
||||
|
||||
// // Create record_unbound with multiple fields
|
||||
// var ast = try parse.parseExpr(&common_env, gpa);
|
||||
// defer ast.deinit(gpa);
|
||||
|
||||
// var can = try Self.init(&env, &ast, null);
|
||||
// defer can.deinit();
|
||||
|
||||
// const expr_idx: parse.AST.Expr.Idx = @enumFromInt(ast.root_node_idx);
|
||||
// const canonical_expr_idx = try can.canonicalizeExpr(expr_idx) orelse {
|
||||
// return error.CanonicalizeError;
|
||||
// };
|
||||
|
||||
// const expr_var = @as(types.Var, @enumFromInt(@intFromEnum(canonical_expr_idx.get_idx())));
|
||||
// const resolved = env.types.resolveVar(expr_var);
|
||||
|
||||
// // Should be record_unbound
|
||||
// switch (resolved.desc.content) {
|
||||
// .structure => |s| switch (s) {
|
||||
// .record_unbound => |fields| {
|
||||
// try testing.expect(fields.len() == 3);
|
||||
|
||||
// // Check field names
|
||||
// const field_slice = env.types.getRecordFieldsSlice(fields);
|
||||
// try testing.expectEqualStrings("a", env.getIdent(field_slice.get(0).name));
|
||||
// try testing.expectEqualStrings("b", env.getIdent(field_slice.get(1).name));
|
||||
// try testing.expectEqualStrings("c", env.getIdent(field_slice.get(2).name));
|
||||
// },
|
||||
// else => return error.ExpectedRecordUnbound,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// }
|
||||
|
||||
// test "record with extension variable" {
|
||||
// const gpa = std.testing.allocator;
|
||||
|
||||
// var common_env = try base.CommonEnv.init(gpa, "");
|
||||
// // Module env takes ownership of Common env -- no need to deinit here
|
||||
|
||||
// var env = try ModuleEnv.init(gpa, &common_env);
|
||||
// defer env.deinit();
|
||||
|
||||
// try env.initCIRFields(gpa, "test");
|
||||
|
||||
// // Test that regular records have extension variables
|
||||
// // Create { x: 42, y: "hi" }* (open record)
|
||||
// const num_var = try env.types.freshFromContent(Content{ .structure = .{ .num = .{ .int_precision = .i32 } } });
|
||||
// const str_var = try env.types.freshFromContent(Content{ .structure = .str });
|
||||
|
||||
// const fields = [_]types.RecordField{
|
||||
// .{ .name = try env.insertIdent( base.Ident.for_text("x")), .var_ = num_var },
|
||||
// .{ .name = try env.insertIdent( base.Ident.for_text("y")), .var_ = str_var },
|
||||
// };
|
||||
// const fields_range = try env.types.appendRecordFields(&fields);
|
||||
// const ext_var = try env.types.fresh(); // Open extension
|
||||
// const record_content = Content{ .structure = .{ .record = .{ .fields = fields_range, .ext = ext_var } } };
|
||||
// const record_var = try env.types.freshFromContent(record_content);
|
||||
|
||||
// // Verify the record has an extension variable
|
||||
// const resolved = env.types.resolveVar(record_var);
|
||||
// switch (resolved.desc.content) {
|
||||
// .structure => |structure| switch (structure) {
|
||||
// .record => |record| {
|
||||
// try testing.expect(record.fields.len() == 2);
|
||||
|
||||
// // Check that extension is a flex var (open record)
|
||||
// const ext_resolved = env.types.resolveVar(record.ext);
|
||||
// switch (ext_resolved.desc.content) {
|
||||
// .flex_var => {
|
||||
// // Success! The record has an open extension
|
||||
// },
|
||||
// else => return error.ExpectedFlexVar,
|
||||
// }
|
||||
// },
|
||||
// else => return error.ExpectedRecord,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
|
||||
// // Now test a closed record
|
||||
// const closed_ext_var = try env.types.freshFromContent(Content{ .structure = .empty_record });
|
||||
// const closed_record_content = Content{ .structure = .{ .record = .{ .fields = fields_range, .ext = closed_ext_var } } };
|
||||
// const closed_record_var = try env.types.freshFromContent(closed_record_content);
|
||||
|
||||
// // Verify the closed record has empty_record as extension
|
||||
// const closed_resolved = env.types.resolveVar(closed_record_var);
|
||||
// switch (closed_resolved.desc.content) {
|
||||
// .structure => |structure| switch (structure) {
|
||||
// .record => |record| {
|
||||
// try testing.expect(record.fields.len() == 2);
|
||||
|
||||
// // Check that extension is empty_record (closed record)
|
||||
// const ext_resolved = env.types.resolveVar(record.ext);
|
||||
// switch (ext_resolved.desc.content) {
|
||||
// .structure => |ext_structure| switch (ext_structure) {
|
||||
// .empty_record => {
|
||||
// // Success! The record is closed
|
||||
// },
|
||||
// else => return error.ExpectedEmptyRecord,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// },
|
||||
// else => return error.ExpectedRecord,
|
||||
// },
|
||||
// else => return error.ExpectedStructure,
|
||||
// }
|
||||
// }
|
||||
349
src/canonicalize/test/scope_test.zig
Normal file
349
src/canonicalize/test/scope_test.zig
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
const std = @import("std");
|
||||
const can = @import("can");
|
||||
const base = @import("base");
|
||||
const parse = @import("parse");
|
||||
|
||||
const CIR = @import("../CIR.zig");
|
||||
const ModuleEnv = can.ModuleEnv;
|
||||
const Ident = base.Ident;
|
||||
const Region = base.Region;
|
||||
const Scope = CIR.Scope;
|
||||
const Pattern = CIR.Pattern;
|
||||
const TypeAnno = CIR.TypeAnno;
|
||||
|
||||
/// Context helper for Scope tests
|
||||
const ScopeTestContext = struct {
|
||||
self: CIR,
|
||||
module_env: *ModuleEnv,
|
||||
gpa: std.mem.Allocator,
|
||||
|
||||
fn init(gpa: std.mem.Allocator) !ScopeTestContext {
|
||||
// heap allocate ModuleEnv for testing
|
||||
const module_env = try gpa.create(ModuleEnv);
|
||||
module_env.* = try ModuleEnv.init(gpa, "");
|
||||
try module_env.initCIRFields(gpa, "test");
|
||||
|
||||
return ScopeTestContext{
|
||||
.self = try CIR.init(module_env, undefined, null),
|
||||
.module_env = module_env,
|
||||
.gpa = gpa,
|
||||
};
|
||||
}
|
||||
|
||||
fn deinit(ctx: *ScopeTestContext) void {
|
||||
ctx.self.deinit();
|
||||
ctx.module_env.deinit();
|
||||
ctx.gpa.destroy(ctx.module_env);
|
||||
}
|
||||
};
|
||||
|
||||
test "basic scope initialization" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
// Test that we start with one scope (top-level)
|
||||
try std.testing.expect(ctx.self.scopes.items.len == 1);
|
||||
}
|
||||
|
||||
test "empty scope has no items" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
const foo_ident = try ctx.module_env.insertIdent(Ident.for_text("foo"));
|
||||
const result = ctx.self.scopeLookup(.ident, foo_ident);
|
||||
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .not_found = {} }, result);
|
||||
}
|
||||
|
||||
test "can add and lookup idents at top level" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
const foo_ident = try ctx.module_env.insertIdent(Ident.for_text("foo"));
|
||||
const bar_ident = try ctx.module_env.insertIdent(Ident.for_text("bar"));
|
||||
const foo_pattern: Pattern.Idx = @enumFromInt(1);
|
||||
const bar_pattern: Pattern.Idx = @enumFromInt(2);
|
||||
|
||||
// Add identifiers
|
||||
const foo_result = ctx.self.scopeIntroduceInternal(gpa, .ident, foo_ident, foo_pattern, false, true);
|
||||
const bar_result = ctx.self.scopeIntroduceInternal(gpa, .ident, bar_ident, bar_pattern, false, true);
|
||||
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, foo_result);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, bar_result);
|
||||
|
||||
// Lookup should find them
|
||||
const foo_lookup = ctx.self.scopeLookup(.ident, foo_ident);
|
||||
const bar_lookup = ctx.self.scopeLookup(.ident, bar_ident);
|
||||
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = foo_pattern }, foo_lookup);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = bar_pattern }, bar_lookup);
|
||||
}
|
||||
|
||||
test "nested scopes shadow outer scopes" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
const x_ident = try ctx.module_env.insertIdent(Ident.for_text("x"));
|
||||
const outer_pattern: Pattern.Idx = @enumFromInt(1);
|
||||
const inner_pattern: Pattern.Idx = @enumFromInt(2);
|
||||
|
||||
// Add x to outer scope
|
||||
const outer_result = ctx.self.scopeIntroduceInternal(gpa, .ident, x_ident, outer_pattern, false, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, outer_result);
|
||||
|
||||
// Enter new scope
|
||||
try ctx.self.scopeEnter(gpa, false);
|
||||
|
||||
// x from outer scope should still be visible
|
||||
const outer_lookup = ctx.self.scopeLookup(.ident, x_ident);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = outer_pattern }, outer_lookup);
|
||||
|
||||
// Add x to inner scope (shadows outer)
|
||||
const inner_result = ctx.self.scopeIntroduceInternal(gpa, .ident, x_ident, inner_pattern, false, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .shadowing_warning = outer_pattern }, inner_result);
|
||||
|
||||
// Now x should resolve to inner scope
|
||||
const inner_lookup = ctx.self.scopeLookup(.ident, x_ident);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = inner_pattern }, inner_lookup);
|
||||
|
||||
// Exit inner scope
|
||||
try ctx.self.scopeExit(gpa);
|
||||
|
||||
// x should resolve to outer scope again
|
||||
const after_exit_lookup = ctx.self.scopeLookup(.ident, x_ident);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = outer_pattern }, after_exit_lookup);
|
||||
}
|
||||
|
||||
test "top level var error" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
const var_ident = try ctx.module_env.insertIdent(Ident.for_text("count_"));
|
||||
const pattern: Pattern.Idx = @enumFromInt(1);
|
||||
|
||||
// Should fail to introduce var at top level
|
||||
const result = ctx.self.scopeIntroduceInternal(gpa, .ident, var_ident, pattern, true, true);
|
||||
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .top_level_var_error = {} }, result);
|
||||
}
|
||||
|
||||
test "type variables are tracked separately from value identifiers" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
// Create identifiers for 'a' - one for value, one for type
|
||||
const a_ident = try ctx.module_env.insertIdent(Ident.for_text("a"));
|
||||
const pattern: Pattern.Idx = @enumFromInt(1);
|
||||
const type_anno: TypeAnno.Idx = @enumFromInt(1);
|
||||
|
||||
// Introduce 'a' as a value identifier
|
||||
const value_result = ctx.self.scopeIntroduceInternal(gpa, .ident, a_ident, pattern, false, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, value_result);
|
||||
|
||||
// Introduce 'a' as a type variable - should succeed because they're in separate namespaces
|
||||
const current_scope = &ctx.self.scopes.items[ctx.self.scopes.items.len - 1];
|
||||
const type_result = current_scope.introduceTypeVar(gpa, a_ident, type_anno, null);
|
||||
try std.testing.expectEqual(Scope.TypeVarIntroduceResult{ .success = {} }, type_result);
|
||||
|
||||
// Lookup 'a' as value should find the pattern
|
||||
const value_lookup = ctx.self.scopeLookup(.ident, a_ident);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = pattern }, value_lookup);
|
||||
|
||||
// Lookup 'a' as type variable should find the type annotation
|
||||
const type_lookup = current_scope.lookupTypeVar(a_ident);
|
||||
try std.testing.expectEqual(Scope.TypeVarLookupResult{ .found = type_anno }, type_lookup);
|
||||
}
|
||||
|
||||
test "var reassignment within same function" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
// Enter function scope
|
||||
try ctx.self.scopeEnter(gpa, true);
|
||||
|
||||
const count_ident = try ctx.module_env.insertIdent(Ident.for_text("count_"));
|
||||
const pattern1: Pattern.Idx = @enumFromInt(1);
|
||||
const pattern2: Pattern.Idx = @enumFromInt(2);
|
||||
|
||||
// Declare var
|
||||
const declare_result = ctx.self.scopeIntroduceInternal(gpa, .ident, count_ident, pattern1, true, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, declare_result);
|
||||
|
||||
// Reassign var (not a declaration)
|
||||
const reassign_result = ctx.self.scopeIntroduceInternal(gpa, .ident, count_ident, pattern2, true, false);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, reassign_result);
|
||||
|
||||
// Should resolve to the reassigned value
|
||||
const lookup_result = ctx.self.scopeLookup(.ident, count_ident);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = pattern2 }, lookup_result);
|
||||
}
|
||||
|
||||
test "var reassignment across function boundary fails" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
// Enter first function scope
|
||||
try ctx.self.scopeEnter(gpa, true);
|
||||
|
||||
const count_ident = try ctx.module_env.insertIdent(Ident.for_text("count_"));
|
||||
const pattern1: Pattern.Idx = @enumFromInt(1);
|
||||
const pattern2: Pattern.Idx = @enumFromInt(2);
|
||||
|
||||
// Declare var in first function
|
||||
const declare_result = ctx.self.scopeIntroduceInternal(gpa, .ident, count_ident, pattern1, true, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, declare_result);
|
||||
|
||||
// Enter second function scope (function boundary)
|
||||
try ctx.self.scopeEnter(gpa, true);
|
||||
|
||||
// Try to reassign var from different function - should fail
|
||||
const reassign_result = ctx.self.scopeIntroduceInternal(gpa, .ident, count_ident, pattern2, true, false);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .var_across_function_boundary = pattern1 }, reassign_result);
|
||||
}
|
||||
|
||||
test "identifiers with and without underscores are different" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
const sum_ident = try ctx.module_env.insertIdent(Ident.for_text("sum"));
|
||||
const sum_underscore_ident = try ctx.module_env.insertIdent(Ident.for_text("sum_"));
|
||||
const pattern1: Pattern.Idx = @enumFromInt(1);
|
||||
const pattern2: Pattern.Idx = @enumFromInt(2);
|
||||
|
||||
// Enter function scope so we can use var
|
||||
try ctx.self.scopeEnter(gpa, true);
|
||||
|
||||
// Introduce regular identifier
|
||||
const regular_result = ctx.self.scopeIntroduceInternal(gpa, .ident, sum_ident, pattern1, false, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, regular_result);
|
||||
|
||||
// Introduce var with underscore - should not conflict
|
||||
const var_result = ctx.self.scopeIntroduceInternal(gpa, .ident, sum_underscore_ident, pattern2, true, true);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, var_result);
|
||||
|
||||
// Both should be found independently
|
||||
const regular_lookup = ctx.self.scopeLookup(.ident, sum_ident);
|
||||
const var_lookup = ctx.self.scopeLookup(.ident, sum_underscore_ident);
|
||||
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = pattern1 }, regular_lookup);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = pattern2 }, var_lookup);
|
||||
}
|
||||
|
||||
test "aliases work separately from idents" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
const foo_ident = try ctx.module_env.insertIdent(Ident.for_text("Foo"));
|
||||
const ident_pattern: Pattern.Idx = @enumFromInt(1);
|
||||
const alias_pattern: Pattern.Idx = @enumFromInt(2);
|
||||
|
||||
// Add as both ident and alias (they're in separate namespaces)
|
||||
const ident_result = ctx.self.scopeIntroduceInternal(gpa, .ident, foo_ident, ident_pattern, false, true);
|
||||
const alias_result = ctx.self.scopeIntroduceInternal(gpa, .alias, foo_ident, alias_pattern, false, true);
|
||||
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, ident_result);
|
||||
try std.testing.expectEqual(Scope.IntroduceResult{ .success = {} }, alias_result);
|
||||
|
||||
// Both should be found in their respective namespaces
|
||||
const ident_lookup = ctx.self.scopeLookup(.ident, foo_ident);
|
||||
const alias_lookup = ctx.self.scopeLookup(.alias, foo_ident);
|
||||
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = ident_pattern }, ident_lookup);
|
||||
try std.testing.expectEqual(Scope.LookupResult{ .found = alias_pattern }, alias_lookup);
|
||||
}
|
||||
|
||||
test "unused variables are sorted by region" {
|
||||
const gpa = std.testing.allocator;
|
||||
|
||||
// Create a test program with unused variables in non-alphabetical order
|
||||
const source =
|
||||
\\app [main!] { pf: platform "../basic-cli/main.roc" }
|
||||
\\
|
||||
\\func = |_| {
|
||||
\\ zebra = 5 # Line 3 - should be reported first
|
||||
\\ apple = 10 # Line 4 - should be reported second
|
||||
\\ monkey = 15 # Line 5 - should be reported third
|
||||
\\ used = 20 # Line 6 - this one is used
|
||||
\\ used
|
||||
\\}
|
||||
\\
|
||||
\\main! = |_| func({})
|
||||
;
|
||||
|
||||
var ctx = try ScopeTestContext.init(gpa);
|
||||
defer ctx.deinit();
|
||||
|
||||
// Parse the source
|
||||
const ast = try parse.AST.parseFromStr(gpa, source, "test.roc", &ctx.module_env.string_interner);
|
||||
defer ast.deinit();
|
||||
|
||||
// Canonicalize the AST
|
||||
const parsed_module = ast.parsed_module;
|
||||
var self = try CIR.initFromAST(parsed_module, &ctx.module_env, source);
|
||||
try self.canonicalizeModule();
|
||||
defer self.deinit();
|
||||
|
||||
// Check that we have unused variable diagnostics
|
||||
var unused_var_diagnostics = std.ArrayList(struct {
|
||||
ident: Ident.Idx,
|
||||
region: Region,
|
||||
}).init(gpa);
|
||||
defer unused_var_diagnostics.deinit();
|
||||
|
||||
// Collect all unused variable diagnostics
|
||||
for (ctx.module_env.diagnostics.items) |diagnostic| {
|
||||
switch (diagnostic) {
|
||||
.unused_variable => |data| {
|
||||
try unused_var_diagnostics.append(.{
|
||||
.ident = data.ident,
|
||||
.region = data.region,
|
||||
});
|
||||
},
|
||||
else => continue,
|
||||
}
|
||||
}
|
||||
|
||||
// We should have exactly 3 unused variables (zebra, apple, monkey)
|
||||
try std.testing.expectEqual(@as(usize, 3), unused_var_diagnostics.items.len);
|
||||
|
||||
// Check that they are sorted by region (line number)
|
||||
// The source positions should be in increasing order
|
||||
var prev_offset: u32 = 0;
|
||||
for (unused_var_diagnostics.items) |diagnostic| {
|
||||
const current_offset = diagnostic.region.start.offset;
|
||||
|
||||
// Each unused variable should appear after the previous one in the source
|
||||
try std.testing.expect(current_offset > prev_offset);
|
||||
prev_offset = current_offset;
|
||||
|
||||
// Also verify the names are in the expected order (zebra, apple, monkey)
|
||||
const ident_text = ctx.module_env.getIdent(diagnostic.ident);
|
||||
if (unused_var_diagnostics.items[0].ident.idx == diagnostic.ident.idx) {
|
||||
try std.testing.expectEqualStrings("zebra", ident_text);
|
||||
} else if (unused_var_diagnostics.items[1].ident.idx == diagnostic.ident.idx) {
|
||||
try std.testing.expectEqualStrings("apple", ident_text);
|
||||
} else if (unused_var_diagnostics.items[2].ident.idx == diagnostic.ident.idx) {
|
||||
try std.testing.expectEqualStrings("monkey", ident_text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -913,7 +913,6 @@ test "SafeList edge cases serialization" {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO FIXME
|
||||
test "SafeList CompactWriter verify offset calculation" {
|
||||
const gpa = testing.allocator;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue