finch lsp: the language server, written in Finch

Hover, go to definition, completion (after a dot too: fields, methods, a module's names, an enum's variants,
array/text/map methods), signature help, the outline and errors while typing -- everything the C++ one did,
and the same tests/lsp_test.py passes against it.

It doesn't type-check by itself: for every change it runs  finch index  on the editor's text (--overlay says
which file the text stands in for) and reads what the compiler learned about every name, which is also what
the compiler's own error messages are made of. Half-typed code keeps using the last version that compiled.
src/docs.fch is the one description of the built-ins that hover and completion share.

tests/run.sh runs the LSP test too. 107 passed.
This commit is contained in:
Nexoniarz 2026-10-25 21:48:11 +01:00
commit 636eaefe59
9 changed files with 64467 additions and 48172 deletions

View file

@ -108,6 +108,7 @@ struct Node {
int line
int col
int file
int endLine // block, fn: the line of the closing }
}
struct Program {

109
src/docs.fch Normal file
View file

@ -0,0 +1,109 @@
// One description of every built-in function and method, shared by the compiler (the index the language
// server asks for) and the language server (hover, completion, signature help).
struct Doc {
str name
str signature
str text
}
fn doc(str name, str signature, str text) -> Doc {
return Doc(name: name, signature: signature, text: text)
}
fn builtins() -> []Doc {
return [
doc("print", "print(a, b, ...)", "Prints the values separated by spaces, then a new line. Works with every type."),
doc("input", "input(str question = \"\") -> str", "Shows the question and reads one line typed by the user, without the newline."),
doc("read_file", "read_file(str path) -> str", "The whole file as text. Stops the program if it can't be read, unless handled: read_file(p) or { ... }, try read_file(p)."),
doc("write_file", "write_file(str path, str text) -> bool", "Writes (replaces) the file. true if it worked; with or / try, the reason it didn't."),
doc("file_exists", "file_exists(str path) -> bool", "Is there a file at this path?"),
doc("delete_file", "delete_file(str path) -> bool", "Deletes the file. true if it worked; with or / try, the reason it didn't."),
doc("error", "error(str message)", "The failure a fallible function returns: return error(\"...\"). The caller handles it with or / try."),
doc("shell", "shell(str command) -> int", "Runs a command in the system's shell and gives back its exit code."),
doc("exit", "exit(int code)", "Ends the program right away with this exit code."),
doc("addr", "addr(x) -> ptr[T]", "A pointer to a variable, field or element. addr(function) gives C a callback."),
doc("new", "new(value) -> ptr[T]", "Puts a copy of the value on the heap. Give it back with free(...)."),
doc("free", "free(ptr p)", "Gives back memory from new(...) (or from C), freeing what the value owns."),
doc("str", "str(x) -> str", "Turns a number, bool, char, []u8 or C char pointer into text."),
doc("int", "int(x) -> int", "Converts to a whole number: cuts off fractions, parses text (stops the program if it isn't a number, unless handled: int(s) or 0)."),
doc("float", "float(x) -> float", "Converts to a decimal number; parses text (int(s) or 0.0 handles bad text)."),
doc("char", "char(int n) -> char", "The character with this code."),
doc("bool", "bool(int n) -> bool", "true for any number but 0."),
doc("ptr", "ptr(x) -> ptr", "An untyped pointer: from a typed pointer, or from a number (an address or offset for C).")
]
}
fn arrayMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of elements."),
doc("ptr", ".ptr -> ptr[T]", "The address of the first element, for C."),
doc("push", "push(T x)", "Adds x at the end."),
doc("pop", "pop() -> T", "Removes the last element and gives it back."),
doc("insert", "insert(int i, T x)", "Puts x at position i, moving the rest."),
doc("remove", "remove(int i) -> T", "Removes position i and gives it back."),
doc("clear", "clear()", "Removes everything."),
doc("resize", "resize(int n)", "Makes the array n long; new elements are zero."),
doc("find", "find(T x) -> int", "The index of x, or -1."),
doc("contains", "contains(T x) -> bool", "Is x in the array?"),
doc("sort", "sort()", "Sorts in place (numbers, chars, str)."),
doc("reverse", "reverse()", "Reverses in place."),
doc("slice", "slice(int start, int end) -> []T", "A new array with the elements start to end-1."),
doc("join", "join(str sep) -> str", "For []str: one text with sep between the parts.")
]
}
fn strMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of bytes."),
doc("ptr", ".ptr -> ptr[char]", "The address of the text, for C (valid while the str lives)."),
doc("sub", "sub(int start, int end) -> str", "The part from start to end-1."),
doc("find", "find(str t) -> int", "Where t starts, or -1."),
doc("contains", "contains(str t) -> bool", "Does the text contain t?"),
doc("starts_with", "starts_with(str t) -> bool", "Does the text start with t?"),
doc("ends_with", "ends_with(str t) -> bool", "Does the text end with t?"),
doc("split", "split(str sep) -> []str", "The parts between each sep; split(\"\") gives single characters."),
doc("trim", "trim() -> str", "Without spaces and newlines at both ends."),
doc("upper", "upper() -> str", "With a-z changed to A-Z."),
doc("lower", "lower() -> str", "With A-Z changed to a-z."),
doc("replace", "replace(str from, str to) -> str", "Every from replaced by to."),
doc("repeat", "repeat(int n) -> str", "The text n times."),
doc("bytes", "bytes() -> []u8", "The bytes of the text.")
]
}
fn mapMethods() -> []Doc {
return [
doc("len", ".len -> int", "The number of keys."),
doc("has", "has(K key) -> bool", "Is the key in the map?"),
doc("get", "get(K key, V default) -> V", "The value for key, or default if the key isn't there."),
doc("remove", "remove(K key) -> bool", "Removes the key and its value; true if it was there."),
doc("clear", "clear()", "Removes everything."),
doc("keys", "keys() -> []K", "The keys, in the order they were added."),
doc("values", "values() -> []V", "The values, in the order their keys were added.")
]
}
fn keywords() -> []str {
return ["fn", "return", "if", "else", "while", "for", "in", "break", "continue", "true", "false", "null", "import",
"link", "struct", "enum", "match", "export", "defer", "or", "try", "self"]
}
fn typeNames() -> []str {
return ["int", "float", "bool", "char", "str", "ptr", "map", "i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64"]
}
// the entry called `name`, or an empty Doc
fn find([]Doc list, str name) -> Doc {
for d in list {
if d.name == name {
return d
}
}
return Doc()
}
// the text of a hover for a documented built-in
fn hover(Doc d) -> str {
return "```finch\n" + d.signature + "\n```\n" + d.text
}

