diff --git a/src/playground_wasm/README.md b/src/playground_wasm/README.md index e82a1c1df8..ce714f4dce 100644 --- a/src/playground_wasm/README.md +++ b/src/playground_wasm/README.md @@ -185,7 +185,24 @@ All communication uses JSON messages. Each message must have a `type` field. } ``` -#### 7. GET_HOVER_INFO +#### 7. QUERY_FORMATTED +**State**: LOADED +**Purpose**: Get formatted Roc code + +**Request**: +```json +{"type": "QUERY_FORMATTED"} +``` + +**Response**: +```json +{ + "status": "SUCCESS", + "data": "module [foo]\n\nfoo = 42\n\nbar = \"baz\"\n" +} +``` + +#### 8. GET_HOVER_INFO **State**: LOADED or REPL_ACTIVE **Purpose**: Get hover information for an identifier at a specific source position @@ -219,7 +236,7 @@ All communication uses JSON messages. Each message must have a `type` field. **Note**: In REPL_ACTIVE state, this works with the last REPL evaluation's ModuleEnv. -#### 8. INIT_REPL +#### 9. INIT_REPL **State**: READY → REPL_ACTIVE **Purpose**: Initialize a REPL session for interactive evaluation @@ -240,7 +257,7 @@ All communication uses JSON messages. Each message must have a `type` field. } ``` -#### 9. REPL_STEP +#### 10. REPL_STEP **State**: REPL_ACTIVE **Purpose**: Submit a line of input to the REPL for evaluation @@ -308,10 +325,10 @@ All communication uses JSON messages. Each message must have a `type` field. - `error_details`: Additional error details, typically the specific error name or message **Compiler Availability**: -- `compiler_available: true`: Compiler queries (QUERY_CIR, QUERY_TYPES, GET_HOVER_INFO) are available +- `compiler_available: true`: Compiler queries (QUERY_CIR, QUERY_TYPES, QUERY_FORMATTED, GET_HOVER_INFO) are available - `compiler_available: false`: Compiler queries are not available (usually due to errors) -#### 10. CLEAR_REPL +#### 11. CLEAR_REPL **State**: REPL_ACTIVE **Purpose**: Clear all REPL definitions while keeping the REPL session active @@ -332,7 +349,7 @@ All communication uses JSON messages. Each message must have a `type` field. } ``` -#### 11. RESET +#### 12. RESET **State**: LOADED or REPL_ACTIVE → READY **Purpose**: Clean up compilation state and return to READY @@ -403,10 +420,10 @@ The playground now supports interactive REPL (Read-Eval-Print Loop) sessions tha When in `REPL_ACTIVE` state, the following compiler query messages work with the last REPL evaluation: - `QUERY_CIR`: Returns CIR for the last REPL evaluation -- `QUERY_TYPES`: Returns types for the last REPL evaluation +- `QUERY_TYPES`: Returns types for the last REPL evaluation - `GET_HOVER_INFO`: Returns hover info for the last REPL evaluation -These queries are only available when `compiler_available: true` in the REPL response. +- These queries are only available when `compiler_available: true` in the REPL response. ## Integration Notes diff --git a/src/playground_wasm/main.zig b/src/playground_wasm/main.zig index 66ea268391..0025c55a96 100644 --- a/src/playground_wasm/main.zig +++ b/src/playground_wasm/main.zig @@ -26,6 +26,7 @@ const compile = @import("compile"); const can = @import("can"); const check = @import("check"); const unbundle = @import("unbundle"); +const fmt = @import("fmt"); const WasmFilesystem = @import("WasmFilesystem.zig"); const Can = can.Can; @@ -58,6 +59,7 @@ const MessageType = enum { QUERY_AST, QUERY_CIR, QUERY_TYPES, + QUERY_FORMATTED, GET_HOVER_INFO, RESET, INIT_REPL, @@ -71,6 +73,7 @@ const MessageType = enum { if (std.mem.eql(u8, str, "QUERY_AST")) return .QUERY_AST; if (std.mem.eql(u8, str, "QUERY_CIR")) return .QUERY_CIR; if (std.mem.eql(u8, str, "QUERY_TYPES")) return .QUERY_TYPES; + if (std.mem.eql(u8, str, "QUERY_FORMATTED")) return .QUERY_FORMATTED; if (std.mem.eql(u8, str, "GET_HOVER_INFO")) return .GET_HOVER_INFO; if (std.mem.eql(u8, str, "RESET")) return .RESET; if (std.mem.eql(u8, str, "INIT_REPL")) return .INIT_REPL; @@ -123,6 +126,7 @@ const CompilerStageData = struct { // Pre-canonicalization HTML representations tokens_html: ?[]const u8 = null, ast_html: ?[]const u8 = null, + formatted_code: ?[]const u8 = null, // Diagnostic reports from each stage tokenize_reports: std.ArrayList(reporting.Report), @@ -149,6 +153,7 @@ const CompilerStageData = struct { // Free pre-generated HTML if (self.tokens_html) |html| allocator.free(html); if (self.ast_html) |html| allocator.free(html); + if (self.formatted_code) |code| allocator.free(code); // Deinit reports, which may reference data in the AST or ModuleEnv for (self.tokenize_reports.items) |*report| { @@ -240,6 +245,20 @@ var debug_log_buffer: [4096]u8 = undefined; var debug_log_pos: usize = 0; var debug_log_oom: bool = false; +/// Reset all global state and allocator +fn resetGlobalState() void { + // Make sure everything is null + compiler_data = null; + repl_instance = null; + host_message_buffer = null; + host_response_buffer = null; + repl_instance = null; + repl_roc_ops = null; + + // Reset allocator to clear all allocations + fba.reset(); +} + /// Writes a formatted string to the in-memory debug log. fn logDebug(comptime format: []const u8, args: anytype) void { if (debug_log_oom) { @@ -408,6 +427,7 @@ fn wasmRocCrashed(crashed_args: *const builtins.host_abi.RocCrashed, _: *anyopaq /// Initialize the WASM module in START state export fn init() void { + // For the very first initialization, we can reset the allocator fba = std.heap.FixedBufferAllocator.init(&wasm_heap_memory); allocator = fba.allocator(); @@ -419,19 +439,10 @@ export fn init() void { // Clean up REPL state cleanupReplState(); - // Clean up any existing buffers - if (host_message_buffer) |buf| { - allocator.free(buf); - host_message_buffer = null; - } - if (host_response_buffer) |buf| { - allocator.free(buf); - host_response_buffer = null; - } - if (last_error) |err| { - allocator.free(err); - last_error = null; - } + // Initialize buffer pointers to null + host_message_buffer = null; + host_response_buffer = null; + last_error = null; } /// Allocate a buffer for incoming messages from the host. @@ -558,7 +569,6 @@ fn handleReadyState(message_type: MessageType, root: std.json.Value, response_bu } // Compile the source through all stages - // Compile and return result const result = compileSource(source) catch |err| { try writeErrorResponse(response_buffer, .ERROR, @errorName(err)); return; @@ -600,27 +610,7 @@ fn handleReadyState(message_type: MessageType, root: std.json.Value, response_bu try writeReplInitResponse(response_buffer); }, .RESET => { - // A RESET message should clean up all compilation-related memory. - if (compiler_data) |*old_data| { - old_data.deinit(); - compiler_data = null; - } - // Also free the host-managed buffers, as they are part of the old state. - if (host_message_buffer) |buf| { - allocator.free(buf); - host_message_buffer = null; - } - if (host_response_buffer) |buf| { - allocator.free(buf); - host_response_buffer = null; - } - - // Clean up REPL state - cleanupReplState(); - - // Now, fully reset the allocator to prevent fragmentation. - fba = std.heap.FixedBufferAllocator.init(&wasm_heap_memory); - allocator = fba.allocator(); + resetGlobalState(); current_state = .READY; @@ -653,28 +643,14 @@ fn handleLoadedState(message_type: MessageType, message_json: std.json.Value, re .QUERY_TYPES => { try writeTypesResponse(response_buffer, data); }, + .QUERY_FORMATTED => { + try writeFormattedResponse(response_buffer, data); + }, .GET_HOVER_INFO => { try writeHoverInfoResponse(response_buffer, data, message_json); }, .RESET => { - // A RESET message should clean up all compilation-related memory. - if (compiler_data) |*old_data| { - old_data.deinit(); - compiler_data = null; - } - // Also free the host-managed buffers, as they are part of the old state. - if (host_message_buffer) |buf| { - allocator.free(buf); - host_message_buffer = null; - } - if (host_response_buffer) |buf| { - allocator.free(buf); - host_response_buffer = null; - } - - // Now, fully reset the allocator to prevent fragmentation. - fba = std.heap.FixedBufferAllocator.init(&wasm_heap_memory); - allocator = fba.allocator(); + resetGlobalState(); current_state = .READY; @@ -737,22 +713,7 @@ fn handleReplState(message_type: MessageType, root: std.json.Value, response_buf try writeReplClearResponse(response_buffer); }, .RESET => { - // Clean up REPL state - cleanupReplState(); - - // Also free the host-managed buffers, as they are part of the old state. - if (host_message_buffer) |buf| { - allocator.free(buf); - host_message_buffer = null; - } - if (host_response_buffer) |buf| { - allocator.free(buf); - host_response_buffer = null; - } - - // Now, fully reset the allocator to prevent fragmentation. - fba = std.heap.FixedBufferAllocator.init(&wasm_heap_memory); - allocator = fba.allocator(); + resetGlobalState(); current_state = .READY; @@ -769,9 +730,9 @@ fn handleReplState(message_type: MessageType, root: std.json.Value, response_buf // Write CIR response directly using the REPL's module env try writeReplCanCirResponse(response_buffer, module_env); }, - .QUERY_TYPES, .GET_HOVER_INFO => { - // These queries need type information which isn't readily available in REPL mode - try writeErrorResponse(response_buffer, .ERROR, "Type queries not available in REPL mode"); + .QUERY_TYPES, .QUERY_FORMATTED, .GET_HOVER_INFO => { + // These queries need parse/type information which isn't readily available in REPL mode + try writeErrorResponse(response_buffer, .ERROR, "Parse/type queries not available in REPL mode"); }, else => { try writeErrorResponse(response_buffer, .INVALID_STATE, "Invalid message type for REPL state"); @@ -830,7 +791,7 @@ fn compileSource(source: []const u8) !CompilerStageData { }; // Generate AST HTML - var ast_html_buffer = std.ArrayList(u8).init(allocator); + var ast_html_buffer = std.ArrayList(u8).init(temp_alloc); const ast_writer = ast_html_buffer.writer().any(); { const file = parse_ast.store.getFile(); @@ -842,9 +803,23 @@ fn compileSource(source: []const u8) !CompilerStageData { try tree.toHtml(ast_writer); } - // the AST HTML is stored in our heap and will be cleaned up when the module is RESET - // no need to free it here, we will re-use whenever the AST is queried - result.ast_html = ast_html_buffer.items; + + result.ast_html = allocator.dupe(u8, ast_html_buffer.items) catch |err| { + logDebug("compileSource: failed to dupe ast_html: {}\n", .{err}); + return err; + }; + + // Generate formatted code + var formatted_code_buffer = std.ArrayList(u8).init(temp_alloc); + fmt.formatAst(parse_ast, formatted_code_buffer.writer().any()) catch |err| { + logDebug("compileSource: formatAst failed: {}\n", .{err}); + return err; + }; + + result.formatted_code = allocator.dupe(u8, formatted_code_buffer.items) catch |err| { + logDebug("compileSource: failed to dupe formatted_code: {}\n", .{err}); + return err; + }; // Collect tokenize diagnostics with additional error handling for (parse_ast.tokenize_diagnostics.items) |diagnostic| { @@ -1265,6 +1240,24 @@ fn writeParseAstResponse(response_buffer: []u8, data: CompilerStageData) Respons try resp_writer.finalize(); } +/// Write formatted response with formatted Roc code +fn writeFormattedResponse(response_buffer: []u8, data: CompilerStageData) ResponseWriteError!void { + var resp_writer = ResponseWriter{ .buffer = response_buffer }; + resp_writer.pos = @sizeOf(u32); + const w = resp_writer.writer(); + + try w.writeAll("{\"status\":\"SUCCESS\",\"data\":\""); + + if (data.formatted_code) |formatted| { + try writeJsonString(w, formatted); + } else { + try writeJsonString(w, "Formatted code not available"); + } + + try w.writeAll("\"}"); + try resp_writer.finalize(); +} + /// Write canonicalized CIR response for REPL mode using ModuleEnv directly fn writeReplCanCirResponse(response_buffer: []u8, module_env: *ModuleEnv) ResponseWriteError!void { var resp_writer = ResponseWriter{ .buffer = response_buffer }; diff --git a/test/playground-integration/main.zig b/test/playground-integration/main.zig index 16fb4b67c8..6654fc7e17 100644 --- a/test/playground-integration/main.zig +++ b/test/playground-integration/main.zig @@ -766,6 +766,16 @@ fn runTestSteps(allocator: std.mem.Allocator, wasm_interface: *WasmInterface, te } else { logDebug(" Step {}: QUERY_TYPES successful. Status: {s}. No type data returned.\n", .{ i + 1, response.status }); } + } else if (std.mem.eql(u8, step.message.type, "QUERY_FORMATTED")) { + if (response.data) |data| { + if (data.len > 0) { + logDebug(" Step {}: QUERY_FORMATTED successful. Status: {s}. Formatted code retrieved ({} chars).\n", .{ i + 1, response.status, data.len }); + } else { + logDebug(" Step {}: QUERY_FORMATTED successful. Status: {s}. Empty formatted code.\n", .{ i + 1, response.status }); + } + } else { + logDebug(" Step {}: QUERY_FORMATTED successful. Status: {s}. No formatted data returned.\n", .{ i + 1, response.status }); + } } else { logDebug(" Step {}: {s} successful. Status: {s}, Message: {?s}\n", .{ i + 1, step.message.type, response.status, response.message }); } @@ -919,7 +929,7 @@ pub fn main() !void { defer test_cases.deinit(); // This will free the TestCase structs and their `steps` slices. // Functional Test - var happy_path_steps = try allocator.alloc(MessageStep, 7); + var happy_path_steps = try allocator.alloc(MessageStep, 8); // Check that INIT returns the compiler version (both test and WASM are built with same options) happy_path_steps[0] = .{ .message = .{ .type = "INIT" }, .expected_status = "SUCCESS", .expected_message_contains = build_options.compiler_version }; const happy_path_code = try TestData.happyPathRocCode(allocator); @@ -951,6 +961,11 @@ pub fn main() !void { .expected_data_contains = "inferred-types", }; happy_path_steps[6] = .{ + .message = .{ .type = "QUERY_FORMATTED" }, + .expected_status = "SUCCESS", + .expected_data_contains = "foo", + }; + happy_path_steps[7] = .{ .message = .{ .type = "GET_HOVER_INFO", .identifier = "foo", .line = 3, .ch = 1 }, .expected_status = "SUCCESS", .expected_hover_info_contains = "Str", @@ -971,6 +986,26 @@ pub fn main() !void { const empty_source_code = try allocator.dupe(u8, ""); try test_cases.append(try createSimpleTest(allocator, "Empty Source Code", empty_source_code, null, false)); // Disable diagnostic expectations + // Code Formatting Test + var formatted_test_steps = try allocator.alloc(MessageStep, 3); + formatted_test_steps[0] = .{ .message = .{ .type = "INIT" }, .expected_status = "SUCCESS" }; + const unformatted_code = try allocator.dupe(u8, "module [foo]\n\nfoo=42\nbar=\"hello world\"\n"); + formatted_test_steps[1] = .{ + .message = .{ .type = "LOAD_SOURCE", .source = unformatted_code }, + .expected_status = "SUCCESS", + .expected_message_contains = "LOADED", + .owned_source = unformatted_code, + }; + formatted_test_steps[2] = .{ + .message = .{ .type = "QUERY_FORMATTED" }, + .expected_status = "SUCCESS", + .expected_data_contains = "foo", + }; + try test_cases.append(.{ + .name = "QUERY_FORMATTED - Code Formatting", + .steps = formatted_test_steps, + }); + // Invalid Message Type Test var invalid_msg_type_steps = try allocator.alloc(MessageStep, 2); invalid_msg_type_steps[0] = .{ .message = .{ .type = "INIT" }, .expected_status = "SUCCESS" };