mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
646 lines
27 KiB
Zig
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);
|
|
}
|