Flash 176 lines
// 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)
}