mirror of
https://github.com/Nexoniarz/Finch.git
synced 2026-11-03 00:44:27 +00:00
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:
parent
b9378050ab
commit
636eaefe59
9 changed files with 64467 additions and 48172 deletions
|
|
@ -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
109
src/docs.fch
Normal 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
|
||||
}
|
||||
173
src/gen.fch
173
src/gen.fch
|
|
@ -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
875
src/lsp.fch
Normal 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
|
||||
}
|
||||
35
src/main.fch
35
src/main.fch
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue