The parser is more robust under error

This commit is contained in:
XeroOl 2022-09-01 16:33:00 -05:00
parent 89eb07c064
commit 3cab59622c
No known key found for this signature in database
GPG Key ID: 9DD4B4B4DAED0322
9 changed files with 202 additions and 141 deletions

View File

@ -25,27 +25,15 @@ Features / To Do List / Things I would enjoy patches for:
- [ ] goes to a.method on `(: a :method)` when triggered at `:method`
- [X] expanded macros (a little bit)
- [ ] table mutation via `fn` special: `(fn obj.new-field [])`
- [ ] local/table mutation via set/tset
- [ ] macro calls / which macros are in scope
- [ ] .lua files (antifennel decompiler)
- [ ] setmetatable
- [ ] function arguments / function calls
- [ ] local/table mutation via set/tset
- [ ] .lua files (antifennel decompiler)
- [ ] mutation on aliased tables (difficult)
- [X] Reports compiler errors
- [ ] Reports linting issues
- [ ] Unused locals
- [ ] Discarding results from pcall/xpcall/other functions
- [ ] `unpack` or `values` into an operator special
- [ ] `do`/`values` with only one inner form
- [ ] redundant `do` as the last/only item in a form that accepts a "body"
- [ ] `var` forms that could be `local`
- [ ] Dead code (I'm not sure what sort of things cause dead code)
- [ ] Unused fields (difficult)
- [ ] unification in a `match` pattern (difficult)
- [ ] Brainstorm more linting patterns (I spent a couple minutes brainstorming these ideas, other ideas are welcome of course)
- [ ] Completion Suggestions
- [X] from globals
- [ ] from current scope
- [X] from current scope
- [ ] from macros (only on first form in a list)
- [ ] from specials (only on first form in a list)
- [ ] "dot completion" for table fields
@ -58,6 +46,19 @@ Features / To Do List / Things I would enjoy patches for:
- [ ] `(: "foo" :` string completions
- [ ] `(require :` module completions
- [ ] snippets? I guess?
- [X] Reports compiler errors
- [.] Report more than one error per top-level form
- [ ] Reports linting issues
- [ ] Unused locals
- [ ] Discarding results from pcall/xpcall/other functions
- [ ] `unpack` or `values` into an operator special
- [ ] `do`/`values` with only one inner form
- [ ] redundant `do` as the last/only item in a form that accepts a "body"
- [ ] `var` forms that could be `local`
- [ ] Dead code (I'm not sure what sort of things cause dead code)
- [ ] Unused fields (difficult)
- [ ] unification in a `match` pattern (difficult)
- [ ] Brainstorm more linting patterns (I spent a couple minutes brainstorming these ideas, other ideas are welcome of course)
- [X] Hover over a symbol for documentation
- [ ] Signature help
- [ ] Regular help
@ -75,9 +76,10 @@ Features / To Do List / Things I would enjoy patches for:
- [ ] lua version
- [ ] allowed global list
- [ ] enable/disable various linters
- [ ] rename local symbols
- [ ] rename module fields (may affect code behavior, may modify other files)
- [ ] rename arbitrary things (may affect code behavior, may modify other files)
- [ ] rename
- [ ] local symbols
- [ ] module fields (may affect code behavior, may modify other files)
- [ ] arbitrary fields (may affect code behavior, may modify other files)
- [ ] formatting with fnlfmt
- [ ] Type annotations? Global type inference?
@ -90,10 +92,11 @@ make
```
2. Configure your editor to use this language server
LSP is editor-agnostic, but that's only if you're able to actually follow the spec, and I'm not sure that fennel-ls is compliant.
LSP is editor-agnostic, but that's only if you're able to actually follow the spec, and I'm not sure if fennel-ls is compliant.
So far, I've only ever tested it with Neovim using the native language client and `lspconfig`.
If you know what that means, here's the relevant code to help you set up Neovim in the same way:
```lua
local lspconfig = require('lspconfig')
-- inform lspconfig about fennel-ls

96
fennel
View File

@ -3876,47 +3876,6 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
return nil
end
end
local function badend()
local accum = utils.map(stack, "closer")
local _218_
if (#stack == 1) then
_218_ = ""
else
_218_ = "s"
end
return parse_error(string.format("expected closing delimiter%s %s", _218_, string.char(unpack(accum))))
end
local function skip_whitespace(b)
if (b and whitespace_3f(b)) then
whitespace_since_dispatch = true
return skip_whitespace(getb())
elseif (not b and (0 < #stack)) then
return badend()
else
return b
end
end
local function parse_comment(b, contents)
if (b and (10 ~= b)) then
local function _222_()
local _221_ = contents
table.insert(_221_, string.char(b))
return _221_
end
return parse_comment(getb(), _222_())
elseif comments then
return dispatch(utils.comment(table.concat(contents), {line = (line - 1), filename = filename}))
else
return b
end
end
local function open_table(b)
if not whitespace_since_dispatch then
parse_error(("expected whitespace before opening delimiter " .. string.char(b)))
else
end
return table.insert(stack, {bytestart = byteindex, closer = delims[b], filename = filename, line = line, col = (col - 1)})
end
local function close_list(list)
return dispatch(setmetatable(list, getmetatable(utils.list())))
end
@ -3928,12 +3887,12 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
return dispatch(val)
end
local function add_comment_at(comments0, index, node)
local _225_ = (comments0)[index]
if (nil ~= _225_) then
local existing = _225_
local _218_ = (comments0)[index]
if (nil ~= _218_) then
local existing = _218_
return table.insert(existing, node)
elseif true then
local _ = _225_
local _ = _218_
comments0[index] = {node}
return nil
else
@ -4011,6 +3970,51 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
return close_curly_table(top)
end
end
local function badend(cause)
local accum = utils.map(stack, "closer")
local _228_
if (#stack == 1) then
_228_ = ""
else
_228_ = "s"
end
parse_error(string.format("expected closing delimiter%s %s", _228_, string.char(unpack(accum))))
for i = #accum, 2, -1 do
close_table(accum[i])
end
return accum[1]
end
local function skip_whitespace(b)
if (b and whitespace_3f(b)) then
whitespace_since_dispatch = true
return skip_whitespace(getb())
elseif (not b and (0 < #stack)) then
return badend("eof")
else
return b
end
end
local function parse_comment(b, contents)
if (b and (10 ~= b)) then
local function _232_()
local _231_ = contents
table.insert(_231_, string.char(b))
return _231_
end
return parse_comment(getb(), _232_())
elseif comments then
return dispatch(utils.comment(table.concat(contents), {line = (line - 1), filename = filename}))
else
return b
end
end
local function open_table(b)
if not whitespace_since_dispatch then
parse_error(("expected whitespace before opening delimiter " .. string.char(b)))
else
end
return table.insert(stack, {bytestart = byteindex, closer = delims[b], filename = filename, line = line, col = (col - 1)})
end
local function parse_string_loop(chars, b, state)
table.insert(chars, b)
local state0
@ -4043,7 +4047,7 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
table.insert(stack, {closer = 34})
local chars = {34}
if not parse_string_loop(chars, getb(), "base") then
badend()
badend("string")
else
end
table.remove(stack)

View File

@ -1,11 +1,16 @@
(local dispatch (require :fennel-ls.dispatch))
(local json-rpc (require :fennel-ls.json-rpc))
(local log (io.open "/home/xerool/Documents/projects/fennel-ls/log.txt" "w"))
(local {: view} (require :fennel))
(λ main-loop [in out]
(local send (partial json-rpc.write out))
(local state [])
(while true
(let [msg (json-rpc.read in)]
(log:write (view msg) "\n")
(log:flush)
(dispatch.handle state send msg))))
(λ main []

View File

@ -124,6 +124,13 @@ later by fennel-ls.language to answer requests from the client."
[-require- modname]
(tset require-calls ast true)))
(λ recoverable? [msg]
(or (msg:find "unknown identifier in strict mode")
(msg:find "expected closing delimiter")
(msg:find "expected body expression")
(msg:find "expected whitespace before opening delimiter")
(msg:find "malformed multisym")))
(λ on-compile-error [_ msg ast call-me-to-reset-the-compiler]
(let [range (or (message.ast->range ast file)
(message.pos->range 0 0 0 0))]
@ -133,8 +140,11 @@ later by fennel-ls.language to answer requests from the client."
:severity message.severity.ERROR
:code 201
:codeDescription "compiler error"}))
(call-me-to-reset-the-compiler)
(error "__NOT_AN_ERROR"))
(if (recoverable? msg)
true
(do
(call-me-to-reset-the-compiler)
(error "__NOT_AN_ERROR"))))
(λ on-parse-error [msg file line byte]
;; assume byte and char count is the same, ie no UTF-8
@ -143,11 +153,16 @@ later by fennel-ls.language to answer requests from the client."
{:range range
:message msg
:severity message.severity.ERROR
:code 201
:codeDescription "compiler error"}))
(error "__NOT_AN_ERROR"))
:code 101
:codeDescription "parse error"}))
(if (recoverable? msg)
true
(do
(print msg)
(error "__NOT_AN_ERROR"))))
(local allowed-globals (icollect [k v (pairs _G)] k))
(table.insert allowed-globals :vim)
;; TODO clean up this code. It's awful now that there is error handling
(let

View File

@ -408,10 +408,10 @@ package.preload["fennel.repl"] = package.preload["fennel.repl"] or function(...)
_676_ = _677_
end
end
if ((_G.type(_676_) == "table") and (nil ~= (_676_).source) and ((_676_).what == "Lua") and (nil ~= (_676_).short_src) and (nil ~= (_676_).linedefined)) then
if ((_G.type(_676_) == "table") and ((_676_).what == "Lua") and (nil ~= (_676_).linedefined) and (nil ~= (_676_).source) and (nil ~= (_676_).short_src)) then
local line = (_676_).linedefined
local source = (_676_).source
local src = (_676_).short_src
local line = (_676_).linedefined
local fnlsrc
do
local t_681_ = compiler.sourcemap
@ -3662,47 +3662,6 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
return nil
end
end
local function badend()
local accum = utils.map(stack, "closer")
local _218_
if (#stack == 1) then
_218_ = ""
else
_218_ = "s"
end
return parse_error(string.format("expected closing delimiter%s %s", _218_, string.char(unpack(accum))))
end
local function skip_whitespace(b)
if (b and whitespace_3f(b)) then
whitespace_since_dispatch = true
return skip_whitespace(getb())
elseif (not b and (0 < #stack)) then
return badend()
else
return b
end
end
local function parse_comment(b, contents)
if (b and (10 ~= b)) then
local function _222_()
local _221_ = contents
table.insert(_221_, string.char(b))
return _221_
end
return parse_comment(getb(), _222_())
elseif comments then
return dispatch(utils.comment(table.concat(contents), {line = (line - 1), filename = filename}))
else
return b
end
end
local function open_table(b)
if not whitespace_since_dispatch then
parse_error(("expected whitespace before opening delimiter " .. string.char(b)))
else
end
return table.insert(stack, {bytestart = byteindex, closer = delims[b], filename = filename, line = line, col = (col - 1)})
end
local function close_list(list)
return dispatch(setmetatable(list, getmetatable(utils.list())))
end
@ -3714,12 +3673,12 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
return dispatch(val)
end
local function add_comment_at(comments0, index, node)
local _225_ = (comments0)[index]
if (nil ~= _225_) then
local existing = _225_
local _218_ = (comments0)[index]
if (nil ~= _218_) then
local existing = _218_
return table.insert(existing, node)
elseif true then
local _ = _225_
local _ = _218_
comments0[index] = {node}
return nil
else
@ -3797,6 +3756,51 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
return close_curly_table(top)
end
end
local function badend(cause)
local accum = utils.map(stack, "closer")
local _228_
if (#stack == 1) then
_228_ = ""
else
_228_ = "s"
end
parse_error(string.format("expected closing delimiter%s %s", _228_, string.char(unpack(accum))))
for i = #accum, 2, -1 do
close_table(accum[i])
end
return accum[1]
end
local function skip_whitespace(b)
if (b and whitespace_3f(b)) then
whitespace_since_dispatch = true
return skip_whitespace(getb())
elseif (not b and (0 < #stack)) then
return badend("eof")
else
return b
end
end
local function parse_comment(b, contents)
if (b and (10 ~= b)) then
local function _232_()
local _231_ = contents
table.insert(_231_, string.char(b))
return _231_
end
return parse_comment(getb(), _232_())
elseif comments then
return dispatch(utils.comment(table.concat(contents), {line = (line - 1), filename = filename}))
else
return b
end
end
local function open_table(b)
if not whitespace_since_dispatch then
parse_error(("expected whitespace before opening delimiter " .. string.char(b)))
else
end
return table.insert(stack, {bytestart = byteindex, closer = delims[b], filename = filename, line = line, col = (col - 1)})
end
local function parse_string_loop(chars, b, state)
table.insert(chars, b)
local state0
@ -3829,7 +3833,7 @@ package.preload["fennel.parser"] = package.preload["fennel.parser"] or function(
table.insert(stack, {closer = 34})
local chars = {34}
if not parse_string_loop(chars, getb(), "base") then
badend()
badend("string")
else
end
table.remove(stack)

View File

@ -10,7 +10,7 @@
(local dispatch (require :fennel-ls.dispatch))
(local message (require :fennel-ls.message))
(local filename (.. ROOT-URI "imaginary-file.fnl"))
(local filename (.. ROOT-URI "/imaginary-file.fnl"))
(fn check-completion [body line col expected ?unexpected]
(local state (doto [] setup-server))
@ -18,8 +18,9 @@
(let [response (dispatch.handle* state (completion-at filename line col))
seen (collect [_ suggestion (ipairs (. response 1 :result))]
suggestion.label suggestion.label)]
(each [_ exp (ipairs expected)]
(is.truthy (. seen exp) (.. exp " was not suggested, but should be")))
(if (= (type expected) :table)
(each [_ exp (ipairs expected)]
(is.truthy (. seen exp) (.. exp " was not suggested, but should be"))))
(if ?unexpected
(each [_ exp (ipairs ?unexpected)]
(is.nil (. seen exp) (.. exp " was suggested, but shouldn't be"))))))
@ -42,17 +43,33 @@
(check-completion "(d)" 0 3 [:do :doto]))
(it "suggests macros in scope"
(check-completion "(macro funny [] `nil)\n()" 1 1 [:funny])))
(check-completion "(macro funny [] `nil)\n()" 1 1 [:funny]))
;; ;; Compiler hardening
;; (it "works without requiring the close parentheses"))
;; (it "works without a body in the `let`"))
;; (it "does not suggest locals out of scope")
;; (it "suggests items from the previous definitions in the same `let`")
(it "does not suggest locals out of scope"
(check-completion "(do (local x 10))\n" 1 0 [] [:x]))
;; ;; Functions
;; (it "suggests function arguments at the top scope of the function")
;; (it "suggests function arguments deep within the function")
(it "does not suggest function args out of scope"
(check-completion "(fn [x] (print x))\n" 1 0 [] [:x]))
(describe "when the program doesn't compile"
(it "still completes without requiring the close parentheses"
(check-completion "(fn foo [z]\n (let [x 10 y 20]\n " 1 2 [:x :y :z]))
(it "still completes with no body in the `let`"
(check-completion "(let [x 10 y 20]\n )" 1 2 [:x :y]))
(it "still completes items from the previous definitions in the same `let`"
(check-completion "(let [a 10\n b 20\n " 1 6 [:a :b])))
;; (it "completes fields with a partially typed multisym that ends in :"
;; (check-completion "(local x {:field (fn [])})\n(x:" 1 3 [:field])))
;; Functions
(it "suggests function arguments at the top scope of the function"
(check-completion "(fn foo [arg1 arg2 arg3]\n )" 1 2 [:arg1 :arg2 :arg3]))
(it "suggests function arguments at the top scope of the function"
(check-completion "(fn foo [arg1 arg2 arg3]\n (do (do (do ))))" 1 14 [:arg1 :arg2 :arg3])))
;; ;; Scope Ordering Rules
;; (it "does not suggest locals past the suggestion location when a symbol is partially typed")
@ -67,7 +84,6 @@
;; (check-completion "(do )"
;; 0 4 [] [:do :let :fn :-> :-?>> :?.])))
;; (it "doesn't suggest specials at the very top level")
;; (it "doesn't suggest macros in the middle of a list (open paren required)")
;; (it "doesn't suggest macros at the very top level")

