From 59b966a25fa58985efcdb2f4f55918dcc70a2f61 Mon Sep 17 00:00:00 2001 From: XeroOl Date: Thu, 28 Mar 2024 23:51:22 -0500 Subject: [PATCH] Add a bunch of tests for compiler recovery Ideally, any time you write: (local x ()) (print x) I don't want it to say "unknown variable x". To make sure that's the case, I need to do as much error recovery as possible. I don't have full error recovery, but I've got the common cases, and I'm working down the fennel.friends list --- src/fennel-ls/compiler.fnl | 21 ++++++++++- src/fennel-ls/handlers.fnl | 6 +--- test/diagnostic.fnl | 74 ++++++++++++++++++++++++++++++++++---- 3 files changed, 88 insertions(+), 13 deletions(-) diff --git a/src/fennel-ls/compiler.fnl b/src/fennel-ls/compiler.fnl index 6ee7172..ac0f9ba 100644 --- a/src/fennel-ls/compiler.fnl +++ b/src/fennel-ls/compiler.fnl @@ -232,25 +232,41 @@ identifiers are declared / referenced in which places." (where [sym] (multisym? sym) (: (tostring sym) :find ":")) (reference sym scope :read))) + (local defer []) + (λ attempt-to-recover! [msg ?ast] (or (= 1 (msg:find "unknown identifier")) + (= 1 (msg:find "local %S+ was overshadowed by a special form or macro")) + (= 1 (msg:find "expected var ")) + (= 1 (msg:find "cannot call literal value")) + (= 1 (msg:find "unexpected vararg")) (= 1 (msg:find "expected closing delimiter")) (= 1 (msg:find "expected body expression")) + (= 1 (msg:find ".*fennel/macros.fnl:%d+: expected body")) (= 1 (msg:find "expected condition and body")) (= 1 (msg:find "expected whitespace before opening delimiter")) (= 1 (msg:find "malformed multisym")) (= 1 (msg:find "expected at least one pattern/body pair")) (= 1 (msg:find "module not found")) + (= 1 (msg:find "expected even number of values in table literal")) + (= 1 (msg:find "use $%.%.%. in hashfn")) (when (and (= 1 (msg:find "expected even number of name/value bindings")) (sequence? ?ast) (= 1 (% (length ?ast) 2))) (table.insert ?ast (sym :nil)) + (table.insert defer #(table.remove ?ast)) true) (when (and (= 1 (msg:find "expected a function, macro, or special to call")) (list? ?ast) (= (length ?ast) 0)) (table.insert ?ast (sym :do)) - true))) + (table.insert defer #(table.remove ?ast)) + true) + (when (= 1 (msg:find "unexpected multi symbol")) + (let [old (tostring ?ast)] + (tset ?ast 1 "!!invalid-multi-symbol!!") + (table.insert defer #(tset ?ast 1 old)) + true)))) (λ on-compile-error [_ msg ast call-me-to-reset-the-compiler] @@ -353,6 +369,9 @@ identifiers are declared / referenced in which places." (set fennel.macro-path old-macro-path)) + (each [_ cmd (ipairs defer)] + (cmd)) + (set file.ast ast) (set file.calls calls) (set file.lexical lexical) diff --git a/src/fennel-ls/handlers.fnl b/src/fennel-ls/handlers.fnl index 07db3d4..39018da 100644 --- a/src/fennel-ls/handlers.fnl +++ b/src/fennel-ls/handlers.fnl @@ -143,11 +143,7 @@ Every time the client sends a message, it gets handled by a function in the corr ?parent (. parents 1) result [] in-call-position? (and (fennel.list? ?parent) - (or (= ?symbol (. ?parent 1)) - ;; so, this is unfortunate - (and (= ?symbol nil) - (fennel.sym? (. ?parent 1) :do) - (= (. ?parent 1 :bytestart) nil))))] + (= ?symbol (. ?parent 1)))] (collect-scope scope :manglings #(doto (make-completion-item self file $ scope) (tset :kind kinds.Variable)) result) (when in-call-position? diff --git a/test/diagnostic.fnl b/test/diagnostic.fnl index 260192a..b7d6c64 100644 --- a/test/diagnostic.fnl +++ b/test/diagnostic.fnl @@ -57,23 +57,83 @@ nil) (fn test-multiple-errors [] - (check "(unknown-global-1 unknown-global-2)" - [{:message "unknown identifier: unknown-global-1"} - {:message "unknown identifier: unknown-global-2"}] []) - (check "(let [x unknown-global" + ;; compiler recovery for every fennel.friend error + + (check "(let [x.y.z 10] + (print +))" + [{:message "unexpected multi symbol x.y.z"} + {:message "tried to reference a special form without calling it"}] []) + + ;; use of global .* is aliased by a local + + (check "(let [+ 10] + (print +))" + [{:message "local + was overshadowed by a special form or macro"} + {:message "tried to reference a special form without calling it"}] []) + + (check "(let [x 10] + (set x 20) + (print x +))" + [{:message "expected var x"} + {:message "tried to reference a special form without calling it"}] []) + + ;; expected macros to be table + ;; expected each macro to be a function + ;; macro tried to bind .* without gensym + + (check "(do unknown-global + (print +))" [{:message "unknown identifier: unknown-global"} - {:message "expected body expression"} - {:message "expected closing delimiters )]"}] []) - ;; recovers from () + {:message "tried to reference a special form without calling it"}] []) + (check "(let [x ()] (print x +))" [{:message "expected a function, macro, or special to call"} {:message "tried to reference a special form without calling it"}] []) + + (check "(let [x (3)] + (print x +))" + [{:message "cannot call literal value 3"} + {:message "tried to reference a special form without calling it"}] []) + + (check "(let [x (fn [] ...)] + (print x +))" + [{:message "unexpected vararg"} + {:message "tried to reference a special form without calling it"}] []) + + (check "(let [x #...] + (print x +))" + [{:message "use $... in hashfn"} + {:message "tried to reference a special form without calling it"}] []) + + (check "(let [x unknown-global" + [{:message "unknown identifier: unknown-global"} + {:message "expected body expression"} + {:message "expected closing delimiters )]"}] []) ;; recovers from mismatched let (check "(let [x] (print x +))" [{:message "expected even number of name/value bindings"} {:message "tried to reference a special form without calling it"}] []) + ;; recovers from missing condition (if) + (check "(let [x (if)] + (print x +))" + [{:message "expected condition and body"} + {:message "tried to reference a special form without calling it"}] []) + ; recovers from missing condition (when) + (check "(let [x (when)] + (print x +))" + [{:message #($:find ".*macros.fnl:%d+: expected body")} + {:message "tried to reference a special form without calling it"}] []) + ;; recovers from missing body (when) + (check "(let [x (when (< (+ 9 10) 21))] + (print x +))" + [{:message #($:find ".*macros.fnl:%d+: expected body")} + {:message "tried to reference a special form without calling it"}] []) + (check "(let [x {:mismatched :curly :braces}] + (print x +))" + [{:message "expected even number of values in table literal"} + {:message "tried to reference a special form without calling it"}] []) nil) {: test-compile-error