Merge pull request #8229 from FabHof/formatter_playground

Add QUERY_FORMATTED to wasm playground
This commit is contained in:
Luke Boswell 2025-09-06 08:09:42 +10:00 committed by GitHub
commit 480bbd6a62
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 130 additions and 85 deletions

View file

@ -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

View file

@ -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 };

View file

@ -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" };