View File

@ -20,7 +20,7 @@
(table.insert t result)
`(accumulate ,t ,body))
(local filename (.. ROOT-URI "imaginary.fnl"))
(local filename (.. ROOT-URI "/imaginary.fnl"))
(describe "diagnostic messages"
(it "handles compile errors"
@ -59,7 +59,13 @@
{:diagnostics
[{:range {:start {:character a :line b}
:end {:character c :line d}}}]}}]
"diagnostics should always have a range"))))
"diagnostics should always have a range")))
(it "gives more than one error"
(local state (doto [] setup-server))
(let [responses (open-file state filename "(unknown-global-1 unknown-global-2)")]
(is-matching responses
[{:params {:diagnostics [a b]}}] "there should be a diagnostic for each one here"))))
;; TODO lints:
;; unnecessary (do) in body position

View File

@ -1,4 +1,5 @@
(import-macros {: is-matching : describe : it : before-each} :test)
(local is (require :luassert))
(local {: ROOT-URI
@ -72,6 +73,7 @@
(check :goto-definition.fnl 47 13 :goto-definition.fnl 47 30 47 52)))
;; TODO
;; (it "doesn't leak function arguments to the surrounding scope")
;; (it "can go to a function in another file imported via destructuring assignment") ;; WORKS, just needs a test case
;; (it "can go through more than one extra file")
;; (it "will give up instead of freezing on recursive requires")
@ -79,11 +81,11 @@
;; (it "can follow import-macros (destructuring)")
;; (it "can follow import-macros (namespaced)")
;; (it "can go to the definition even in a lua file")
;; (it "finds (fn a.b [] ...) declarations")
;; (it "finds (set a.b) definitions")
;; (it "finds (fn a.b [] ...) declarations")
;; (it "finds (tset a :b) definitions")
;; (it "finds (setmetatable a {__index {:b def}) definitions")
;; (it "finds definitions from inside a function (fn foo [] (local x 10) {: x}) (let [result (foo)] (print result.x)) finds result.x")
;; (it "finds definitions into a function (fn foo [] (local x 10) {: x}) (let [result (foo)] (print result.x)) finds result.x")
;; (it "finds basic setmetatable definitions with an __index function")
;; (it "can return to callsite and go through a function's arguments when they're available")
;; (it "can go to a function's reference OR read type inference comments when callsite isn't available (PICK ONE)")

View File

@ -2,18 +2,24 @@
(fn it [desc ...]
"busted's `it` function"
`((. (require :busted) :it)
,desc (fn [] ,desc ,...)))
(let [body [...]]
(table.insert body `nil)
`((. (require :busted) :it)
,desc (fn [] ,desc ,(unpack body)))))
(fn describe [desc ...]
"busted's `describe` function"
`((. (require :busted) :describe)
,desc (fn [] ,desc ,...)))
(let [body [...]]
(table.insert body `nil)
`((. (require :busted) :describe)
,desc (fn [] ,desc ,(unpack body)))))
(fn before-each [...]
"busted's `describe` function"
`((. (require :busted) :before_each)
(fn [] ,...)))
(let [body [...]]
(table.insert body `nil)
`((. (require :busted) :before_each)
(fn [] ,(unpack body)))))
(fn is-matching [item pattern ?msg]