View file

@ -4,6 +4,7 @@
// C runtime the main compiler uses. This compiler copies values like the main one
// (so programs behave the same) but never frees them: it is the bootstrap compiler.
import ast
import docs
import cimport
import platform
@ -13,6 +14,9 @@ struct Var {
str slot
bool readonly
int order // how many variables were declared before this one (for defer)
str dpath // where it was declared, for the language server
int dline
int dcol
}
// A scope's variables, and the defers that run when it ends, newest first.
@ -20,6 +24,7 @@ struct Var {
// What the driver asks for besides the program itself.
struct Options {
bool shared // finch build --shared
bool index // finch index: say what every name in the program is, for the language server
bool debug // -g
str cwd // the folder the compiler was started in (for the debug info)
platform.Target target
@ -147,6 +152,8 @@ struct Gen {
int nvars // variables declared so far in this function
cimport.CImports ci
bool shared // building a library (finch build --shared): no main, its export fns are the way in
bool index // the language server's index is being made: see note...
[]str idx // its lines
bool debug // -g: debug info (DWARF, as LLVM metadata)
platform.Target tg // the system the program is for
str cwd
@ -168,7 +175,119 @@ struct Gen {
// ---------- basics ----------
// ---------- the language server's index ----------
//
// `finch index file.fch` runs the compiler up to the end of type checking and prints what it learned instead of
// the program, one record per line, fields separated by tabs:
// @R file line col len defFile defLine defCol hover a name in the source, what it is, where it was made
// @V name type file line col fnFile fnLine fnEnd a variable (for completion), in the function that spans those lines
// @F module name recv file line col endLine signature a function or method
// @S module name file line col and @SF module struct field type
// @E module name file line col variants an enum
// @I module a module the file imports
// Text in a record has its newlines written \n.
fn esc(str s) -> str {
return s.replace("\\", "\\\\").replace("\n", "\\n").replace("\t", " ")
}
fn noteRef(ptr[Gen] g, ast.Node at, int col, int len, str hover, str defPath, int defLine, int defCol) {
if !g.index {
return
}
g.idx.push("@R\t" + g.files[at.file] + "\t" + str(at.line) + "\t" + str(col) + "\t" + str(len) + "\t" + defPath + "\t" + str(defLine) + "\t" + str(defCol) + "\t" + esc(hover))
}
// fn add(int a, int b) -> int fn Counter.add(int n) fn parse(str s) -> int!
fn sigText(FnSig sig) -> str {
[]str ps
for k in 0..sig.params.len {
ps.push(ast.show(sig.params[k]) + " " + sig.pnames[k])
}
name := sig.name
if sig.recv.len > 0 {
name = sig.recv + "." + sig.name
}
text := "fn " + name + "(" + ps.join(", ") + ")"
if sig.fallible {
if sig.ret.kind == "void" {
text = text + " -> !"
} else {
text = text + " -> " + ast.show(sig.ret) + "!"
}
} else if sig.ret.kind != "void" {
text = text + " -> " + ast.show(sig.ret)
}
return text
}
// a call of a Finch function or method: the name in the source, and the function it is
fn noteCall(ptr[Gen] g, ast.Node c, FnSig sig) {
if !g.index {
return
}
col := c.col
if c.kind == "method" {
col = c.col + 1
}
f := g.progs[sig.prog].fns[sig.index]
shownName := sig.name
if sig.targs.len > 0 && sig.recv.len == 0 {
shownName = f.name
}
noteRef(g, c, col, shownName.len, "```finch\n" + sigText(sig) + "\n```", g.files[f.file], f.line, f.col + 3)
}
// what is known about every function, struct and enum, for completion and the outline
fn indexAll(ptr[Gen] g, []str mainImports) {
for im in mainImports {
g.idx.push("@I\t" + im)
}
for sg in g.sigs {
f := g.progs[sg.prog].fns[sg.index]
if sg.targs.len > 0 {
continue
}
[]str ps
for k in 0..sg.params.len {
ps.push(ast.show(sg.params[k]) + " " + sg.pnames[k])
}
g.idx.push("@F\t" + sg.module + "\t" + sg.name + "\t" + sg.recv + "\t" + g.files[f.file] + "\t" + str(f.line) + "\t" + str(f.col) + "\t" + str(f.endLine) + "\t" + esc(sigText(sg)) + "\t" + ps.join("|"))
}
for tp in g.templates {
if tp.isStruct {
continue
}
f := g.progs[tp.prog].fns[tp.index]
g.idx.push("@F\t" + tp.module + "\t" + tp.name + "\t" + tp.recv + "\t" + g.files[f.file] + "\t" + str(f.line) + "\t" + str(f.col) + "\t" + str(f.endLine) + "\t" + esc("fn " + tp.name + "[" + tp.tparams.join(", ") + "](...)") + "\t")
}
for sd in g.structs {
if sd.isC || sd.base.len > 0 {
continue
}
node := g.progs[sd.prog].structs[sd.index]
g.idx.push("@S\t" + sd.module + "\t" + sd.name + "\t" + g.files[node.file] + "\t" + str(node.line) + "\t" + str(node.col))
for k in 0..sd.fnames.len {
g.idx.push("@SF\t" + sd.module + "\t" + sd.name + "\t" + sd.fnames[k] + "\t" + ast.show(sd.ftypes[k]))
}
}
for pi in 0..g.progs.len {
for en in g.progs[pi].enums {
vs := ""
[]str names
for v in en.kids {
names.push(v.name)
}
vs = names.join(",")
g.idx.push("@E\t" + g.progs[pi].module + "\t" + en.name + "\t" + g.files[en.file] + "\t" + str(en.line) + "\t" + str(en.col) + "\t" + vs)
}
}
}
fn fail(ptr[Gen] g, ast.Node n, str msg) {
if g.index && g.idx.len > 0 {
print(g.idx.join("\n")) // what was learned before the error
}
print(g.files[n.file] + ":" + str(n.line) + ":" + str(n.col) + ": error: " + msg + g.instNote)
exit(1)
}
@ -1081,9 +1200,14 @@ fn addVar(ptr[Gen] g, ast.Node at, str name, ast.Type t, str init, bool readonly
}
slot := alloca(g, llty(g, t))
store(g, llty(g, t), init, slot)
g.scopes[g.scopes.len - 1].vars.push(Var(name: name, ty: t, slot: slot, readonly: readonly, order: g.nvars))
g.scopes[g.scopes.len - 1].vars.push(Var(name: name, ty: t, slot: slot, readonly: readonly, order: g.nvars, dpath: g.files[at.file], dline: at.line, dcol: at.col))
g.nvars += 1
dbgVariable(g, at, name, t, slot)
if g.index && name != "err" && !name.starts_with("$") {
fnode := g.progs[g.cur.prog].fns[g.cur.index]
g.idx.push("@V\t" + name + "\t" + ast.show(t) + "\t" + g.files[at.file] + "\t" + str(at.line) + "\t" + str(at.col) + "\t" + g.files[fnode.file] + "\t" + str(fnode.line) + "\t" + str(fnode.endLine))
noteRef(g, at, at.col, name.len, "```finch\n" + ast.show(t) + " " + name + "\n```", "", 0, 0)
}
return slot
}
@ -1468,6 +1592,7 @@ fn genericCall(ptr[Gen] g, ast.Node c, int ti, int first, str shown) -> Val {
}
si := instFn(g, ti, b.types, T("void"), c)
sig := g.sigs[si]
noteCall(g, c, sig)
[]str out
for k in 0..nparams {
v := coerce(g, vals[k], sig.params[k], c.kids[first + k], "argument '" + sig.pnames[k] + "'")
@ -3215,6 +3340,7 @@ fn ref(ptr[Gen] g, ast.Node e, bool write) -> Ref {
fail(g, e, "there is no variable named '" + e.name + "'")
}
v := varAt(g, id)
noteRef(g, e, e.col, e.name.len, "```finch\n" + ast.show(v.ty) + " " + e.name + "\n```", v.dpath, v.dline, v.dcol)
return Ref(isPlace: true, pl: Place(addr: v.slot, ty: v.ty))
}
if e.kind == "member" {
@ -3263,6 +3389,7 @@ fn enumValue(ptr[Gen] g, ast.Node m, int ei) -> Val {
if k < 0 {
fail(g, m, "the enum " + ed.name + " has no variant '" + m.name + "' (it has: " + ed.variants.join(", ") + ")")
}
noteRef(g, m, m.col + 1, m.name.len, "```finch\n" + ed.name + "." + m.name + " = " + str(ed.values[k]) + "\n```", "", 0, 0)
return Val(v: str(ed.values[k]), ty: ast.Type(kind: "enum", name: ed.name, module: ed.module))
}
@ -3357,6 +3484,7 @@ fn member(ptr[Gen] g, ast.Node m, bool write) -> Ref {
fail(g, m, "the struct " + ast.show(t) + " has no field '" + m.name + "' (it has: " + sd.fnames.join(", ") + ")")
}
ft := sd.ftypes[k]
noteRef(g, m, m.col + 1, m.name.len, "```finch\n" + ast.show(ft) + " " + m.name + "\n```", "", 0, 0)
if obj.isPlace {
fp := op(g, "getelementptr inbounds " + llty(g, t) + ", ptr " + obj.pl.addr + ", i32 0, i32 " + str(sd.lidx[k]))
return Ref(isPlace: true, pl: Place(addr: fp, ty: ft))
@ -4009,6 +4137,16 @@ fn cLL(ptr[Gen] g, ast.Type t) -> str {
}
fn callC(ptr[Gen] g, ast.Node c, cimport.CFunc f) -> Val {
if g.index {
[]str ps
for p in f.params {
ps.push(ast.show(p))
}
if f.variadic {
ps.push("...")
}
noteRef(g, c, c.col, f.name.len, "C function from `" + f.header + "`\n```c\n" + ast.show(f.ret) + " " + f.name + "(" + ps.join(", ") + ")\n```", "", 0, 0)
}
if f.unsupported.len > 0 {
fail(g, c, "the C function '" + f.name + "' can't be used from Finch yet: it " + f.unsupported)
}
@ -4163,6 +4301,12 @@ fn headerHint(str name) -> str {
fn call(ptr[Gen] g, ast.Node c) -> Val {
n := c.name
if g.index && isBuiltin(n) {
d := docs.find(docs.builtins(), n)
if d.name.len > 0 {
noteRef(g, c, c.col, n.len, docs.hover(d), "", 0, 0)
}
}
if n == "error" {
fail(g, c, "error(...) makes an error to return from a function that can fail: return error(\"...\")")
}
@ -4351,6 +4495,11 @@ fn call(ptr[Gen] g, ast.Node c) -> Val {
}
si := findStruct(g, g.module, n)
if si >= 0 {
sd := g.structs[si]
if g.index && !sd.isC {
node := g.progs[sd.prog].structs[sd.index]
noteRef(g, c, c.col, n.len, "```finch\nstruct " + sd.name + " { " + sd.fnames.join(", ") + " }\n```", g.files[node.file], node.line, node.col)
}
return construct(g, si, c.kids, c.names, c)
}
ei := findEnum(g, g.module, n)
@ -4376,6 +4525,7 @@ fn callFn(ptr[Gen] g, int fi, ast.Node c, int first, str shown, str selfAddr, st
if sig.module.len == 0 && sig.name == "main" {
fail(g, c, "main can't be called, it is where the program starts")
}
noteCall(g, c, sig)
checkArgs(g, c, first, sig.params.len, shown)
[]str args
if sig.recv.len > 0 {
@ -4482,6 +4632,17 @@ fn construct(ptr[Gen] g, int si, []ast.Node args, []str names, ast.Node at) -> V
// ---------- methods ----------
// a method of an array, text or map: hover from the built-in docs
fn noteMethod(ptr[Gen] g, ast.Node m, []docs.Doc list) {
if !g.index {
return
}
d := docs.find(list, m.name)
if d.name.len > 0 {
noteRef(g, m, m.col + 1, m.name.len, docs.hover(d), "", 0, 0)
}
}
fn method(ptr[Gen] g, ast.Node m) -> Val {
objNode := m.kids[0]
if objNode.kind == "var" && lookup(g, objNode.name) < 0 {
@ -4527,6 +4688,7 @@ fn method(ptr[Gen] g, ast.Node m) -> Val {
}
a = spill(g, r.val)
}
noteMethod(g, m, docs.arrayMethods())
return arrayMethod(g, m, a, t)
}
if t.kind == "str" {
@ -4534,6 +4696,7 @@ fn method(ptr[Gen] g, ast.Node m) -> Val {
if r.isPlace {
sv = Val(v: load(g, "{ ptr, i64, i64 }", r.pl.addr), ty: t)
}
noteMethod(g, m, docs.strMethods())
return strMethod(g, m, spill(g, sv))
}
if t.kind == "map" {
@ -4546,6 +4709,7 @@ fn method(ptr[Gen] g, ast.Node m) -> Val {
}
mp = spill(g, r.val)
}
noteMethod(g, m, docs.mapMethods())
return mapMethod(g, m, mp, t)
}
st := t
@ -4958,7 +5122,7 @@ fn assemble(ptr[Gen] g) -> str {
fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt) -> str {
shared := opt.shared
gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, cwd: opt.cwd, tg: opt.target, dbgScope: -1)
gen := Gen(progs: progs, files: files, ci: ci, shared: opt.shared, debug: opt.debug, index: opt.index, cwd: opt.cwd, tg: opt.target, dbgScope: -1)
g := addr(gen)
if g.debug {
// !0 the compile unit, !1 and !2 the module flags, then the source files
@ -4986,6 +5150,11 @@ fn generate([]ast.Program progs, []str files, cimport.CImports ci, Options opt)
define(g, di)
di += 1
}
if g.index {
mainImports := g.progs[0].imports
indexAll(g, mainImports)
return g.idx.join("\n")
}
for sg in g.sigs {
if sg.exported {
exportWrapper(g, sg)

875
src/lsp.fch Normal file
View file

@ -0,0 +1,875 @@
// The language server: `finch lsp`. An editor talks to it over stdin and stdout (JSON-RPC, as the Language Server
// Protocol says) and gets errors while it types, hover, go to definition, completion, an outline and signature
// help.
//
// It asks the compiler instead of being one: for every change it runs `finch index` on the text the editor has
// (see the index records in gen.fch), and answers from what comes back.
import json
import docs
import "stdio.h"
import "unistd.h"
import "stdlib.h"
// ---------- what one run of the compiler told ----------
struct Ref {
str file
int line
int col
int len
str defFile
int defLine
int defCol
str hover
}
struct VarRec {
str name
str type
str file
int line
int col
str fnFile
int fnLine
int fnEnd
}
struct FnRec {
str module
str name
str recv
str file
int line
int col
int endLine
str signature
[]str params
}
struct StructRec {
str module
str name
str file
int line
int col
[]str fields
[]str types
}
struct EnumRec {
str module
str name
str file
int line
int col
[]str variants
}
struct Diag {
bool has
str file
int line
int col
str message
}
struct Analysis {
[]Ref refs
[]VarRec vars
[]FnRec fns
[]StructRec structs
[]EnumRec enums
[]str imports
Diag diag
}
struct Server {
str self // the compiler's file: what to run for each check
str tmp // a folder for the copies of the editor's text
map[str]str docs // path -> the text the editor has
map[str]Analysis results // path -> the last check
map[str]Analysis good // path -> the last check that found no error
[]str diagnosed // the files an error was shown for
bool stopping
}
// ---------- paths and text ----------
fn hexval(char c) -> int {
if c >= '0' && c <= '9' {
return int(c) - int('0')
}
if c >= 'a' && c <= 'f' {
return int(c) - int('a') + 10
}
if c >= 'A' && c <= 'F' {
return int(c) - int('A') + 10
}
return -1
}
fn uriToPath(str uri) -> str {
s := uri
if s.starts_with("file://") {
s = s.sub(7, s.len)
}
[]u8 out
i := 0
while i < s.len {
if s[i] == '%' && i + 2 < s.len + 0 && hexval(s[i + 1]) >= 0 && hexval(s[i + 2]) >= 0 {
out.push(u8(hexval(s[i + 1]) * 16 + hexval(s[i + 2])))
i += 3
} else {
out.push(u8(s[i]))
i += 1
}
}
return str(out)
}
fn pathToUri(str path) -> str {
digits := "0123456789ABCDEF"
[]str parts = ["file://"]
for c in path {
n := int(c)
if (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || "/-_.~".contains(str(c)) {
parts.push(str(c))
} else {
parts.push("%" + str(digits[(n / 16) % 16]) + str(digits[n % 16]))
}
}
return parts.join("")
}
// "\n" written as two characters, back to a line break
fn unesc(str s) -> str {
[]str parts
i := 0
while i < s.len {
if s[i] == '\\' && i + 1 < s.len {
if s[i + 1] == 'n' {
parts.push("\n")
} else {
parts.push(str(s[i + 1]))
}
i += 2
} else {
parts.push(str(s[i]))
i += 1
}
}
return parts.join("")
}
fn lineOf(str text, int line) -> str {
lines := text.split("\n")
if line >= 1 && line <= lines.len {
return lines[line - 1]
}
return ""
}
// ---------- the protocol ----------
fn readLine() -> str {
[]u8 bytes
while true {
c := getchar()
if c < 0 {
return ""
}
if c == 10 {
break
}
bytes.push(u8(c))
}
line := str(bytes)
if line.ends_with("\r") {
line = line.sub(0, line.len - 1)
}
return line
}
// the next message; an error at the end of the input
fn readMessage() -> json.Json! {
length := 0
while true {
header := readLine()
if header.len == 0 {
if length > 0 {
break
}
if feof(stdin) != 0 {
return error("the editor closed the connection")
}
continue
}
if header.starts_with("Content-Length:") {
length = int(header.sub(15, header.len).trim()) or 0
}
}
[]u8 body
for i in 0..length {
c := getchar()
if c < 0 {
return error("the message was cut short")
}
body.push(u8(c))
}
return try json.parse(str(body))
}
fn send(json.Json v) {
body := json.stringify(v)
head := "Content-Length: " + str(body.len) + "\r\n\r\n"
text := head + body
fwrite(text.ptr, 1, u64(text.len), stdout)
fflush(stdout)
}
fn reply(json.Json id, json.Json result) {
m := json.new_object()
m.set("jsonrpc", json.new_text("2.0"))
m.set("id", id)
m.set("result", result)
send(m)
}
fn notify(str method, json.Json params) {
m := json.new_object()
m.set("jsonrpc", json.new_text("2.0"))
m.set("method", json.new_text(method))
m.set("params", params)
send(m)
}
fn num(int n) -> json.Json {
return json.new_number(float(n))
}
fn position(int line, int col) -> json.Json {
p := json.new_object()
p.set("line", num(line))
p.set("character", num(col))
return p
}
fn range(int line, int col, int endCol) -> json.Json {
r := json.new_object()
r.set("start", position(line, col))
r.set("end", position(line, endCol))
return r
}
// ---------- asking the compiler ----------
fn splitTabs(str line) -> []str {
return line.split("\t")
}
fn intOf(str s) -> int {
return int(s) or 0
}
// "path:line:col: error: message" -> a diagnostic
fn parseDiag(str line) -> Diag {
at := line.find(": error: ")
if at < 0 {
return Diag()
}
where := line.sub(0, at)
message := line.sub(at + 9, line.len)
// the last two colon-separated numbers are the line and column
parts := where.split(":")
if parts.len < 3 {
return Diag()
}
col := intOf(parts[parts.len - 1])
ln := intOf(parts[parts.len - 2])
[]str pathParts
for i in 0..parts.len - 2 {
pathParts.push(parts[i])
}
return Diag(has: true, file: pathParts.join(":"), line: ln, col: col, message: message)
}
fn parseOutput(str output) -> Analysis {
a := Analysis()
for line in output.split("\n") {
if line.starts_with("@") {
f := splitTabs(line)
tag := f[0]
if tag == "@R" && f.len >= 9 {
a.refs.push(Ref(file: f[1], line: intOf(f[2]), col: intOf(f[3]), len: intOf(f[4]), defFile: f[5], defLine: intOf(f[6]), defCol: intOf(f[7]), hover: unesc(f[8])))
} else if tag == "@V" && f.len >= 9 {
a.vars.push(VarRec(name: f[1], type: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5]), fnFile: f[6], fnLine: intOf(f[7]), fnEnd: intOf(f[8])))
} else if tag == "@F" && f.len >= 10 {
a.fns.push(FnRec(module: f[1], name: f[2], recv: f[3], file: f[4], line: intOf(f[5]), col: intOf(f[6]), endLine: intOf(f[7]), signature: unesc(f[8]), params: f[9].split("|")))
} else if tag == "@S" && f.len >= 6 {
a.structs.push(StructRec(module: f[1], name: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5])))
} else if tag == "@SF" && f.len >= 5 && a.structs.len > 0 {
k := a.structs.len - 1
a.structs[k].fields.push(f[3])
a.structs[k].types.push(f[4])
} else if tag == "@E" && f.len >= 7 {
a.enums.push(EnumRec(module: f[1], name: f[2], file: f[3], line: intOf(f[4]), col: intOf(f[5]), variants: f[6].split(",")))
} else if tag == "@I" && f.len >= 2 {
a.imports.push(f[1])
}
} else if !a.diag.has {
a.diag = parseDiag(line)
}
}
return a
}
fn Server.analyse(str path) {
// the editor's text, as files the compiler can read: it asks for the file by its real name
shell("mkdir -p '" + self.tmp + "'")
overlays := ""
n := 0
for p in self.docs.keys() {
copy := self.tmp + "/doc" + str(n) + ".fch"
write_file(copy, self.docs[p])
overlays = overlays + " --overlay '" + p + "=" + copy + "'"
n += 1
}
out := self.tmp + "/out.txt"
shell("'" + self.self + "' index '" + path + "'" + overlays + " > '" + out + "' 2>&1")
a := parseOutput(read_file(out) or "")
if !a.diag.has {
self.good[path] = a
} else if self.good.has(path) {
// half-typed code doesn't compile: completion and signatures keep using what the last good version knew
g := self.good[path]
a.fns = g.fns
a.structs = g.structs
a.enums = g.enums
a.vars = g.vars
if a.imports.len == 0 {
a.imports = g.imports
}
}
self.results[path] = a
self.publish(path, a)
}
fn Server.publish(str path, Analysis a) {
// the files to say something about: the one checked, and any that had an error that is gone
[]str files = [path]
for d in self.diagnosed {
if !files.contains(d) {
files.push(d)
}
}
[]str now
for f in files {
list := json.new_list()
if a.diag.has && a.diag.file == f {
line := a.diag.line - 1
if line < 0 {
line = 0
}
col := a.diag.col - 1
if col < 0 {
col = 0
}
d := json.new_object()
d.set("range", range(line, col, col + 1))
d.set("severity", num(1))
d.set("source", json.new_text("finch"))
d.set("message", json.new_text(a.diag.message))
list.push(d)
now.push(f)
}
p := json.new_object()
p.set("uri", json.new_text(pathToUri(f)))
p.set("diagnostics", list)
notify("textDocument/publishDiagnostics", p)
}
self.diagnosed = now
}
// ---------- the questions ----------
fn refAt(Analysis a, str path, int line, int col) -> int {
for i in 0..a.refs.len {
r := a.refs[i]
if r.line == line && col >= r.col && col <= r.col + r.len && r.file == path {
return i
}
}
return -1
}
fn hoverAt(Analysis a, str path, int line, int col) -> json.Json {
i := refAt(a, path, line, col)
if i < 0 {
return json.new_null()
}
contents := json.new_object()
contents.set("kind", json.new_text("markdown"))
contents.set("value", json.new_text(a.refs[i].hover))
out := json.new_object()
out.set("contents", contents)
return out
}
fn definitionAt(Analysis a, str path, int line, int col) -> json.Json {
i := refAt(a, path, line, col)
if i < 0 || a.refs[i].defFile.len == 0 {
return json.new_null()
}
r := a.refs[i]
out := json.new_object()
out.set("uri", json.new_text(pathToUri(r.defFile)))
out.set("range", range(r.defLine - 1, r.defCol - 1, r.defCol - 1))
return out
}
fn symbolsOf(Analysis a, str path) -> json.Json {
list := json.new_list()
for f in a.fns {
if f.file != path {
continue
}
kind := 12 // Function
name := f.name
if f.recv.len > 0 {
kind = 6 // Method
name = f.recv + "." + f.name
}
list.push(symbol(name, kind, f.line, f.col, f.endLine))
}
for s in a.structs {
if s.file == path {
list.push(symbol(s.name, 23, s.line, s.col, s.line + s.fields.len + 1))
}
}
for e in a.enums {
if e.file == path {
list.push(symbol(e.name, 10, e.line, e.col, e.line + e.variants.len + 1))
}
}
return list
}
fn symbol(str name, int kind, int line, int col, int endLine) -> json.Json {
s := json.new_object()
s.set("name", json.new_text(name))
s.set("kind", num(kind))
full := json.new_object()
full.set("start", position(line - 1, 0))
full.set("end", position(endLine, 0))
s.set("range", full)
s.set("selectionRange", range(line - 1, col - 1, col - 1))
return s
}
// ---------- completion ----------
fn item(str label, int kind, str detail, str text) -> json.Json {
o := json.new_object()
o.set("label", json.new_text(label))
o.set("kind", num(kind))
if detail.len > 0 {
o.set("detail", json.new_text(detail))
}
if text.len > 0 {
d := json.new_object()
d.set("kind", json.new_text("markdown"))
d.set("value", json.new_text(text))
o.set("documentation", d)
}
return o
}
fn isWordChar(char c) -> bool {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_'
}
fn methodItems(ptr[json.Json] items, []docs.Doc list) {
for d in list {
kind := 2 // Method
if d.signature.starts_with(".") {
kind = 5 // Field
}
items.push(item(d.name, kind, d.signature, d.text))
}
}
// the variable called `name` that is in scope at `line` (the innermost one declared before it)
fn varNamed(Analysis a, str path, str name, int line) -> VarRec {
found := VarRec()
for v in a.vars {
if v.name == name && v.line <= line && line <= v.fnEnd + 1 && v.fnFile == path {
found = v
}
}
return found
}
fn completionAt(Analysis a, str text, str path, int line, int col) -> json.Json {
items := json.new_list()
lineText := lineOf(text, line)
end := col - 1
if end > lineText.len {
end = lineText.len
}
before := lineText.sub(0, end)
// the word being typed, and whether a dot comes right before it
wordStart := before.len
while wordStart > 0 && isWordChar(before[wordStart - 1]) {
wordStart -= 1
}
afterDot := wordStart > 0 && before[wordStart - 1] == '.'
if afterDot {
s := wordStart - 1
while s > 0 && (isWordChar(before[s - 1]) || before[s - 1] == '[' || before[s - 1] == ']') {
s -= 1
}
owner := before.sub(s, wordStart - 1)
element := false
br := owner.find("[")
if br >= 0 {
owner = owner.sub(0, br)
element = true
}
// a module: its functions, structs and enums
if a.imports.contains(owner) {
for f in a.fns {
if f.module == owner && f.recv.len == 0 {
items.push(item(f.name, 3, f.signature, ""))
}
}
for st in a.structs {
if st.module == owner {
items.push(item(st.name, 22, "struct " + st.name, ""))
}
}
for en in a.enums {
if en.module == owner {
items.push(item(en.name, 13, "enum " + en.name, ""))
}
}
return completionList(items)
}
// an enum: its variants
for en in a.enums {
if en.name == owner && varNamed(a, path, owner, line).name.len == 0 {
for v in en.variants {
items.push(item(v, 20, en.name + "." + v, ""))
}
return completionList(items)
}
}
// a variable: by its type
type := varNamed(a, path, owner, line).type
if element && type.starts_with("[]") {
type = type.sub(2, type.len)
} else if element && type == "str" {
type = "char"
}
if type.starts_with("ptr[") && type.ends_with("]") {
type = type.sub(4, type.len - 1)
}
if type.starts_with("[]") {
methodItems(addr(items), docs.arrayMethods())
} else if type == "str" {
methodItems(addr(items), docs.strMethods())
} else if type.starts_with("map[") {
methodItems(addr(items), docs.mapMethods())
} else {
base := type
for st in a.structs {
if st.name == base {
for k in 0..st.fields.len {
items.push(item(st.fields[k], 5, st.types[k], ""))
}
}
}
for f in a.fns {
if f.recv == base {
items.push(item(f.name, 2, f.signature, ""))
}
}
}
return completionList(items)
}
// not after a dot: what is in scope
seen := Names()
for v in a.vars {
if v.line <= line && line <= v.fnEnd + 1 && v.fnFile == path && seen.add(v.name) {
items.push(item(v.name, 6, v.type, ""))
}
}
for f in a.fns {
if f.recv.len == 0 && f.module == "" && seen.add(f.name) {
items.push(item(f.name, 3, f.signature, ""))
}
}
for st in a.structs {
if st.module == "" && seen.add(st.name) {
items.push(item(st.name, 22, "struct " + st.name, ""))
}
}
for en in a.enums {
if en.module == "" && seen.add(en.name) {
items.push(item(en.name, 13, "enum " + en.name, ""))
}
}
for m in a.imports {
if seen.add(m) {
items.push(item(m, 9, "module", ""))
}
}
for d in docs.builtins() {
if seen.add(d.name) {
items.push(item(d.name, 3, d.signature, d.text))
}
}
for k in docs.keywords() {
if seen.add(k) {
items.push(item(k, 14, "", ""))
}
}
for t in docs.typeNames() {
if seen.add(t) {
items.push(item(t, 25, "", ""))
}
}
return completionList(items)
}
struct Names {
[]str items
}
fn Names.add(str x) -> bool {
if self.items.contains(x) {
return false
}
self.items.push(x)
return true
}
fn completionList(json.Json items) -> json.Json {
o := json.new_object()
o.set("isIncomplete", json.new_bool(false))
o.set("items", items)
return o
}
// ---------- signature help ----------
fn signatureAt(Analysis a, str text, str path, int line, int col) -> json.Json {
lineText := lineOf(text, line)
end := col - 1
if end > lineText.len {
end = lineText.len
}
before := lineText.sub(0, end)
// back to the `(` that is still open, counting the commas at its own level
depth := 0
commas := 0
open := -1
i := before.len - 1
while i >= 0 {
c := before[i]
if c == ')' || c == ']' {
depth += 1
} else if c == '(' || c == '[' {
if depth == 0 {
if c == '(' {
open = i
break
}
} else {
depth -= 1
}
} else if c == ',' && depth == 0 {
commas += 1
}
i -= 1
}
if open < 0 {
return json.new_null()
}
nameEnd := open
nameStart := nameEnd
while nameStart > 0 && isWordChar(before[nameStart - 1]) {
nameStart -= 1
}
name := before.sub(nameStart, nameEnd)
if name.len == 0 {
return json.new_null()
}
recv := ""
if nameStart > 0 && before[nameStart - 1] == '.' {
// a method: its struct from the type of what comes before the dot
s := nameStart - 1
while s > 0 && isWordChar(before[s - 1]) {
s -= 1
}
owner := before.sub(s, nameStart - 1)
type := varNamed(a, path, owner, line).type
if type.starts_with("ptr[") && type.ends_with("]") {
type = type.sub(4, type.len - 1)
}
recv = type
}
for f in a.fns {
if f.name == name && f.recv == recv {
sig := json.new_object()
sig.set("label", json.new_text(f.signature))
params := json.new_list()
for p in f.params {
if p.len > 0 {
one := json.new_object()
one.set("label", json.new_text(p))
params.push(one)
}
}
sig.set("parameters", params)
sigs := json.new_list()
sigs.push(sig)
out := json.new_object()
out.set("signatures", sigs)
out.set("activeSignature", num(0))
out.set("activeParameter", num(commas))
return out
}
}
d := docs.find(docs.builtins(), name)
if d.name.len > 0 {
sig := json.new_object()
sig.set("label", json.new_text(d.signature))
sig.set("documentation", json.new_text(d.text))
sigs := json.new_list()
sigs.push(sig)
out := json.new_object()
out.set("signatures", sigs)
out.set("activeSignature", num(0))
out.set("activeParameter", num(commas))
return out
}
return json.new_null()
}
// ---------- the messages ----------
fn textOf(json.Json params) -> str {
return params.get("textDocument").get("text").str_or("")
}
fn pathOf(json.Json params) -> str {
return uriToPath(params.get("textDocument").get("uri").str_or(""))
}
fn Server.answer(json.Json msg) {
method := msg.get("method").str_or("")
params := msg.get("params")
id := msg.get("id")
match method {
"initialize" -> {
caps := json.new_object()
caps.set("textDocumentSync", num(1))
caps.set("hoverProvider", json.new_bool(true))
caps.set("definitionProvider", json.new_bool(true))
caps.set("documentSymbolProvider", json.new_bool(true))
comp := json.new_object()
dot := json.new_list()
dot.push(json.new_text("."))
comp.set("triggerCharacters", dot)
caps.set("completionProvider", comp)
sigp := json.new_object()
trig := json.new_list()
trig.push(json.new_text("("))
trig.push(json.new_text(","))
sigp.set("triggerCharacters", trig)
caps.set("signatureHelpProvider", sigp)
result := json.new_object()
result.set("capabilities", caps)
info := json.new_object()
info.set("name", json.new_text("finch"))
result.set("serverInfo", info)
reply(id, result)
}
"shutdown" -> {
self.stopping = true
reply(id, json.new_null())
}
"exit" -> {
exit(0)
}
"textDocument/didOpen" -> {
path := pathOf(params)
self.docs[path] = textOf(params)
self.analyse(path)
}
"textDocument/didChange" -> {
path := pathOf(params)
changes := params.get("contentChanges")
if changes.len() > 0 {
self.docs[path] = changes.at(changes.len() - 1).get("text").str_or("")
self.analyse(path)
}
}
"textDocument/didSave" -> {
path := pathOf(params)
if self.docs.has(path) {
self.analyse(path)
}
}
"textDocument/didClose" -> {
path := pathOf(params)
self.docs.remove(path)
self.results.remove(path)
self.good.remove(path)
empty := json.new_object()
empty.set("uri", json.new_text(pathToUri(path)))
empty.set("diagnostics", json.new_list())
notify("textDocument/publishDiagnostics", empty)
}
"textDocument/hover", "textDocument/definition", "textDocument/completion", "textDocument/documentSymbol", "textDocument/signatureHelp" -> {
path := pathOf(params)
line := params.get("position").get("line").int_or(0) + 1
col := params.get("position").get("character").int_or(0) + 1
if !self.results.has(path) {
reply(id, json.new_null())
return
}
a := self.results[path]
text := ""
if self.docs.has(path) {
text = self.docs[path]
}
match method {
"textDocument/hover" -> reply(id, hoverAt(a, path, line, col))
"textDocument/definition" -> reply(id, definitionAt(a, path, line, col))
"textDocument/completion" -> reply(id, completionAt(a, text, path, line, col))
"textDocument/documentSymbol" -> reply(id, symbolsOf(a, path))
else -> reply(id, signatureAt(a, text, path, line, col))
}
}
else -> {
// a request we don't know gets an answer so the editor does not wait; notifications get none
if id.kind != json.Kind.Null {
reply(id, json.new_null())
}
}
}
}
// finch lsp
fn run(str self) -> int {
s := Server(self: self, tmp: "/tmp/finch_lsp_" + str(int(getpid())))
while true {
msg := readMessage() or {
break
}
s.answer(msg)
}
shell("rm -rf '" + s.tmp + "'")
return 0
}

View file

@ -12,6 +12,7 @@ import gen
import cimport
import platform
import linker
import lsp
import "stdlib.h"
fn dirOf(str path) -> str {
@ -44,6 +45,16 @@ struct Loader {
str os // "windows", "macos" or "linux": a module may have a version for each (time_windows.fch)
str std // the standard library's folder, or ""
[]str paths // FINCH_PATH: more folders to look for modules in
[]str overlayPaths // --overlay path=file: read `path` from `file` (the text an editor has, not saved yet)
[]str overlayFiles
}
fn textOfFile(ptr[Loader] l, str path) -> str {
k := l.overlayPaths.find(path)
if k >= 0 {
return read_file(l.overlayFiles[k])
}
return read_file(path)
}
// import json: the file next to the one that imports it, else the standard library's.
@ -77,7 +88,7 @@ fn findModule(ptr[Loader] l, str from, str name) -> str {
}
fn load(ptr[Loader] l, str path, str module) {
if !file_exists(path) {
if !file_exists(path) && !l.overlayPaths.contains(path) {
if module.len > 0 {
print("error: can't find the module '" + module + "' (looked for " + path + ")")
} else {
@ -85,7 +96,7 @@ fn load(ptr[Loader] l, str path, str module) {
}
exit(1)
}
src := read_file(path)
src := textOfFile(l, path)
file := l.files.len
l.files.push(path)
prog := parser.parse(lexer.lex(src, path), file, path)
@ -108,13 +119,13 @@ fn usage() {
print(" finch build <file.fch> [-o name] build a program")
print(" finch ir <file.fch> print the LLVM IR")
print(" finch version show the version")
print(" finch lsp the language server, for editors (VS Code, Kate, ...)")
print("options:")
print(" -O0 no optimization (faster to build)")
print(" -l <lib> link a library (the same as link \"lib\" in the program)")
print(" -I <dir> look for imported C headers there too")
print(" -g debug info (for gdb and lldb; use -O0 with it)")
print(" --target <name> build for another system: windows, linux, arm64 (Linux), macos, or an LLVM triple")
print("not in this compiler yet: lsp. It is in the C++ one (branch before-bootstrap).")
exit(1)
}
@ -123,12 +134,15 @@ fn main([]str args) -> int {
print("Finch 2.4.0 (the compiler is written in Finch)")
return 0
}
if args.len == 2 && args[1] == "lsp" {
return lsp.run(args[0]) // the language server, for editors
}
if args.len < 3 {
usage()
}
cmd := args[1]
file := args[2]
if cmd != "build" && cmd != "run" && cmd != "ir" {
if cmd != "build" && cmd != "run" && cmd != "ir" && cmd != "index" {
usage()
}
out := stem(file)
@ -139,6 +153,8 @@ fn main([]str args) -> int {
opt := "-O2"
[]str extraLibs
[]str extraDirs
[]str overlayPaths
[]str overlayFiles
[]str progArgs
ai := 3
while ai < args.len {
@ -161,6 +177,11 @@ fn main([]str args) -> int {
} else if a == "-I" && ai + 1 < args.len {
extraDirs.push(args[ai + 1])
ai += 1
} else if a == "--overlay" && ai + 1 < args.len {
both := args[ai + 1].split("=")
overlayPaths.push(both[0])
overlayFiles.push(both[1])
ai += 1
} else if a == "-g" {
debug = true
} else if a == "--target" && ai + 1 < args.len {
@ -200,6 +221,8 @@ fn main([]str args) -> int {
loader.std = cand
}
}
loader.overlayPaths = overlayPaths
loader.overlayFiles = overlayFiles
for d in getenv("FINCH_PATH").split(":") {
if d.len > 0 {
loader.paths.push(d)
@ -239,8 +262,8 @@ fn main([]str args) -> int {
print(err)
return 1
}
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, cwd: getenv("PWD"), target: tg))
if cmd == "ir" {
ir := gen.generate(loader.progs, loader.files, ci, gen.Options(shared: shared, debug: debug, index: cmd == "index", cwd: getenv("PWD"), target: tg))
if cmd == "ir" || cmd == "index" {
print(ir)
return 0
}

View file

@ -420,6 +420,7 @@ fn parseFn(ptr[Parser] p) -> ast.Node {
}
}
f.kids.push(parseBlock(p))
f.endLine = f.kids[f.kids.len - 1].endLine
return f
}
@ -438,6 +439,7 @@ fn parseBlock(ptr[Parser] p) -> ast.Node {
endOfStatement(p)
}
p.depth = saved
blk.endLine = cur(p).line
next(p)
return blk
}