// check — in-process diagnostics: the selfhost frontend over one open // document, rendered as a `textDocument/publishDiagnostics` notification. // // The compiler is a library here: the lexer and parser (and, on save, // the semantic checker) run directly over the stored document text — no // flashc process is spawned and no error text is parsed back. The // parser's diagnostic carries a line but no column, so its range is the // whole offending line; sema anchors recover line and column through // `locate`, so those get a one-character point range, and a diagnostic's // optional note becomes a second, information-severity entry at its own // location. The parser stops at the first error — one parse diagnostic // per run is the accepted limit until error recovery lands. // // Every run renders a complete notification, empty `diagnostics` array // included: publishing the empty list is what clears stale squiggles // after a fix. Positions are 0-based byte offsets within their line // (the utf-8 position encoding; on the default utf-16 encoding, // non-ASCII lines may render slightly off — accepted for Phase A). use std use core use "parser" use "sema" pub const Error = error{OutOfMemory} // Render the full publishDiagnostics notification body for `uri`: parse // always, sema only when `run_sema` (the on-save pass). pub fn publishJson(arena std.mem.Allocator, uri []u8, src []u8, run_sema bool) Error![]u8 { var out core.list.List(u8) = .empty try out.appendSlice(arena, "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/publishDiagnostics\",\"params\":{\"uri\":") try out.appendSlice(arena, try jsonStr(arena, uri)) try out.appendSlice(arena, ",\"diagnostics\":[") try appendDiagnostics(arena, &out, src, run_sema) try out.appendSlice(arena, "]}}") return out.toOwnedSlice(arena) } fn appendDiagnostics(arena std.mem.Allocator, out *mut core.list.List(u8), src []u8, run_sema bool) Error!void { var p = parser.Parser.init(arena, src) program := p.parseProgram() catch |err| switch err { error.UnexpectedToken => { if p.diag |d| { span := lineSpan(src, d.line) try appendOne(arena, out, d.line - 1, span.start_col, d.line - 1, span.end_col, 1, d.msg) } else { try appendOne(arena, out, 0, 0, 0, 1, 1, "parse error") } return }, error.OutOfMemory => return error.OutOfMemory, } if !run_sema { return } diags := try sema.check(arena, program) for d in diags { loc := sema.locate(src, d.anchor) try appendOne(arena, out, loc.line - 1, loc.col - 1, loc.line - 1, loc.col, 1, d.msg) if d.note_anchor |na| { nloc := sema.locate(src, na) nmsg := d.note_msg orelse "note" try appendOne(arena, out, nloc.line - 1, nloc.col - 1, nloc.line - 1, nloc.col, 3, nmsg) } } } // Append one LSP diagnostic object, comma-separated from a predecessor. fn appendOne(arena std.mem.Allocator, out *mut core.list.List(u8), l0 u32, c0 u32, l1 u32, c1 u32, severity u8, msg []u8) Error!void { if out.items.len > 0 && out.items[out.items.len - 1] == '}' { try out.append(arena, ',') } try out.appendSlice(arena, "{\"range\":{\"start\":{\"line\":") try appendNum(arena, out, l0) try out.appendSlice(arena, ",\"character\":") try appendNum(arena, out, c0) try out.appendSlice(arena, "},\"end\":{\"line\":") try appendNum(arena, out, l1) try out.appendSlice(arena, ",\"character\":") try appendNum(arena, out, c1) try out.appendSlice(arena, "}},\"severity\":") try appendNum(arena, out, severity) try out.appendSlice(arena, ",\"source\":\"flashc\",\"message\":") try out.appendSlice(arena, try jsonStr(arena, msg)) try out.append(arena, '}') } fn appendNum(arena std.mem.Allocator, out *mut core.list.List(u8), n u32) Error!void { var buf [16]u8 = undefined s := core.fmt.bufPrint(buf[0..], "{d}", .{n}) catch unreachable try out.appendSlice(arena, s) } // The 0-based character span of 1-based line `line1`: 0 to the line's // byte length, so a column-less parser diagnostic underlines the whole // line. const Span = struct { start_col u32, end_col u32, } fn lineSpan(src []u8, line1 u32) Span { var line u32 = 1 var start usize = 0 var i usize = 0 while i < src.len && line < line1 { if src[i] == '\n' { line += 1 start = i + 1 } i += 1 } var end usize = start while end < src.len && src[end] != '\n' { end += 1 } var width u32 = #as(u32, #intCast(end - start)) if width == 0 { width = 1 } return .{ .start_col = 0, .end_col = width } } // A JSON string literal (quoted, escaped) for splicing into a response. fn jsonStr(arena std.mem.Allocator, s []u8) Error![]u8 { v := core.json.Value{ .string = s } return core.json.stringify(arena, v) catch return error.OutOfMemory } fn contains(haystack []u8, needle []u8) bool { return core.mem.indexOf(u8, haystack, needle) != null } test "a clean document publishes an empty diagnostics array" { var a = core.arena.ArenaAllocator.init(std.testing.allocator) defer a.deinit() body := try publishJson(a.allocator(), "file:///ok.flash", "fn id(x u32) u32 {\n return x\n}\n", true) try std.testing.expect(contains(body, "\"method\":\"textDocument/publishDiagnostics\"")) try std.testing.expect(contains(body, "\"uri\":\"file:///ok.flash\"")) try std.testing.expect(contains(body, "\"diagnostics\":[]")) } test "a parse error publishes one whole-line diagnostic" { var a = core.arena.ArenaAllocator.init(std.testing.allocator) defer a.deinit() // `const` without a name errors on the `=` token: line 1, and the // column-less parser diagnostic underlines the whole 9-byte line. body := try publishJson(a.allocator(), "file:///bad.flash", "const = 1\n", false) try std.testing.expect(contains(body, "\"severity\":1")) try std.testing.expect(contains(body, "\"source\":\"flashc\"")) try std.testing.expect(contains(body, "\"start\":{\"line\":0,\"character\":0}")) try std.testing.expect(contains(body, "\"end\":{\"line\":0,\"character\":9}")) try std.testing.expect(!contains(body, "\"diagnostics\":[]")) } test "sema runs only when asked" { var a = core.arena.ArenaAllocator.init(std.testing.allocator) defer a.deinit() // Parses fine; the evaluator rejects the definite division by zero. src := "fn f() usize {\n return 1 / 0\n}\n" quiet := try publishJson(a.allocator(), "file:///s.flash", src, false) try std.testing.expect(contains(quiet, "\"diagnostics\":[]")) loud := try publishJson(a.allocator(), "file:///s.flash", src, true) try std.testing.expect(contains(loud, "\"severity\":1")) try std.testing.expect(contains(loud, "\"line\":1")) } test "diagnostic messages arrive JSON-escaped" { var a = core.arena.ArenaAllocator.init(std.testing.allocator) defer a.deinit() body := try publishJson(a.allocator(), "file:///q.flash", "\"unterminated\n", false) // Whatever the message text, the body must stay parseable JSON. v := try core.json.parse(a.allocator(), body) try std.testing.expect(v.get("params") != null) }