roc/src/eval/test/helpers.zig
2025-11-16 22:59:24 -05:00

646 lines
27 KiB
Zig

//! Tests for the expression evaluator
const std = @import("std");
const parse = @import("parse");
const types = @import("types");
const base = @import("base");
const can = @import("can");
const check = @import("check");
const builtins = @import("builtins");
const collections = @import("collections");
const compiled_builtins = @import("compiled_builtins");
const eval_mod = @import("../mod.zig");
const builtin_loading_mod = eval_mod.builtin_loading;
const TestEnv = @import("TestEnv.zig");
const Interpreter = eval_mod.Interpreter;
const StackValue = eval_mod.StackValue;
const BuiltinTypes = eval_mod.BuiltinTypes;
const LoadedModule = builtin_loading_mod.LoadedModule;
const deserializeBuiltinIndices = builtin_loading_mod.deserializeBuiltinIndices;
const loadCompiledModule = builtin_loading_mod.loadCompiledModule;
const Check = check.Check;
const Can = can.Can;
const CIR = can.CIR;
const ModuleEnv = can.ModuleEnv;
const test_allocator = std.testing.allocator;
const TestParseError = parse.Parser.Error || error{ TokenizeError, SyntaxError };
const TraceWriter = struct {
buffer: [256]u8 = undefined,
writer: std.fs.File.Writer = undefined,
fn init() TraceWriter {
var tw = TraceWriter{};
tw.writer = std.fs.File.stderr().writer(&tw.buffer);
return tw;
}
fn interface(self: *TraceWriter) *std.Io.Writer {
return &self.writer.interface;
}
};
/// Helper function to run an expression and expect a specific error.
pub fn runExpectError(src: []const u8, expected_error: anyerror, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
_ = interpreter.evalMinimal(resources.expr_idx, ops) catch |err| {
try std.testing.expectEqual(expected_error, err);
return;
};
// If we reach here, no error was thrown.
try std.testing.expect(false);
}
/// Helpers to setup and run an interpreter expecting an integer result.
pub fn runExpectInt(src: []const u8, expected_int: i128, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
try std.testing.expectEqual(expected_int, result.asI128());
}
/// Helper function to run an expression and expect a boolean result.
pub fn runExpectBool(src: []const u8, expected_bool: bool, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
const actual = switch (result.layout.tag) {
.scalar => switch (result.layout.data.scalar.tag) {
.bool => result.asBool(),
.int => result.asI128() != 0,
else => return error.TestUnexpectedResult,
},
else => return error.TestUnexpectedResult,
};
try std.testing.expectEqual(expected_bool, actual);
}
/// Helper function to run an expression and expect an f32 result (with epsilon tolerance).
pub fn runExpectF32(src: []const u8, expected_f32: f32, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
const actual = result.asF32();
const epsilon: f32 = 0.0001;
const diff = @abs(actual - expected_f32);
if (diff > epsilon) {
std.debug.print("Expected {d}, got {d}, diff {d}\n", .{ expected_f32, actual, diff });
return error.TestExpectedEqual;
}
}
/// Helper function to run an expression and expect an f64 result (with epsilon tolerance).
pub fn runExpectF64(src: []const u8, expected_f64: f64, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
const actual = result.asF64();
const epsilon: f64 = 0.000000001;
const diff = @abs(actual - expected_f64);
if (diff > epsilon) {
std.debug.print("Expected {d}, got {d}, diff {d}\n", .{ expected_f64, actual, diff });
return error.TestExpectedEqual;
}
}
/// Helper function to run an expression and expect a Dec result.
/// Dec is a fixed-point decimal type stored as i128 with 18 decimal places.
/// For testing, we compare the raw i128 values directly.
pub fn runExpectDec(src: []const u8, expected_dec_num: i128, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
const actual_dec = result.asDec();
if (actual_dec.num != expected_dec_num) {
std.debug.print("Expected Dec({d}), got Dec({d})\n", .{ expected_dec_num, actual_dec.num });
return error.TestExpectedEqual;
}
}
/// Helpers to setup and run an interpreter expecting a string result.
pub fn runExpectStr(src: []const u8, expected_str: []const u8, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
try std.testing.expect(result.layout.tag == .scalar);
try std.testing.expect(result.layout.data.scalar.tag == .str);
const roc_str: *const builtins.str.RocStr = @ptrCast(@alignCast(result.ptr.?));
const str_slice = roc_str.asSlice();
try std.testing.expectEqualStrings(expected_str, str_slice);
if (!roc_str.isSmallStr()) {
const mutable_roc_str: *builtins.str.RocStr = @constCast(roc_str);
mutable_roc_str.decref(ops);
} else {
result.decref(layout_cache, ops);
}
}
/// A record field we expect to see in our unit test results
pub const ExpectedField = struct {
name: []const u8,
value: i128,
};
/// A tuple element we expect to see in our unit test results
pub const ExpectedElement = struct {
index: u32,
value: i128,
};
/// Helpers to setup and run an interpreter expecting a tuple result.
pub fn runExpectTuple(src: []const u8, expected_elements: []const ExpectedElement, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
// Verify we got a tuple layout
try std.testing.expect(result.layout.tag == .tuple);
// Use the TupleAccessor to safely access tuple elements
const tuple_accessor = try result.asTuple(layout_cache);
try std.testing.expectEqual(expected_elements.len, tuple_accessor.getElementCount());
for (expected_elements) |expected_element| {
// Get the element at the specified index
const element = try tuple_accessor.getElement(@intCast(expected_element.index));
// Verify it's an integer
try std.testing.expect(element.layout.tag == .scalar and element.layout.data.scalar.tag == .int);
// Get the integer value from the element
const int_val = element.asI128();
try std.testing.expectEqual(expected_element.value, int_val);
}
}
/// Helpers to setup and run an interpreter expecting a record result.
pub fn runExpectRecord(src: []const u8, expected_fields: []const ExpectedField, should_trace: enum { trace, no_trace }) !void {
const resources = try parseAndCanonicalizeExpr(test_allocator, src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const enable_trace = should_trace == .trace;
if (enable_trace) {
interpreter.startTrace();
}
defer if (enable_trace) interpreter.endTrace();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
// Verify we got a record layout
try std.testing.expect(result.layout.tag == .record);
const record_data = layout_cache.getRecordData(result.layout.data.record.idx);
const sorted_fields = layout_cache.record_fields.sliceRange(record_data.getFields());
try std.testing.expectEqual(expected_fields.len, sorted_fields.len);
for (expected_fields) |expected_field| {
var found = false;
var i: u32 = 0;
while (i < sorted_fields.len) : (i += 1) {
const sorted_field = sorted_fields.get(i);
const field_name = resources.module_env.getIdent(sorted_field.name);
if (std.mem.eql(u8, field_name, expected_field.name)) {
found = true;
const field_layout = layout_cache.getLayout(sorted_field.layout);
try std.testing.expect(field_layout.tag == .scalar and field_layout.data.scalar.tag == .int);
const offset = layout_cache.getRecordFieldOffset(result.layout.data.record.idx, i);
const field_ptr = @as([*]u8, @ptrCast(result.ptr.?)) + offset;
const field_value = StackValue{
.layout = field_layout,
.ptr = field_ptr,
.is_initialized = true,
};
const int_val = field_value.asI128();
try std.testing.expectEqual(expected_field.value, int_val);
break;
}
}
try std.testing.expect(found);
}
}
/// Parse and canonicalize an expression.
pub fn parseAndCanonicalizeExpr(allocator: std.mem.Allocator, source: []const u8) TestParseError!struct {
module_env: *ModuleEnv,
parse_ast: *parse.AST,
can: *Can,
checker: *Check,
expr_idx: CIR.Expr.Idx,
bool_stmt: CIR.Statement.Idx,
builtin_module: LoadedModule,
builtin_indices: CIR.BuiltinIndices,
builtin_types: BuiltinTypes,
} {
// Load Builtin module once - Bool, Try, and Str are all types within this module
const builtin_indices = try deserializeBuiltinIndices(allocator, compiled_builtins.builtin_indices_bin);
var builtin_module = try loadCompiledModule(allocator, compiled_builtins.builtin_bin, "Builtin", compiled_builtins.builtin_source);
errdefer builtin_module.deinit();
// Initialize the ModuleEnv
const module_env = try allocator.create(ModuleEnv);
module_env.* = try ModuleEnv.init(allocator, source);
module_env.common.source = source;
try module_env.common.calcLineStarts(module_env.gpa);
// Parse the source code as an expression (following REPL pattern)
const parse_ast = try allocator.create(parse.AST);
parse_ast.* = try parse.parseExpr(&module_env.common, module_env.gpa);
// Check for parse errors in test code
// NOTE: This is TEST-ONLY behavior! In production, the parser continues and collects
// diagnostics to provide better error messages. But for tests, we want to fail early
// on syntax errors to catch issues like semicolons that shouldn't be in Roc code.
if (parse_ast.tokenize_diagnostics.items.len > 0) {
// Found tokenization errors in test code
return error.TokenizeError;
}
if (parse_ast.parse_diagnostics.items.len > 0) {
// Found parse errors in test code
return error.SyntaxError;
}
// Empty scratch space (required before canonicalization)
parse_ast.store.emptyScratch();
// Initialize CIR fields in ModuleEnv
try module_env.initCIRFields(allocator, "test");
// Register Builtin as import so Bool, Try, and Str are available
_ = try module_env.imports.getOrPut(allocator, &module_env.common.strings, "Builtin");
// Get Bool, Try, and Str statement indices from Builtin module
const bool_stmt_in_bool_module = builtin_indices.bool_type;
const try_stmt_in_result_module = builtin_indices.try_type;
const str_stmt_in_builtin_module = builtin_indices.str_type;
const common_idents: Check.CommonIdents = .{
.module_name = try module_env.insertIdent(base.Ident.for_text("test")),
.list = try module_env.insertIdent(base.Ident.for_text("List")),
.box = try module_env.insertIdent(base.Ident.for_text("Box")),
.bool_stmt = bool_stmt_in_bool_module,
.try_stmt = try_stmt_in_result_module,
.str_stmt = str_stmt_in_builtin_module,
.builtin_module = builtin_module.env,
};
// Create module_envs map for canonicalization (enables qualified calls)
var module_envs_map = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(allocator);
defer module_envs_map.deinit();
const bool_ident = try module_env.insertIdent(base.Ident.for_text("Bool"));
const result_ident = try module_env.insertIdent(base.Ident.for_text("Try"));
const str_ident = try module_env.insertIdent(base.Ident.for_text("Str"));
const list_ident = try module_env.insertIdent(base.Ident.for_text("List"));
const dict_ident = try module_env.insertIdent(base.Ident.for_text("Dict"));
const set_ident = try module_env.insertIdent(base.Ident.for_text("Set"));
try module_envs_map.put(bool_ident, .{
.env = builtin_module.env,
.statement_idx = builtin_indices.bool_type,
});
try module_envs_map.put(result_ident, .{
.env = builtin_module.env,
.statement_idx = builtin_indices.try_type,
});
// Str does NOT get a statement_idx because it's transformed to a primitive type
// (see transformStrNominalToPrimitive in builtin_compiler)
try module_envs_map.put(str_ident, .{
.env = builtin_module.env,
});
try module_envs_map.put(list_ident, .{
.env = builtin_module.env,
.statement_idx = builtin_indices.list_type,
});
try module_envs_map.put(dict_ident, .{
.env = builtin_module.env,
.statement_idx = builtin_indices.dict_type,
});
try module_envs_map.put(set_ident, .{
.env = builtin_module.env,
.statement_idx = builtin_indices.set_type,
});
// Create czer with module_envs_map for qualified name resolution (following REPL pattern)
const czer = try allocator.create(Can);
czer.* = try Can.init(module_env, parse_ast, &module_envs_map);
// NOTE: Qualified tags like Bool.True and Bool.False do not currently work in test expressions
// because the canonicalizer doesn't support cross-module type references.
// See QUALIFIED_TAGS.md for details on what needs to be implemented.
//
// For now, tests should use unqualified tags (True, False) which work via unqualified_nominal_tags map.
// Canonicalize the expression (following REPL pattern)
const expr_idx: parse.AST.Expr.Idx = @enumFromInt(parse_ast.root_node_idx);
const canonical_expr = try czer.canonicalizeExpr(expr_idx) orelse {
// If canonicalization fails, create a runtime error
const diagnostic_idx = try module_env.store.addDiagnostic(.{ .not_implemented = .{
.feature = try module_env.insertString("canonicalization failed"),
.region = base.Region.zero(),
} });
const checker = try allocator.create(Check);
// Pass Bool and Try as imported modules
const imported_envs = [_]*const ModuleEnv{builtin_module.env};
checker.* = try Check.init(allocator, &module_env.types, module_env, &imported_envs, &module_envs_map, &module_env.store.regions, common_idents);
const builtin_types = BuiltinTypes.init(builtin_indices, builtin_module.env, builtin_module.env, builtin_module.env);
return .{
.module_env = module_env,
.parse_ast = parse_ast,
.can = czer,
.checker = checker,
.expr_idx = try module_env.store.addExpr(.{ .e_runtime_error = .{
.diagnostic = diagnostic_idx,
} }, base.Region.zero()),
.bool_stmt = bool_stmt_in_bool_module,
.builtin_module = builtin_module,
.builtin_indices = builtin_indices,
.builtin_types = builtin_types,
};
};
const canonical_expr_idx = canonical_expr.get_idx();
// Create type checker - pass Builtin as imported module
const imported_envs = [_]*const ModuleEnv{builtin_module.env};
const checker = try allocator.create(Check);
checker.* = try Check.init(allocator, &module_env.types, module_env, &imported_envs, &module_envs_map, &module_env.store.regions, common_idents);
// Type check the expression
_ = try checker.checkExprRepl(canonical_expr_idx);
const builtin_types = BuiltinTypes.init(builtin_indices, builtin_module.env, builtin_module.env, builtin_module.env);
return .{
.module_env = module_env,
.parse_ast = parse_ast,
.can = czer,
.checker = checker,
.expr_idx = canonical_expr_idx,
.bool_stmt = bool_stmt_in_bool_module,
.builtin_module = builtin_module,
.builtin_indices = builtin_indices,
.builtin_types = builtin_types,
};
}
/// Cleanup resources allocated by parseAndCanonicalizeExpr.
pub fn cleanupParseAndCanonical(allocator: std.mem.Allocator, resources: anytype) void {
// Cast away const since deinit() needs mutable access
var builtin_module_copy = resources.builtin_module;
builtin_module_copy.deinit();
resources.checker.deinit();
resources.can.deinit();
resources.parse_ast.deinit(allocator);
// module_env.source is not owned by module_env - don't free it
resources.module_env.deinit();
allocator.destroy(resources.checker);
allocator.destroy(resources.can);
allocator.destroy(resources.parse_ast);
allocator.destroy(resources.module_env);
}
test "eval runtime error - returns crash error" {
try runExpectError("{ crash \"test feature\" 0 }", error.Crash, .no_trace);
}
test "eval tag - already primitive" {
const resources = try parseAndCanonicalizeExpr(test_allocator, "True");
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
const builtin_types = BuiltinTypes.init(resources.builtin_indices, resources.builtin_module.env, resources.builtin_module.env, resources.builtin_module.env);
var interpreter = try Interpreter.init(test_allocator, resources.module_env, builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const ops = test_env_instance.get_ops();
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
try std.testing.expect(result.layout.tag == .scalar);
try std.testing.expect(result.ptr != null);
}
test "interpreter reuse across multiple evaluations" {
const cases = [_]struct {
src: []const u8,
expected: i128,
}{
.{ .src = "42", .expected = 42 },
.{ .src = "100 + 200", .expected = 300 },
.{ .src = "if True 1 else 2", .expected = 1 },
};
for (cases) |case| {
const resources = try parseAndCanonicalizeExpr(test_allocator, case.src);
defer cleanupParseAndCanonical(test_allocator, resources);
var test_env_instance = TestEnv.init(test_allocator);
defer test_env_instance.deinit();
var interpreter = try Interpreter.init(test_allocator, resources.module_env, resources.builtin_types, &[_]*const can.ModuleEnv{});
defer interpreter.deinit();
const ops = test_env_instance.get_ops();
var iteration: usize = 0;
while (iteration < 2) : (iteration += 1) {
const result = try interpreter.evalMinimal(resources.expr_idx, ops);
const layout_cache = &interpreter.runtime_layout_store;
defer result.decref(layout_cache, ops);
try std.testing.expect(result.layout.tag == .scalar);
try std.testing.expect(result.layout.data.scalar.tag == .int);
try std.testing.expectEqual(case.expected, result.asI128());
}
try std.testing.expectEqual(@as(usize, 0), interpreter.bindings.items.len);
}
}
test "nominal type context preservation - boolean" {
// Test that True and False get correct boolean layout
// This tests the nominal type context preservation fix
// Test True
try runExpectBool("True", true, .no_trace);
// Test False
try runExpectBool("False", false, .no_trace);
// Test boolean negation with nominal types
try runExpectBool("!True", false, .no_trace);
try runExpectBool("!False", true, .no_trace);
// Test boolean operations with nominal types
try runExpectBool("True and False", false, .no_trace);
try runExpectBool("True or False", true, .no_trace);
}
test "nominal type context preservation - regression prevention" {
// Test that the fix prevents the original regression
// The original issue was that (|x| !x)(True) would return "0" instead of "False"
// This should work correctly now with nominal type context preservation
try runExpectBool("(|x| !x)(True)", false, .no_trace);
try runExpectBool("(|x| !x)(False)", true, .no_trace);
}