roc/src/repl/eval.zig
2025-10-26 10:16:13 -04:00

605 lines
25 KiB
Zig

//! The evaluation part of the Read-Eval-Print-Loop (REPL)
const std = @import("std");
const base = @import("base");
const compile = @import("compile");
const parse = @import("parse");
const types = @import("types");
const can = @import("can");
const Can = can.Can;
const check = @import("check");
const Check = check.Check;
const builtins = @import("builtins");
const eval_mod = @import("eval");
const CrashContext = eval_mod.CrashContext;
const BuiltinTypes = eval_mod.BuiltinTypes;
const collections = @import("collections");
const AST = parse.AST;
const Allocator = std.mem.Allocator;
const ModuleEnv = can.ModuleEnv;
const RocOps = builtins.host_abi.RocOps;
/// Wrapper for a loaded compiled builtin module that tracks the buffer
const LoadedModule = struct {
env: *ModuleEnv,
buffer: []align(collections.CompactWriter.SERIALIZATION_ALIGNMENT.toByteUnits()) u8,
gpa: std.mem.Allocator,
fn deinit(self: *LoadedModule) void {
// IMPORTANT: When a module is deserialized from a buffer, most internal structures
// (common, types, external_decls, store, imports.items) contain pointers INTO the buffer,
// not separately allocated memory. However, the imports HASHMAP IS separately
// allocated during deserialization and MUST be freed.
//
// Memory to free:
// 1. The imports hashmap only (allocated during deserialization at CIR.zig:648)
// 2. The buffer itself (which contains all the deserialized data)
// 3. The env struct itself (which was allocated with create())
// Free ONLY the imports hashmap (allocated during deserialization at CIR.zig:648)
// Do NOT call full deinit on imports store as the items list points into the buffer!
self.env.imports.map.deinit(self.gpa);
// Free the buffer (all other data structures point into this buffer)
self.gpa.free(self.buffer);
// Free the env struct itself
self.gpa.destroy(self.env);
}
};
/// Deserialize BuiltinIndices from the binary data generated at build time
fn deserializeBuiltinIndices(gpa: std.mem.Allocator, bin_data: []const u8) !can.CIR.BuiltinIndices {
// Copy embedded data to properly aligned memory
const alignment = @alignOf(can.CIR.BuiltinIndices);
const buffer = try gpa.alignedAlloc(u8, @enumFromInt(alignment), bin_data.len);
defer gpa.free(buffer);
@memcpy(buffer, bin_data);
// Cast to the structure
const indices_ptr = @as(*const can.CIR.BuiltinIndices, @ptrCast(buffer.ptr));
return indices_ptr.*;
}
/// Load a compiled ModuleEnv from embedded binary data
fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: []const u8, source: []const u8) !LoadedModule {
// Copy the embedded data to properly aligned memory
// CompactWriter requires specific alignment for serialization
const CompactWriter = collections.CompactWriter;
const buffer = try gpa.alignedAlloc(u8, CompactWriter.SERIALIZATION_ALIGNMENT, bin_data.len);
@memcpy(buffer, bin_data);
// Cast to the serialized structure
const serialized_ptr = @as(
*ModuleEnv.Serialized,
@ptrCast(@alignCast(buffer.ptr)),
);
const env = try gpa.create(ModuleEnv);
errdefer gpa.destroy(env);
// Deserialize
const base_ptr = @intFromPtr(buffer.ptr);
env.* = ModuleEnv{
.gpa = gpa,
.common = serialized_ptr.common.deserialize(@as(i64, @intCast(base_ptr)), source).*,
.types = serialized_ptr.types.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, // Pass gpa to types deserialize
.module_kind = serialized_ptr.module_kind,
.all_defs = serialized_ptr.all_defs,
.all_statements = serialized_ptr.all_statements,
.exports = serialized_ptr.exports,
.builtin_statements = serialized_ptr.builtin_statements,
.external_decls = serialized_ptr.external_decls.deserialize(@as(i64, @intCast(base_ptr))).*,
.imports = (try serialized_ptr.imports.deserialize(@as(i64, @intCast(base_ptr)), gpa)).*,
.module_name = module_name,
.module_name_idx = undefined, // Not used for deserialized modules (only needed during fresh canonicalization)
.diagnostics = serialized_ptr.diagnostics,
.store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*,
.evaluation_order = null,
};
return LoadedModule{
.env = env,
.buffer = buffer,
.gpa = gpa,
};
}
/// REPL state that tracks past definitions and evaluates expressions
pub const Repl = struct {
allocator: Allocator,
/// Map from variable name to source string for definitions
definitions: std.StringHashMap([]const u8),
/// Operations for the Roc runtime
roc_ops: *RocOps,
/// Shared crash context managed by the host (optional)
crash_ctx: ?*CrashContext,
/// Optional trace writer for debugging evaluation
//trace_writer: ?std.io.AnyWriter,
/// ModuleEnv from last successful evaluation (for snapshot generation)
last_module_env: ?*ModuleEnv,
/// Debug flag to store rendered HTML for snapshot generation
debug_store_snapshots: bool,
/// Storage for rendered CAN HTML at each step (only when debug_store_snapshots is true)
debug_can_html: std.array_list.Managed([]const u8),
/// Storage for rendered TYPES HTML at each step (only when debug_store_snapshots is true)
debug_types_html: std.array_list.Managed([]const u8),
/// Builtin type declaration indices (loaded once at startup from builtin_indices.bin)
builtin_indices: can.CIR.BuiltinIndices,
/// Loaded Builtin module (loaded once at startup)
builtin_module: LoadedModule,
pub fn init(allocator: Allocator, roc_ops: *RocOps, crash_ctx: ?*CrashContext) !Repl {
const compiled_builtins = @import("compiled_builtins");
// Load builtin indices once at startup (generated at build time)
const builtin_indices = try deserializeBuiltinIndices(allocator, compiled_builtins.builtin_indices_bin);
// Load Builtin module once at startup
const builtin_source = compiled_builtins.builtin_source;
var builtin_module = try loadCompiledModule(allocator, compiled_builtins.builtin_bin, "Builtin", builtin_source);
errdefer builtin_module.deinit();
return Repl{
.allocator = allocator,
.definitions = std.StringHashMap([]const u8).init(allocator),
.roc_ops = roc_ops,
.crash_ctx = crash_ctx,
//.trace_writer = null,
.last_module_env = null,
.debug_store_snapshots = false,
.debug_can_html = std.array_list.Managed([]const u8).init(allocator),
.debug_types_html = std.array_list.Managed([]const u8).init(allocator),
.builtin_indices = builtin_indices,
.builtin_module = builtin_module,
};
}
// pub fn setTraceWriter(self: *Repl, trace_writer: std.io.AnyWriter) void {
// self.trace_writer = trace_writer;
// }
/// Enable debug mode to store snapshot HTML for each REPL step
pub fn enableDebugSnapshots(self: *Repl) void {
self.debug_store_snapshots = true;
}
/// Get pointer to last ModuleEnv for snapshot generation
pub fn getLastModuleEnv(self: *Repl) ?*ModuleEnv {
return self.last_module_env;
}
/// Get debug CAN HTML for all steps (only available when debug_store_snapshots is enabled)
pub fn getDebugCanHtml(self: *Repl) []const []const u8 {
return self.debug_can_html.items;
}
/// Get debug TYPES HTML for all steps (only available when debug_store_snapshots is enabled)
pub fn getDebugTypesHtml(self: *Repl) []const []const u8 {
return self.debug_types_html.items;
}
/// Allocate a new ModuleEnv and save it
fn allocateModuleEnv(self: *Repl, source: []const u8) !*ModuleEnv {
// Clean up previous ModuleEnv if it exists
if (self.last_module_env) |old_env| {
old_env.deinit();
self.allocator.destroy(old_env);
}
// Allocate new ModuleEnv on heap
const new_env = try self.allocator.create(ModuleEnv);
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
new_env.* = try ModuleEnv.init(self.allocator, source);
self.last_module_env = new_env;
return new_env;
}
/// Generate and store CAN and TYPES HTML for debugging
fn generateAndStoreDebugHtml(self: *Repl, module_env: *ModuleEnv, expr_idx: can.CIR.Expr.Idx) !void {
const SExprTree = @import("base").SExprTree;
// Generate CAN HTML
{
var tree = SExprTree.init(self.allocator);
defer tree.deinit();
try module_env.pushToSExprTree(expr_idx, &tree);
var can_buffer = std.ArrayList(u8).empty;
defer can_buffer.deinit(self.allocator);
try tree.toStringPretty(can_buffer.writer(self.allocator).any(), .include_linecol);
const can_html = try self.allocator.dupe(u8, can_buffer.items);
try self.debug_can_html.append(can_html);
}
// Generate TYPES HTML
{
var tree = SExprTree.init(self.allocator);
defer tree.deinit();
try module_env.pushTypesToSExprTree(expr_idx, &tree);
var types_buffer = std.ArrayList(u8).empty;
defer types_buffer.deinit(self.allocator);
try tree.toStringPretty(types_buffer.writer(self.allocator).any(), .include_linecol);
const types_html = try self.allocator.dupe(u8, types_buffer.items);
try self.debug_types_html.append(types_html);
}
}
/// Add or replace a definition in the REPL context
pub fn addOrReplaceDefinition(self: *Repl, source: []const u8, var_name: []const u8) !void {
// Check if we're replacing an existing definition
if (self.definitions.fetchRemove(var_name)) |kv| {
// Free both the old key and value
self.allocator.free(kv.key);
self.allocator.free(kv.value);
}
// Duplicate both key and value since they're borrowed from input
const owned_key = try self.allocator.dupe(u8, var_name);
const owned_source = try self.allocator.dupe(u8, source);
try self.definitions.put(owned_key, owned_source);
}
pub fn deinit(self: *Repl) void {
// Clean up definition strings and keys
var iterator = self.definitions.iterator();
while (iterator.next()) |kv| {
self.allocator.free(kv.key_ptr.*); // Free the variable name
self.allocator.free(kv.value_ptr.*); // Free the source string
}
self.definitions.deinit();
// Clean up debug HTML storage
for (self.debug_can_html.items) |html| {
self.allocator.free(html);
}
self.debug_can_html.deinit();
for (self.debug_types_html.items) |html| {
self.allocator.free(html);
}
self.debug_types_html.deinit();
// Clean up last ModuleEnv if it exists
if (self.last_module_env) |module_env| {
module_env.deinit();
self.allocator.destroy(module_env);
}
// Clean up loaded builtin module
self.builtin_module.deinit();
}
/// Process a line of input and return the result
pub fn step(self: *Repl, line: []const u8) ![]const u8 {
const trimmed = std.mem.trim(u8, line, " \t\n\r");
// Handle special commands
if (trimmed.len == 0) {
return try self.allocator.dupe(u8, "");
}
if (std.mem.eql(u8, trimmed, ":help")) {
return try self.allocator.dupe(u8,
\\Enter an expression to evaluate, or a definition (like x = 1) to use later.
\\
\\ - :q quits
\\ - :help shows this text again
);
}
if (std.mem.eql(u8, trimmed, ":exit") or
std.mem.eql(u8, trimmed, ":quit") or
std.mem.eql(u8, trimmed, ":q") or
std.mem.eql(u8, trimmed, "exit") or
std.mem.eql(u8, trimmed, "quit") or
std.mem.eql(u8, trimmed, "exit()") or
std.mem.eql(u8, trimmed, "quit()"))
{
return try self.allocator.dupe(u8, "Goodbye!");
}
// Process the input
return try self.processInput(trimmed);
}
/// Process regular input (not special commands)
fn processInput(self: *Repl, input: []const u8) ![]const u8 {
// Try to parse as a statement first
const parse_result = try self.tryParseStatement(input);
switch (parse_result) {
.assignment => |info| {
// Add or replace definition (duplicates the strings for ownership)
try self.addOrReplaceDefinition(info.source, info.var_name);
// Return descriptive output for assignments
return try std.fmt.allocPrint(self.allocator, "assigned `{s}`", .{info.var_name});
},
.import => {
// Imports are not supported in this implementation
return try self.allocator.dupe(u8, "Imports not yet supported");
},
.expression => {
// Evaluate expression with all past definitions
const full_source = try self.buildFullSource(input);
defer self.allocator.free(full_source);
return try self.evaluateSource(full_source);
},
.type_decl => {
// Type declarations can't be evaluated
return try self.allocator.dupe(u8, "");
},
.parse_error => |msg| {
defer self.allocator.free(msg);
return try std.fmt.allocPrint(self.allocator, "Parse error: {s}", .{msg});
},
}
}
const ParseResult = union(enum) {
assignment: struct {
source: []const u8, // Borrowed from input
var_name: []const u8, // Borrowed from input
},
import,
expression,
type_decl,
parse_error: []const u8, // Must be allocator.dupe'd
};
/// Try to parse input as a statement
fn tryParseStatement(self: *Repl, input: []const u8) !ParseResult {
var arena = std.heap.ArenaAllocator.init(self.allocator);
defer arena.deinit();
var module_env = try ModuleEnv.init(self.allocator, input);
defer module_env.deinit();
// Try statement parsing
if (parse.parseStatement(&module_env.common, self.allocator)) |ast_const| {
var ast = ast_const;
defer ast.deinit(self.allocator);
if (ast.root_node_idx != 0) {
const stmt_idx: AST.Statement.Idx = @enumFromInt(ast.root_node_idx);
const stmt = ast.store.getStatement(stmt_idx);
switch (stmt) {
.decl => |decl| {
const pattern = ast.store.getPattern(decl.pattern);
if (pattern == .ident) {
// Extract the identifier name from the pattern
const ident_tok = pattern.ident.ident_tok;
const token_region = ast.tokens.resolve(ident_tok);
const ident_name = module_env.common.source[token_region.start.offset..token_region.end.offset];
// Return borrowed strings (no duplication needed)
return ParseResult{ .assignment = .{
.source = input,
.var_name = ident_name,
} };
}
return ParseResult.expression;
},
.import => return ParseResult.import,
.type_decl => return ParseResult.type_decl,
else => return ParseResult.expression,
}
}
} else |_| {
// Statement parse failed, continue to try expression parsing
}
// Try expression parsing
if (parse.parseExpr(&module_env.common, self.allocator)) |ast_const| {
var ast = ast_const;
defer ast.deinit(self.allocator);
if (ast.root_node_idx != 0) {
return ParseResult.expression;
}
} else |_| {
// Expression parse failed too
}
return ParseResult{ .parse_error = try self.allocator.dupe(u8, "Failed to parse input") };
}
/// Build full source including all definitions wrapped in block syntax
pub fn buildFullSource(self: *Repl, current_expr: []const u8) ![]const u8 {
// If no definitions exist, just return the expression as-is
if (self.definitions.count() == 0) {
return try self.allocator.dupe(u8, current_expr);
}
var buffer = std.ArrayList(u8).empty;
errdefer buffer.deinit(self.allocator);
// Start block
try buffer.appendSlice(self.allocator, "{\n");
// Add all definitions in order
var iterator = self.definitions.iterator();
while (iterator.next()) |kv| {
try buffer.appendSlice(self.allocator, " ");
try buffer.appendSlice(self.allocator, kv.value_ptr.*);
try buffer.append(self.allocator, '\n');
}
// Add current expression
try buffer.appendSlice(self.allocator, " ");
try buffer.appendSlice(self.allocator, current_expr);
try buffer.append(self.allocator, '\n');
// End block
try buffer.append(self.allocator, '}');
return try buffer.toOwnedSlice(self.allocator);
}
/// Evaluate source code
fn evaluateSource(self: *Repl, source: []const u8) ![]const u8 {
const module_env = try self.allocateModuleEnv(source);
return try self.evaluatePureExpression(module_env);
}
/// Evaluate a program (which may contain definitions)
fn evaluatePureExpression(self: *Repl, module_env: *ModuleEnv) ![]const u8 {
// Determine if we have definitions (which means we built a block expression)
const has_definitions = self.definitions.count() > 0;
// Parse appropriately based on whether we have definitions
var parse_ast = if (has_definitions)
// Has definitions - we built a block expression, parse as expression
parse.parseExpr(&module_env.common, self.allocator) catch |err| {
return try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err});
}
else
// No definitions - simple expression, parse as expression
parse.parseExpr(&module_env.common, self.allocator) catch |err| {
return try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err});
};
defer parse_ast.deinit(self.allocator);
// Empty scratch space
parse_ast.store.emptyScratch();
// Create CIR
const cir = module_env; // CIR is now just ModuleEnv
try cir.initCIRFields(self.allocator, "repl");
// Get Bool and Result statement indices from the IMPORTED modules (not copied!)
// These refer to the actual statements in the Bool/Result modules
const bool_stmt_in_bool_module = self.builtin_indices.bool_type;
const result_stmt_in_result_module = self.builtin_indices.result_type;
const module_common_idents: Check.CommonIdents = .{
.module_name = try module_env.insertIdent(base.Ident.for_text("repl")),
.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,
.result_stmt = result_stmt_in_result_module,
};
// Create canonicalizer with nested types available for qualified name resolution
// Register Bool, Result, Str, Dict, and Set individually so qualified access works (e.g., Bool.True)
var module_envs_map = std.AutoHashMap(base.Ident.Idx, can.Can.AutoImportedType).init(self.allocator);
defer module_envs_map.deinit();
const bool_ident = try cir.common.idents.insert(self.allocator, base.Ident.for_text("Bool"));
const result_ident = try cir.common.idents.insert(self.allocator, base.Ident.for_text("Result"));
const str_ident = try cir.common.idents.insert(self.allocator, base.Ident.for_text("Str"));
const dict_ident = try cir.common.idents.insert(self.allocator, base.Ident.for_text("Dict"));
const set_ident = try cir.common.idents.insert(self.allocator, base.Ident.for_text("Set"));
try module_envs_map.put(bool_ident, .{
.env = self.builtin_module.env,
.statement_idx = self.builtin_indices.bool_type,
});
try module_envs_map.put(result_ident, .{
.env = self.builtin_module.env,
.statement_idx = self.builtin_indices.result_type,
});
// Str is added without statement_idx because it's a primitive builtin type
try module_envs_map.put(str_ident, .{
.env = self.builtin_module.env,
});
try module_envs_map.put(dict_ident, .{
.env = self.builtin_module.env,
.statement_idx = self.builtin_indices.dict_type,
});
try module_envs_map.put(set_ident, .{
.env = self.builtin_module.env,
.statement_idx = self.builtin_indices.set_type,
});
var czer = Can.init(cir, &parse_ast, &module_envs_map) catch |err| {
return try std.fmt.allocPrint(self.allocator, "Canonicalize init error: {}", .{err});
};
defer czer.deinit();
// NOTE: True/False/Ok/Err are now just anonymous tags that unify with Bool/Result automatically!
// No need to register unqualified_nominal_tags - the type system handles it.
// Since we're always parsing as expressions now, handle them the same way
const expr_idx: AST.Expr.Idx = @enumFromInt(parse_ast.root_node_idx);
const canonical_expr = try czer.canonicalizeExpr(expr_idx) orelse {
return try self.allocator.dupe(u8, "Canonicalize expr error: expression returned null");
};
const final_expr_idx = canonical_expr.get_idx();
// Type check - Pass Builtin as imported module
const imported_modules = [_]*const ModuleEnv{self.builtin_module.env};
var checker = Check.init(
self.allocator,
&module_env.types,
cir,
&imported_modules,
&module_envs_map,
&cir.store.regions,
module_common_idents,
) catch |err| {
return try std.fmt.allocPrint(self.allocator, "Type check init error: {}", .{err});
};
defer checker.deinit();
// Check the expression (no need to check defs since we're parsing as expressions)
_ = checker.checkExprRepl(final_expr_idx) catch |err| {
return try std.fmt.allocPrint(self.allocator, "Type check expr error: {}", .{err});
};
// Create interpreter instance with BuiltinTypes containing real Builtin module
const builtin_types_for_eval = BuiltinTypes.init(self.builtin_indices, self.builtin_module.env, self.builtin_module.env, self.builtin_module.env);
var interpreter = eval_mod.Interpreter.init(self.allocator, module_env, builtin_types_for_eval, &imported_modules) catch |err| {
return try std.fmt.allocPrint(self.allocator, "Interpreter init error: {}", .{err});
};
defer interpreter.deinitAndFreeOtherEnvs();
if (self.crash_ctx) |ctx| {
ctx.reset();
}
const result = interpreter.evalMinimal(final_expr_idx, self.roc_ops) catch |err| switch (err) {
error.Crash => {
if (self.crash_ctx) |ctx| {
if (ctx.crashMessage()) |msg| {
return try std.fmt.allocPrint(self.allocator, "Crash: {s}", .{msg});
}
}
return try self.allocator.dupe(u8, "Evaluation error: error.Crash");
},
else => return try std.fmt.allocPrint(self.allocator, "Evaluation error: {}", .{err}),
};
// Generate debug HTML if enabled
if (self.debug_store_snapshots) {
try self.generateAndStoreDebugHtml(module_env, final_expr_idx);
}
const expr_ct_var = can.ModuleEnv.varFrom(final_expr_idx);
const output = blk: {
const expr_rt_var = interpreter.translateTypeVar(module_env, expr_ct_var) catch {
break :blk try interpreter.renderValueRoc(result);
};
break :blk try interpreter.renderValueRocWithType(result, expr_rt_var);
};
result.decref(&interpreter.runtime_layout_store, self.roc_ops);
if (result.layout.tag == .record) {
self.allocator.free(output);
return try self.allocator.dupe(u8, "<record>");
}
return output;
}
};