//! 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 builtin_loading = eval_mod.builtin_loading; const collections = @import("collections"); const reporting = @import("reporting"); const AST = parse.AST; const Allocator = std.mem.Allocator; const ModuleEnv = can.ModuleEnv; const RocOps = builtins.host_abi.RocOps; const LoadedModule = builtin_loading.LoadedModule; /// 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 builtin_loading.deserializeBuiltinIndices(allocator, compiled_builtins.builtin_indices_bin); // Load Builtin module once at startup const builtin_source = compiled_builtins.builtin_source; var builtin_module = try builtin_loading.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 structured result data. /// This is the preferred API for programmatic use (e.g., playground, tests). pub fn stepStructured(self: *Repl, line: []const u8) !StepResult { const trimmed = std.mem.trim(u8, line, " \t\n\r"); // Handle special commands if (trimmed.len == 0) { return .empty; } if (std.mem.eql(u8, trimmed, ":help")) { return .{ .help = 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 .quit; } // Process the input return try self.processInputStructured(trimmed); } /// Process a line of input and return the result as a string. /// This is a convenience wrapper for CLI REPL use. /// For programmatic use, prefer stepStructured() which returns typed data. pub fn step(self: *Repl, line: []const u8) ![]const u8 { const result = try self.stepStructured(line); return switch (result) { .expression => |s| s, .definition => |s| s, .help => |s| s, .parse_error => |s| s, .canonicalize_error => |s| s, .type_error => |s| s, .eval_error => |s| s, .quit => try self.allocator.dupe(u8, "Goodbye!"), .empty => try self.allocator.dupe(u8, ""), }; } /// Process regular input (not special commands) - returns structured result fn processInputStructured(self: *Repl, input: []const u8) !StepResult { // 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 .{ .definition = try std.fmt.allocPrint(self.allocator, "assigned `{s}`", .{info.var_name}) }; }, .import => { // Imports are not supported in this implementation return .{ .parse_error = 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.evaluateSourceStructured(full_source); }, .type_decl => { // Type declarations can't be evaluated return .empty; }, .parse_error => |msg| { defer self.allocator.free(msg); return .{ .parse_error = try std.fmt.allocPrint(self.allocator, "Parse error: {s}", .{msg}) }; }, } } /// Process regular input (not special commands) - returns string (legacy API) fn processInput(self: *Repl, input: []const u8) ![]const u8 { const result = try self.processInputStructured(input); return switch (result) { .expression => |s| s, .definition => |s| s, .help => |s| s, .parse_error => |s| s, .canonicalize_error => |s| s, .type_error => |s| s, .eval_error => |s| s, .quit => try self.allocator.dupe(u8, "Goodbye!"), .empty => try self.allocator.dupe(u8, ""), }; } 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 }; /// The result of a REPL step - structured data that callers can use directly /// without parsing human-readable strings. pub const StepResult = union(enum) { /// Successfully evaluated an expression, contains the rendered value expression: []const u8, /// Successfully defined a variable definition: []const u8, /// Help text requested help: []const u8, /// User requested quit quit, /// Empty input empty, /// Parse error with rendered message parse_error: []const u8, /// Canonicalization error with rendered message canonicalize_error: []const u8, /// Type checking error with rendered message type_error: []const u8, /// Evaluation/runtime error with rendered message eval_error: []const u8, pub fn deinit(self: StepResult, allocator: Allocator) void { switch (self) { .expression => |s| allocator.free(s), .definition => |s| allocator.free(s), .help => |s| allocator.free(s), .parse_error => |s| allocator.free(s), .canonicalize_error => |s| allocator.free(s), .type_error => |s| allocator.free(s), .eval_error => |s| allocator.free(s), .quit, .empty => {}, } } /// Returns true if this result represents an error pub fn isError(self: StepResult) bool { return switch (self) { .parse_error, .canonicalize_error, .type_error, .eval_error => true, else => false, }; } /// Get the message/output for display, if any pub fn getMessage(self: StepResult) ?[]const u8 { return switch (self) { .expression => |s| s, .definition => |s| s, .help => |s| s, .parse_error => |s| s, .canonicalize_error => |s| s, .type_error => |s| s, .eval_error => |s| s, .quit, .empty => null, }; } }; /// 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 - returns structured result fn evaluateSourceStructured(self: *Repl, source: []const u8) !StepResult { const module_env = try self.allocateModuleEnv(source); return try self.evaluatePureExpressionStructured(module_env); } /// Evaluate source code - returns string (legacy API) fn evaluateSource(self: *Repl, source: []const u8) ![]const u8 { const result = try self.evaluateSourceStructured(source); return switch (result) { .expression => |s| s, .definition => |s| s, .help => |s| s, .parse_error => |s| s, .canonicalize_error => |s| s, .type_error => |s| s, .eval_error => |s| s, .quit => try self.allocator.dupe(u8, "Goodbye!"), .empty => try self.allocator.dupe(u8, ""), }; } /// 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); // Check for parse errors and render them if (parse_ast.hasErrors()) { // Render the first error as the error message if (parse_ast.tokenize_diagnostics.items.len > 0) { var report = try parse_ast.tokenizeDiagnosticToReport( parse_ast.tokenize_diagnostics.items[0], self.allocator, null, ); defer report.deinit(); var output = std.array_list.Managed(u8).init(self.allocator); var unmanaged = output.moveToUnmanaged(); var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged); report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; unmanaged = writer_alloc.toArrayList(); output = unmanaged.toManaged(self.allocator); return try output.toOwnedSlice(); } else if (parse_ast.parse_diagnostics.items.len > 0) { var report = try parse_ast.parseDiagnosticToReport( &module_env.common, parse_ast.parse_diagnostics.items[0], self.allocator, "repl", ); defer report.deinit(); var output = std.array_list.Managed(u8).init(self.allocator); var unmanaged = output.moveToUnmanaged(); var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged); report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; unmanaged = writer_alloc.toArrayList(); output = unmanaged.toManaged(self.allocator); // Trim trailing newlines from the output and return a properly allocated copy const full_result = try output.toOwnedSlice(); defer self.allocator.free(full_result); const trimmed = std.mem.trimRight(u8, full_result, "\n"); return try self.allocator.dupe(u8, trimmed); } } // Empty scratch space parse_ast.store.emptyScratch(); // Create CIR const cir = module_env; // CIR is now just ModuleEnv try cir.initCIRFields("repl"); // Get Bool, Try, and Str statement indices from the IMPORTED modules (not copied!) // These refer to the actual statements in the Builtin module const bool_stmt_in_bool_module = self.builtin_indices.bool_type; const try_stmt_in_try_module = self.builtin_indices.try_type; const str_stmt_in_builtin_module = self.builtin_indices.str_type; const module_builtin_ctx: Check.BuiltinContext = .{ .module_name = try module_env.insertIdent(base.Ident.for_text("repl")), .bool_stmt = bool_stmt_in_bool_module, .try_stmt = try_stmt_in_try_module, .str_stmt = str_stmt_in_builtin_module, .builtin_module = self.builtin_module.env, .builtin_indices = self.builtin_indices, }; // Create canonicalizer with nested types available for qualified name resolution // Populate all auto-imported builtin types using the shared helper to keep behavior consistent var module_envs_map = std.AutoHashMap(base.Ident.Idx, can.Can.AutoImportedType).init(self.allocator); defer module_envs_map.deinit(); try Can.populateModuleEnvs( &module_envs_map, module_env, self.builtin_module.env, self.builtin_indices, ); 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/Try 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 { // Check for diagnostics and render them as error messages const diagnostics = try module_env.getDiagnostics(); if (diagnostics.len > 0) { // Render the first diagnostic as the error message const diagnostic = diagnostics[0]; var report = try module_env.diagnosticToReport(diagnostic, self.allocator, "repl"); defer report.deinit(); var output = std.array_list.Managed(u8).init(self.allocator); var unmanaged = output.moveToUnmanaged(); var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged); report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; unmanaged = writer_alloc.toArrayList(); output = unmanaged.toManaged(self.allocator); return try output.toOwnedSlice(); } 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}; // Resolve imports - map each import to its index in imported_modules // This uses the same resolution logic as compile_package.zig module_env.imports.resolveImports(module_env, &imported_modules); var checker = Check.init( self.allocator, &module_env.types, cir, &imported_modules, &module_envs_map, &cir.store.regions, module_builtin_ctx, ) 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, self.builtin_module.env, &imported_modules, &checker.import_mapping, null) 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.eval(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); } // Use the runtime type from the result value if available (set by e.g. makeBoolValue), // otherwise fall back to translating the compile-time type from the expression. // This is important when the compile-time type is a generic constraint (e.g. from == or !=) // but the runtime type is concrete (e.g. Bool). const output = blk: { if (result.rt_var) |rt_var| { break :blk try interpreter.renderValueRocWithType(result, rt_var, self.roc_ops); } const expr_ct_var = can.ModuleEnv.varFrom(final_expr_idx); 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, self.roc_ops); }; result.decref(&interpreter.runtime_layout_store, self.roc_ops); return output; } /// Evaluate a program (which may contain definitions) - returns structured result fn evaluatePureExpressionStructured(self: *Repl, module_env: *ModuleEnv) !StepResult { // 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) parse.parseExpr(&module_env.common, self.allocator) catch |err| { return .{ .parse_error = try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err}) }; } else parse.parseExpr(&module_env.common, self.allocator) catch |err| { return .{ .parse_error = try std.fmt.allocPrint(self.allocator, "Parse error: {}", .{err}) }; }; defer parse_ast.deinit(self.allocator); // Check for parse errors and render them if (parse_ast.hasErrors()) { if (parse_ast.tokenize_diagnostics.items.len > 0) { var report = try parse_ast.tokenizeDiagnosticToReport( parse_ast.tokenize_diagnostics.items[0], self.allocator, null, ); defer report.deinit(); var output = std.array_list.Managed(u8).init(self.allocator); var unmanaged = output.moveToUnmanaged(); var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged); report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; unmanaged = writer_alloc.toArrayList(); output = unmanaged.toManaged(self.allocator); return .{ .parse_error = try output.toOwnedSlice() }; } else if (parse_ast.parse_diagnostics.items.len > 0) { var report = try parse_ast.parseDiagnosticToReport( &module_env.common, parse_ast.parse_diagnostics.items[0], self.allocator, "repl", ); defer report.deinit(); var output = std.array_list.Managed(u8).init(self.allocator); var unmanaged = output.moveToUnmanaged(); var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged); report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; unmanaged = writer_alloc.toArrayList(); output = unmanaged.toManaged(self.allocator); const full_result = try output.toOwnedSlice(); defer self.allocator.free(full_result); const trimmed = std.mem.trimRight(u8, full_result, "\n"); return .{ .parse_error = try self.allocator.dupe(u8, trimmed) }; } } // Empty scratch space parse_ast.store.emptyScratch(); // Create CIR const cir = module_env; try cir.initCIRFields("repl"); // Populate all auto-imported builtin types using the shared helper to keep behavior consistent var module_envs_map = std.AutoHashMap(base.Ident.Idx, can.Can.AutoImportedType).init(self.allocator); defer module_envs_map.deinit(); try Can.populateModuleEnvs( &module_envs_map, module_env, self.builtin_module.env, self.builtin_indices, ); var czer = Can.init(cir, &parse_ast, &module_envs_map) catch |err| { return .{ .canonicalize_error = try std.fmt.allocPrint(self.allocator, "Canonicalize init error: {}", .{err}) }; }; defer czer.deinit(); const expr_idx: AST.Expr.Idx = @enumFromInt(parse_ast.root_node_idx); const canonical_expr = try czer.canonicalizeExpr(expr_idx) orelse { const diagnostics = try module_env.getDiagnostics(); if (diagnostics.len > 0) { const diagnostic = diagnostics[0]; var report = try module_env.diagnosticToReport(diagnostic, self.allocator, "repl"); defer report.deinit(); var output = std.array_list.Managed(u8).init(self.allocator); var unmanaged = output.moveToUnmanaged(); var writer_alloc = std.Io.Writer.Allocating.fromArrayList(self.allocator, &unmanaged); report.render(&writer_alloc.writer, .markdown) catch |err| switch (err) { error.WriteFailed => return error.OutOfMemory, else => return err, }; unmanaged = writer_alloc.toArrayList(); output = unmanaged.toManaged(self.allocator); return .{ .canonicalize_error = try output.toOwnedSlice() }; } return .{ .canonicalize_error = try self.allocator.dupe(u8, "Canonicalize expr error: expression returned null") }; }; const final_expr_idx = canonical_expr.get_idx(); const imported_modules = [_]*const ModuleEnv{self.builtin_module.env}; // Resolve imports - map each import to its index in imported_modules module_env.imports.resolveImports(module_env, &imported_modules); const builtin_ctx: Check.BuiltinContext = .{ .module_name = try module_env.insertIdent(base.Ident.for_text("repl")), .bool_stmt = self.builtin_indices.bool_type, .try_stmt = self.builtin_indices.try_type, .str_stmt = self.builtin_indices.str_type, .builtin_module = self.builtin_module.env, .builtin_indices = self.builtin_indices, }; var checker = Check.init( self.allocator, &module_env.types, cir, &imported_modules, &module_envs_map, &cir.store.regions, builtin_ctx, ) catch |err| { return .{ .type_error = try std.fmt.allocPrint(self.allocator, "Type check init error: {}", .{err}) }; }; defer checker.deinit(); _ = checker.checkExprRepl(final_expr_idx) catch |err| { return .{ .type_error = try std.fmt.allocPrint(self.allocator, "Type check expr error: {}", .{err}) }; }; // Check for type problems (e.g., type mismatches) if (checker.problems.problems.items.len > 0) { // Return a generic type error message // TODO: Use ReportBuilder to produce a more detailed error message return .{ .type_error = try self.allocator.dupe(u8, "TYPE MISMATCH") }; } 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, self.builtin_module.env, &imported_modules, &checker.import_mapping, null) catch |err| { return .{ .eval_error = try std.fmt.allocPrint(self.allocator, "Interpreter init error: {}", .{err}) }; }; defer interpreter.deinitAndFreeOtherEnvs(); if (self.crash_ctx) |ctx| { ctx.reset(); } const result = interpreter.eval(final_expr_idx, self.roc_ops) catch |err| switch (err) { error.Crash => { if (self.crash_ctx) |ctx| { if (ctx.crashMessage()) |msg| { return .{ .eval_error = try std.fmt.allocPrint(self.allocator, "Crash: {s}", .{msg}) }; } } return .{ .eval_error = try self.allocator.dupe(u8, "Evaluation error: error.Crash") }; }, else => return .{ .eval_error = try std.fmt.allocPrint(self.allocator, "Evaluation error: {}", .{err}) }, }; if (self.debug_store_snapshots) { try self.generateAndStoreDebugHtml(module_env, final_expr_idx); } const output = blk: { if (result.rt_var) |rt_var| { break :blk try interpreter.renderValueRocWithType(result, rt_var, self.roc_ops); } const expr_ct_var = can.ModuleEnv.varFrom(final_expr_idx); 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, self.roc_ops); }; result.decref(&interpreter.runtime_layout_store, self.roc_ops); return .{ .expression = output }; } };