Reject a malformed catch clause with a clean error in both modes
jolt's catch is (catch class binding body*); the binding (3rd element) must be a symbol. Neither the analyzer nor the interpreter validated it, so a non-symbol binding crashed with an internal Janet error (expected integer key for array...) and, in the interpreter, a malformed clause whose body never threw was silently swallowed (returned the try value). Clojure rejects a non-class/non-symbol catch clause; match that with an up-front error in analyze-try and eval-try. Surfaced building the Chez try/throw emit. Regression rows in exceptions-spec (runs x3 modes) plus a unit test asserting the clean message in interpret and compile. jolt-kg6p.
This commit is contained in:
parent
ffa122440a
commit
d35783bf1b
4 changed files with 67 additions and 1 deletions
|
|
@ -162,6 +162,12 @@
|
|||
(cond
|
||||
(= hname "catch")
|
||||
(let [cl (vec (form-elements c))]
|
||||
;; (catch class binding body*) — binding (3rd elem) MUST be a symbol.
|
||||
;; Validate eagerly (plain throw, NOT uncompilable, so it's a real
|
||||
;; error rather than a compile->interpret punt) instead of letting
|
||||
;; form-sym-name crash on a non-symbol.
|
||||
(when (or (< (count cl) 3) (not (form-sym? (nth cl 2))))
|
||||
(throw "Unable to parse catch clause; expected (catch class binding body*)"))
|
||||
(reset! catch-sym (form-sym-name (nth cl 2)))
|
||||
(reset! catch-body (drop 3 cl)))
|
||||
(= hname "finally")
|
||||
|
|
|
|||
|
|
@ -435,6 +435,13 @@
|
|||
(when (and (struct? head) (= :symbol (head :jolt/type)))
|
||||
(match (head :name)
|
||||
"catch" (do
|
||||
# (catch class binding body*) — binding (3rd elem) must
|
||||
# be a symbol. Validate up front (even when the body
|
||||
# never throws) so a malformed clause is a clean error,
|
||||
# not a silent nil or an internal crash when caught.
|
||||
(let [b (when (>= (length clause) 3) (in clause 2))]
|
||||
(unless (and b (struct? b) (= :symbol (b :jolt/type)))
|
||||
(error "Unable to parse catch clause; expected (catch class binding body*)")))
|
||||
(set catch-sym (in clause 2))
|
||||
(set catch-body (tuple/slice clause 3)))
|
||||
"finally" (set finally-body (tuple/slice clause 1)))))))
|
||||
|
|
|
|||
|
|
@ -15,7 +15,12 @@
|
|||
["finally runs on throw" "9"
|
||||
"(let [a (atom 0)] (try (throw (ex-info \"x\" {})) (catch :default e nil) (finally (reset! a 9))) @a)"]
|
||||
["catch value of body" "5"
|
||||
"(try (+ 2 3) (catch :default e 0))"])
|
||||
"(try (+ 2 3) (catch :default e 0))"]
|
||||
# a malformed catch clause (binding not a symbol, or clause too short) is a
|
||||
# clean error, not a silent nil or an internal crash (jolt-kg6p).
|
||||
["malformed catch: non-symbol binding" :throws "(try 1 (catch e (* e 10)))"]
|
||||
["malformed catch: literal binding" :throws "(try 1 (catch e 5))"]
|
||||
["malformed catch: too short" :throws "(try 1 (catch Exception))"])
|
||||
|
||||
(defspec "exceptions / assert"
|
||||
["assert true -> ok" ":ok" "(do (assert true) :ok)"]
|
||||
|
|
|
|||
48
test/unit/try-catch-validation-test.janet
Normal file
48
test/unit/try-catch-validation-test.janet
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# A malformed catch clause must be rejected with a clean, Clojure-like error in
|
||||
# BOTH the interpreter and the compiler — not an internal Janet crash ("expected
|
||||
# integer key for array …") and not silently swallowed. jolt's catch is
|
||||
# (catch Class binding body*); the binding (3rd element) must be a symbol.
|
||||
# Regression for jolt-kg6p (surfaced building the Chez try/throw emit).
|
||||
(import ../../src/jolt/api :as api)
|
||||
|
||||
(var total 0) (var fails 0)
|
||||
(defn ok [name pred &opt extra]
|
||||
(++ total)
|
||||
(if pred (printf "ok: %s" name)
|
||||
(do (++ fails) (printf "FAIL: %s %s" name (or extra "")))))
|
||||
|
||||
(defn err-msg [ctx s]
|
||||
(let [r (protect (api/eval-string ctx s))]
|
||||
(if (r 0) :no-error (string (r 1)))))
|
||||
|
||||
(defn val-of [ctx s] (api/eval-string ctx s))
|
||||
|
||||
# malformed: the binding position holds a non-symbol (a call form / a literal),
|
||||
# or the clause is too short. Each must raise a clean error mentioning catch,
|
||||
# and must NOT leak the internal Janet indexing crash.
|
||||
(def malformed
|
||||
["(try 1 (catch e (* e 10)))"
|
||||
"(try 1 (catch e 5))"
|
||||
"(try 1 (catch Exception))"])
|
||||
|
||||
# well-formed catch still works (class is a symbol or :default; binding a symbol).
|
||||
(def wellformed
|
||||
[["(try (throw 7) (catch Exception e (* e 10)))" 70]
|
||||
["(try (throw 42) (catch :default e e))" 42]
|
||||
["(try (+ 2 3) (catch :default e 0))" 5]])
|
||||
|
||||
(each [mode opts] [["interpret" {}] ["compile" {:compile? true}]]
|
||||
(def ctx (api/init opts))
|
||||
(each s malformed
|
||||
(def m (err-msg ctx s))
|
||||
(ok (string mode " rejects: " s)
|
||||
(and (not= m :no-error)
|
||||
(string/find "catch" m)
|
||||
(not (string/find "expected integer key" m)))
|
||||
(string "msg=" m)))
|
||||
(each [s want] wellformed
|
||||
(ok (string mode " ok: " s) (= want (val-of ctx s))
|
||||
(string "got " (val-of ctx s)))))
|
||||
|
||||
(printf "\ntry-catch-validation: %d/%d passed" (- total fails) total)
|
||||
(when (> fails 0) (error (string fails " failing")))
|
||||
Loading…
Add table
Add a link
Reference in a new issue