mirror of
https://github.com/roc-lang/roc.git
synced 2025-12-23 08:48:03 +00:00
Merge pull request #8229 from FabHof/formatter_playground
Add QUERY_FORMATTED to wasm playground
This commit is contained in:
commit
480bbd6a62
3 changed files with 130 additions and 85 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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" };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue