diff --git a/README.md b/README.md index 012f571..9a87a02 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # The Parsatron +Fork of Nate's Excellent Parsatron library. + Born from Haskell's Parsec library, The Parsatron is a functional parser library. The Parsatron provides a lot of very small functions that can be combined into larger ones to very quickly write parsers for languages. @@ -16,7 +18,7 @@ with it. You can use The Parsatron by including - [the/parsatron "0.0.8"] +[![Clojars Project](http://clojars.org/org.clojars.quoll/parsatron/latest-version.svg)](http://clojars.org/org.clojars.quoll/parsatron) in your `project.clj` dependencies. It's available for download from Clojars. diff --git a/project.clj b/project.clj index 6ad402c..7dd1be6 100644 --- a/project.clj +++ b/project.clj @@ -1,18 +1,29 @@ -(defproject the/parsatron "0.0.9-SNAPSHOT" +(defproject org.clojars.quoll/parsatron "0.0.10" :description "Clojure parser combinators" - :dependencies [[org.clojure/clojure "1.10.1"] - [org.clojure/clojurescript "1.10.520"]] + :dependencies [[org.clojure/clojure "1.10.2"]] - :plugins [[lein-cljsbuild "1.1.7"]] + :plugins [[lein-cljsbuild "1.1.8"]] - :source-paths ["src/clj" "src/cljs"] - :test-paths ["test/clj"] + :source-paths ["src"] + :test-paths ["test"] :global-vars {*warn-on-reflection* false} - :cljsbuild {:builds [{:source-paths ["src/cljs" "test/cljs"] - :compiler {:optimizations :simple - :target :nodejs - :output-to "test/resources/parsatron_test.js"}}] - :test-commands { "unit" ["node" "test/resources/parsatron_test.js"]}}) + :profiles { + :dev { + :dependencies [[org.clojure/clojurescript "1.10.773"]]}} + + :cljsbuild { + :builds { + :dev { + :source-paths ["src"] + :compiler {:optimizations :simple + :target :nodejs + :output-to "target/parsatron.js"}} + :test { + :source-paths ["src" "test"] + :compiler {:optimizations :simple + :target :nodejs + :output-to "test/resources/parsatron_test.js"}} } + :test-commands {"unit" ["node" "test/resources/parsatron_test.js"]}}) diff --git a/src/clj/parsatron/languages/bf.clj b/src/clj/parsatron/languages/bf.clj deleted file mode 100644 index f5b101b..0000000 --- a/src/clj/parsatron/languages/bf.clj +++ /dev/null @@ -1,16 +0,0 @@ -(ns parsatron.languages.bf - (:refer-clojure :exclude [char]) - (:use [the.parsatron])) - -(defparser instruction [] - (choice (char \>) - (char \<) - (char \+) - (char \-) - (char \.) - (char \,) - (between (char \[) (char \]) (many (instruction))))) - -(defparser bf [] - (many (instruction)) - (eof)) diff --git a/src/cljs/the/parsatron.cljs b/src/cljs/the/parsatron.cljs deleted file mode 100644 index 2b15e41..0000000 --- a/src/cljs/the/parsatron.cljs +++ /dev/null @@ -1,301 +0,0 @@ -(ns the.parsatron - (:refer-clojure :exclude [char char?]) - (:require [clojure.string :as str]) - (:require-macros [the.parsatron :refer [defparser >> let->>]])) - -(defrecord InputState [input pos]) -(defrecord SourcePos [line column]) - -(defrecord Continue [fn]) -(defrecord Ok [item]) -(defrecord Err [errmsg]) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; position -(defn inc-sourcepos - "Increment the source position by a single character, c. On newline, - increments the SourcePos's line number and resets the column, on - all other characters, increments the column" - [{:keys [line column]} c] - (if (= c \newline) - (SourcePos. (inc line) 1) - (SourcePos. line (inc column)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; errors -(defprotocol ShowableError - (show-error [this])) - -(defrecord ParseError [pos msgs] - ShowableError - (show-error [_] (str (str/join ", " msgs) - " at" - " line: " (:line pos) - " column: " (:column pos)))) - -(defn unknown-error [{:keys [pos] :as state}] - (ParseError. pos ["Error"])) - -(defn unexpect-error [msg pos] - (ParseError. pos [(str "Unexpected " msg)])) - -(defn expect-error [msg pos] - (ParseError. pos [(str "Expected " msg)])) - -(defn merge-errors [{:keys [pos] :as err} other-err] - (ParseError. pos (flatten (concat (:msgs err) (:msgs other-err))))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; trampoline -(defn parsatron-poline - "A trampoline for executing potentially stack-blowing recursive - functions without running out of stack space. This particular - trampoline differs from clojure.core/trampoline by requiring - continuations to be wrapped in a Continue record. Will loop until - the value is no longer a Continue record, returning that." - [f & args] - (loop [value (apply f args)] - (condp instance? value - Continue (recur ((:fn value))) - value))) - -(defn sequentially [f value] - (condp instance? value - Continue (Continue. #(sequentially f ((:fn value)))) - (f value))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; host environment -(defn fail [message] - (js/Error. message)) - -(defn char? - "Test for a single-character string. - - ClojureScript doesn't support a character type, so we pretend it - does" - [x] - (and (string? x) (= (count x) 1))) - -(defn digit? - "Tests if a character is a digit: [0-9]" - [c] - (re-matches #"\d" c)) - -(defn letter? - "Tests if a character is a letter: [a-zA-Z]" - [c] - (re-matches #"[a-zA-Z]" c)) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; m -(defn always - "A parser that always succeeds with the value given and consumes no - input" - [x] - (fn [state cok cerr eok eerr] - (eok x state))) - -(defn bind - "Parse p, and then q. The function f must be of one argument, it - will be given the value of p and must return the q to follow p" - [p f] - (fn [state cok cerr eok eerr] - (letfn [(pcok [item state] - (sequentially - (fn [q] (Continue. #(q state cok cerr cok cerr))) - (f item))) - (peok [item state] - (sequentially - (fn [q] (Continue. #(q state cok cerr eok eerr))) - (f item)))] - (Continue. #(p state pcok cerr peok eerr))))) - -(defn nxt - "Parse p and then q, returning q's value and discarding p's" - [p q] - (bind p (fn [_] q))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; m+ -(defn never - "A parser that always fails, consuming no input" - [] - (fn [state cok cerr eok eerr] - (eerr (unknown-error state)))) - -(defn either - "A parser that tries p, upon success, returning its value, and upon - failure (if no input was consumed) tries to parse q" - [p q] - (fn [state cok cerr eok eerr] - (letfn [(peerr [err-from-p] - (letfn [(qeerr [err-from-q] - (eerr (merge-errors err-from-p err-from-q)))] - (Continue. #(q state cok cerr eok qeerr))))] - (Continue. #(p state cok cerr eok peerr))))) - -(defn attempt - "A parser that will attempt to parse p, and upon failure never - consume any input" - [p] - (fn [state cok cerr eok eerr] - (Continue. #(p state cok eerr eok eerr)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; interacting with the parser's state - -(defn extract - "Extract information from the Parser's current state. f should be a - fn of one argument, the parser's current state, and any value that - it deems worthy of returning will be returned by the entire parser. - No input is consumed by this parser, and the state itself is not - altered." - [f] - (fn [state _ _ eok _] - (eok (f state) state))) - -(defn examine - "Return the Parser's current state" - [] - (extract identity)) - -(defn lineno - "A parser that returns the current line number. It consumes no input" - [] - (extract (comp :line :pos))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; token -(defn token - "Consume a single item from the head of the input if (consume? item) - is not nil. This parser will fail to consume if either the consume? - test returns nil or if the input is empty" - [consume?] - (fn [{:keys [input pos] :as state} cok cerr eok eerr] - (if-not (empty? input) - (let [tok (first input)] - (if (consume? tok) - (cok tok (InputState. (rest input) (inc-sourcepos pos tok))) - (eerr (unexpect-error (str "token '" tok "'") pos)))) - (eerr (unexpect-error "end of input" pos))))) - -(defn many - "Consume zero or more p. A RuntimeException will be thrown if this - combinator is applied to a parser that accepts the empty string, as - that would cause the parser to loop forever" - [p] - (letfn [(many-err [_ _] - (fail "Combinator '*' is applied to a parser that accepts an empty string")) - (safe-p [state cok cerr eok eerr] - (Continue. #(p state cok cerr many-err eerr)))] - (either - (let->> [x safe-p - xs (many safe-p)] - (always (cons x xs))) - (always [])))) - -(defn times - "Consume exactly n number of p" - [n p] - (if (= n 0) - (always []) - (let->> [x p - xs (times (dec n) p)] - (always (cons x xs))))) - -(defn lookahead - "A parser that upon success consumes no input, but returns what was - parsed" - [p] - (fn [state cok cerr eok eerr] - (letfn [(ok [item _] - (eok item state))] - (Continue. #(p state ok cerr eok eerr))))) - -(defn choice - "A varargs version of either that tries each given parser in turn, - returning the value of the first one that succeeds" - [& parsers] - (if (empty? parsers) - (never) - (let [p (first parsers)] - (either p (apply choice (rest parsers)))))) - -(defn eof - "A parser to detect the end of input. If there is nothing more to - consume from the underlying input, this parser suceeds with a nil - value, otherwise it fails" - [] - (fn [{:keys [input pos] :as state} cok cerr eok eerr] - (if (empty? input) - (eok nil state) - (eerr (expect-error "end of input" pos))))) - -(defn char - "Consume the given character" - [c] - (token #(= c %))) - -(defn any-char - "Consume any character" - [] - (token #(char? %))) - -(defn digit - "Consume a digit [0-9] character" - [] - (token digit?)) - -(defn letter - "Consume a letter [a-zA-Z] character" - [] - (token letter?)) - -(defn string - "Consume the given string" - [s] - (reduce nxt (concat (map char s) - (list (always s))))) - -(defn between - "Parse p after parsing open and before parsing close, returning the - value of p and discarding the values of open and close" - [open close p] - (let->> [_ open - x p - _ close] - (always x))) - -(defn many1 - "Consume 1 or more p" - [p] - (let->> [x p - xs (many p)] - (always (cons x xs)))) - -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; run parsers -(defn run-parser - "Execute a parser p, given some state, Returns Ok or Err" - [p state] - (parsatron-poline p state - (fn cok [item _] - (Ok. item)) - (fn cerr [err] - (Err. (show-error err))) - (fn eok [item _] - (Ok. item)) - (fn eerr [err] - (Err. (show-error err))))) - -(defn run - "Run a parser p over some input. The input can be a string or a seq - of tokens, if the parser produces an error, its message is wrapped - in a RuntimeException and thrown, and if the parser succeeds, its - value is returned" - [p input] - (let [result (run-parser p (InputState. input (SourcePos. 1 1)))] - (condp instance? result - Ok (:item result) - Err (throw (fail ^String (:errmsg result)))))) diff --git a/src/clj/parsatron/languages/bencode.clj b/src/parsatron/languages/bencode.cljc similarity index 58% rename from src/clj/parsatron/languages/bencode.clj rename to src/parsatron/languages/bencode.cljc index 6ed9a83..48fae75 100644 --- a/src/clj/parsatron/languages/bencode.clj +++ b/src/parsatron/languages/bencode.cljc @@ -1,38 +1,44 @@ (ns parsatron.languages.bencode - (:refer-clojure :exclude [char]) - (:use [the.parsatron])) + (:require + #?(:clj [the.parsatron + :refer [defparser digit ch any-char many many1 always either between choice let->> >> times]] + :cljs [the.parsatron + :refer [digit ch any-char many many1 always either between choice times] + :refer-macros [let->> >> defparser]]))) (declare ben-value) +(def to-int #?(:clj read-string :cljs int)) + (defparser positive-int [] (let->> [digits (many1 (digit))] - (always (read-string (apply str digits))))) + (always (to-int (apply str digits))))) (defparser negative-int [] - (let->> [digits (>> (char \-) (many1 (digit)))] - (always (read-string (apply str digits))))) + (let->> [digits (>> (ch \-) (many1 (digit)))] + (always (to-int (apply str digits))))) (defparser ben-integer [] - (between (char \i) (char \e) + (between (ch \i) (ch \e) (either (positive-int) (negative-int)))) (defparser ben-bytestring [] (let->> [length (positive-int) - _ (char \:) + _ (ch \:) chars (times length (any-char))] (always (apply str chars)))) (defparser ben-list [] - (between (char \l) (char \e) + (between (ch \l) (ch \e) (many (ben-value)))) (defparser ben-dictionary [] (let [entry (let->> [key (ben-bytestring) val (ben-value)] (always [key val]))] - (between (char \d) (char \e) + (between (ch \d) (ch \e) (let->> [entries (many entry)] (always (into (sorted-map) entries)))))) diff --git a/src/parsatron/languages/bf.cljc b/src/parsatron/languages/bf.cljc new file mode 100644 index 0000000..e76a749 --- /dev/null +++ b/src/parsatron/languages/bf.cljc @@ -0,0 +1,18 @@ +(ns parsatron.languages.bf + (:require #?(:clj [the.parsatron :refer [choice ch many between eof defparser always let->>]] + :cljs [the.parsatron :refer [choice ch many between eof always] + :refer-macros [defparser let->>]]))) + +(defparser instruction [] + (choice (ch \>) + (ch \<) + (ch \+) + (ch \-) + (ch \.) + (ch \,) + (between (ch \[) (ch \]) (many (instruction))))) + +(defparser bf [] + (let->> [result (many (instruction)) + _ (eof)] + (always result))) diff --git a/src/clj/the/parsatron.clj b/src/the/parsatron.cljc similarity index 86% rename from src/clj/the/parsatron.clj rename to src/the/parsatron.cljc index 00029a6..c5c3a38 100644 --- a/src/clj/the/parsatron.clj +++ b/src/the/parsatron.cljc @@ -1,13 +1,13 @@ (ns the.parsatron - (:refer-clojure :exclude [char]) - (:require [clojure.string :as str])) + (:require [clojure.string :as str]) + #?(:cljs (:require-macros [the.parsatron :refer [defparser >> let->>]]))) (defrecord InputState [input pos]) (defrecord SourcePos [line column]) (defrecord Continue [fn]) (defrecord Ok [item]) -(defrecord Err [errmsg]) +(defrecord Err [errmsg pos]) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; position @@ -65,18 +65,39 @@ ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; host environment -(defn fail [message] - (RuntimeException. message)) - -(defn digit? - "Tests if a character is a digit: [0-9]" - [c] - (Character/isDigit ^Character c)) - -(defn letter? - "Tests if a character is a letter: [a-zA-Z]" - [c] - (Character/isLetter ^Character c)) +#?(:clj + (def ch? char?) + + :cljs + (defn ch? + "Test for a single-character string. + ClojureScript doesn't support a character type, so we pretend it does" + [x] + (and (string? x) (= (count x) 1)))) + +#?(:clj + (defn digit? + "Tests if a character is a digit: [0-9]" + [c] + (Character/isDigit ^Character c)) + + :cljs + (defn digit? + "Tests if a character is a digit: [0-9]" + [c] + (re-matches #"\d" c))) + +#?(:clj + (defn letter? + "Tests if a character is a letter: [a-zA-Z]" + [c] + (Character/isLetter ^Character c)) + + :cljs + (defn letter? + "Tests if a character is a letter: [a-zA-Z]" + [c] + (re-matches #"[a-zA-Z]" c))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; m @@ -200,10 +221,10 @@ combinator is applied to a parser that accepts the empty string, as that would cause the parser to loop forever" [p] - (letfn [(many-err [_ _] - (fail "Combinator '*' is applied to a parser that accepts an empty string")) + (letfn [(many-err [pos _ _] + (Err. "Combinator '*' is applied to a parser that accepts an empty string" pos)) (safe-p [state cok cerr eok eerr] - (Continue. #(p state cok cerr many-err eerr)))] + (Continue. #(p state cok cerr (partial many-err (:pos state)) eerr)))] (either (let->> [x safe-p xs (many safe-p)] @@ -247,7 +268,7 @@ (eok nil state) (eerr (expect-error "end of input" pos))))) -(defn char +(defn ch "Consume the given character" [c] (token #(= c %))) @@ -255,7 +276,7 @@ (defn any-char "Consume any character" [] - (token char?)) + (token ch?)) (defn digit "Consume a digit [0-9] character" @@ -270,7 +291,7 @@ (defn string "Consume the given string" [s] - (reduce nxt (concat (map char s) + (reduce nxt (concat (map ch s) (list (always s))))) (defn between @@ -298,11 +319,11 @@ (fn cok [item _] (Ok. item)) (fn cerr [err] - (Err. (show-error err))) + (Err. (show-error err) (:pos err))) (fn eok [item _] (Ok. item)) (fn eerr [err] - (Err. (show-error err))))) + (Err. (show-error err) (:pos err))))) (defn run "Run a parser p over some input. The input can be a string or a seq @@ -313,4 +334,4 @@ (let [result (run-parser p (InputState. input (SourcePos. 1 1)))] (condp instance? result Ok (:item result) - Err (throw (fail ^String (:errmsg result)))))) + Err (throw (ex-info ^String (:errmsg result) (:pos result)))))) diff --git a/test/clj/parsatron/languages/test_bf.clj b/test/clj/parsatron/languages/test_bf.clj deleted file mode 100644 index caa54d2..0000000 --- a/test/clj/parsatron/languages/test_bf.clj +++ /dev/null @@ -1,28 +0,0 @@ -(ns parsatron.languages.test-bf - (:refer-clojure :exclude [char]) - (:use [the.parsatron] - [parsatron.languages.bf] - [clojure.test])) - -(deftest test-accepts-valid-brainf*ck - (are [input] (try - (run (bf) input) - true - (catch Exception _ - false)) - ">" - "<" - "+" - "-" - "." - "," - "[+]" - ",>++++++[<-------->-],[<+>-]<.")) - -(deftest test-rejects-invalid-brainf*ck - (are [input] (thrown? RuntimeException (run (bf) input)) - "a" - "abc" - "[+" - "]" - "[+>[+]")) \ No newline at end of file diff --git a/test/cljs/parsatron/test.cljs b/test/cljs/parsatron/test.cljs deleted file mode 100644 index e4c1caf..0000000 --- a/test/cljs/parsatron/test.cljs +++ /dev/null @@ -1,161 +0,0 @@ -(ns parsatron.test - (:require [the.parsatron :as p]) - (:import [goog.testing TestRunner TestCase]) - (:require-macros [the.parsatron :refer (>>)])) - -(def tr (TestRunner.)) -(def test (TestCase. "The Parsatron")) - -(defn parser-result? [expected p input] - (js/assertEquals expected (p/run p input))) - -(defn throws-with-msg? [re f] - (let [err (js/assertThrows f)] - (js/assertTrue (.test re (.-message err))))) - -(defn doeach [f & args] - (doall (map f args))) - - - -(defn test-always [] - (parser-result? 5 (p/always 5) "") - (parser-result? 5 (p/always 5) "abc")) - -(.add test (TestCase.Test. "test-always" test-always)) - -(defn test-nxt [] - (parser-result? 5 (p/nxt (p/always 3) - (p/always 5)) "")) - -(.add test (TestCase.Test. "test-nxt" test-nxt)) - -(defn test-bind [] - (parser-result? 8 (p/bind (p/always 3) - (fn [x] - (p/always (+ x 5)))) "")) - -(.add test (TestCase.Test. "test-bind" test-bind)) - -(defn test-never [] - (js/assertThrows #(p/run (p/never) ""))) - -(.add test (TestCase.Test. "test-never" test-never)) - -(defn test-either [] - ;; first parser succeeds - (parser-result? 5 (p/either (p/always 5) (p/always 3)) "") - - ;; second parser succeeds, when first fails with empty - (parser-result? 5 (p/either (p/never) (p/always 5)) "") - - ;; when neither succeed, errors are merged - (throws-with-msg? #"Unexpected token 'c', Unexpected token 'c'" #(p/run (p/either (p/char "a") (p/char "b")) "c"))) - -(.add test (TestCase.Test. "test-either" test-either)) - -(defn test-attempt [] - ;; success returns value of p - (parser-result? "a" (p/attempt (p/char "a")) "a") - - ;; failure is same as never - (js/assertThrows #(p/run (p/attempt (char "a")) "b")) - (parser-result? "c" (p/either (p/attempt (>> (p/char "a") (p/char "b"))) - (>> (p/char "a") (p/char "c"))) "ac")) - -(.add test (TestCase.Test. "test-attempt" test-attempt)) - -(defn test-token [] - ;; throws error on empty input - (throws-with-msg? #"Unexpected end of input" #(p/run (p/token (constantly true)) "")) - - ;; consume? determines parser's behavior, show-f used in error message - (parser-result? "a" (p/token (constantly true)) "a") - (throws-with-msg? #"Unexpected token 'a'" #(p/run (p/token (constantly false)) "a"))) - -(.add test (TestCase.Test. "test-token" test-token)) - -(defn test-many [] - ;; throws an error if parser does not consume - (throws-with-msg? #"Combinator '\*' is applied to a parser that accepts an empty string" #(p/run (p/many (p/always 5)) "")) - - ;; returns empty list when no input consumed - (parser-result? [] (p/many (p/char "a")) "") - - ;; parser returns list of consumed items - (js/assertTrue (= ["a" "a" "b" "a" "b" "b"] - (p/run - (p/many (p/either (p/char "a") - (p/char "b"))) - "aababbc"))) - - ;; does not blow the stack - (js/assertTrue (= (take 1000 (repeat "a")) - (p/run - (p/many (p/char "a")) - (apply str (take 1000 (repeat "a"))))))) - -(.add test (TestCase.Test. "test-many" test-many)) - -(defn test-times [] - ;; 0 times returns [], and does not consume - (parser-result? [] (p/times 0 (p/char "a")) "") - - ;; throws an error (from underlying parser) if fewer than specified - (doeach - (fn [input] - (throws-with-msg? #"Unexpected end of input" #(p/run (p/times 3 (p/char "a")) input))) - "" - "a" - "aa") - - ;; returns a list with the results - (js/assertTrue (= ["a" "a" "a"] (p/run (p/times 3 (p/char "a")) "aaa"))) - (js/assertTrue (= [5 5 5] (p/run (p/times 3 (p/always 5)) ""))) - - ;; does not blow the stack - (js/assertTrue (= (take 10000 (repeat "a")) - (p/run - (p/times 10000 (p/char "a")) - (apply str (take 10000 (repeat "a"))))))) - -(.add test (TestCase.Test. "test-times" test-times)) - -(defn test-lookahead [] - ;; returns value of p on success - (parser-result? "a" (p/lookahead (p/char "a")) "a") - - ;; does not consume input on success - (parser-result? "a" (>> (p/lookahead (p/char "a")) (p/char "a")) "a")) - -(.add test (TestCase.Test. "test-lookahead" test-lookahead)) - -(defn test-choice [] - ;; choice with no choices throws an exception - (js/assertThrows #(p/run (p/choice) "")) - - ;; first parser to succeed returns result - (doeach - (fn [input] - (parser-result? (first input) (p/choice (p/char "a") (p/char "b") (p/char "c")) input)) - "a" - "b" - "c")) - -(.add test (TestCase.Test. "test-choice" test-choice)) - -(defn test-eof [] - ;; parser succeeds, returns nil when no more input left - (parser-result? nil (p/eof) "") - (parser-result? nil (>> (p/char "a") (p/eof)) "a") - - ;; parser fails with message when input if left - (throws-with-msg? #"Expected end of input" - #(p/run (p/eof) "a")) - (throws-with-msg? #"Expected end of input" - #(p/run (>> (p/char "a") (p/eof)) "ab"))) - -(.add test (TestCase.Test. "test-eof" test-eof)) - -(.initialize tr test) -(.execute tr) diff --git a/test/clj/parsatron/languages/test_bencode.clj b/test/parsatron/languages/test_bencode.cljc similarity index 65% rename from test/clj/parsatron/languages/test_bencode.clj rename to test/parsatron/languages/test_bencode.cljc index 7df7c61..5db02fe 100644 --- a/test/clj/parsatron/languages/test_bencode.clj +++ b/test/parsatron/languages/test_bencode.cljc @@ -1,8 +1,9 @@ (ns parsatron.languages.test-bencode - (:refer-clojure :exclude [char]) - (:use [the.parsatron] - [parsatron.languages.bencode] - [clojure.test])) + (:require [the.parsatron :refer [run]] + [parsatron.languages.bencode :refer [ben-integer ben-bytestring ben-list ben-dictionary]] + #?(:clj [clojure.test :refer [deftest are is]] + :cljs [clojure.test :refer [run-tests] :refer-macros [deftest are is]])) + #?(:clj (:import [clojure.lang ExceptionInfo]))) (deftest test-ben-integer (are [expected input] (= expected (run (ben-integer) input)) @@ -26,5 +27,6 @@ "address" {"street" "1 Home St" "city" "Anywhere"}} "d4:name4:Mary3:agei33e8:childrenl5:Betty3:Same7:addressd6:street9:1 Home St4:city8:Anywhereee") - (is (thrown? RuntimeException (run (ben-dictionary) "di42e4:spam4:spami42ee")))) + (is (thrown? ExceptionInfo (run (ben-dictionary) "di42e4:spam4:spami42ee")))) +#?(:cljs (run-tests)) diff --git a/test/parsatron/languages/test_bf.cljc b/test/parsatron/languages/test_bf.cljc new file mode 100644 index 0000000..0c749da --- /dev/null +++ b/test/parsatron/languages/test_bf.cljc @@ -0,0 +1,31 @@ +(ns parsatron.languages.test-bf + (:require [the.parsatron :refer [run]] + [parsatron.languages.bf :refer [bf]] + #?(:clj [clojure.test :refer [deftest are]] + :cljs [clojure.test :refer [run-tests] :refer-macros [deftest are]])) + #?(:clj (:import [clojure.lang ExceptionInfo]))) + +(deftest test-accepts-valid-brainf*ck + (are [input] (try + (run (bf) input) + true + (catch ExceptionInfo _ + false)) + ">" + "<" + "+" + "-" + "." + "," + "[+]" + ",>++++++[<-------->-],[<+>-]<.")) + +(deftest test-rejects-invalid-brainf*ck + (are [input] (thrown? ExceptionInfo (run (bf) input)) + "a" + "abc" + "[+" + "]" + "[+>[+]")) + +#?(:cljs (run-tests)) diff --git a/test/clj/parsatron/test.clj b/test/parsatron/test.cljc similarity index 52% rename from test/clj/parsatron/test.clj rename to test/parsatron/test.cljc index a673c94..d251b12 100644 --- a/test/clj/parsatron/test.clj +++ b/test/parsatron/test.cljc @@ -1,8 +1,16 @@ (ns parsatron.test - (:refer-clojure :exclude [char]) - (:use [the.parsatron] - [clojure.test]) - (:import (the.parsatron SourcePos))) + #?(:clj + (:require [the.parsatron :refer [run always nxt bind never either ch attempt token + many times lookahead choice eof >>]] + [clojure.test :refer [is are deftest testing]]) + :cljs + (:require [the.parsatron :refer [run always nxt bind never either ch attempt token + many times lookahead choice eof SourcePos] + :refer-macros [>>]] + [clojure.test :refer [run-tests] :refer-macros [is are deftest testing]])) + #?(:clj + (:import (the.parsatron SourcePos) + (clojure.lang ExceptionInfo)))) (defn parser-result? [expected p input] (= expected (run p input))) @@ -21,8 +29,8 @@ (always (+ x 5)))) ""))) (deftest test-never - (is (thrown? RuntimeException (run (never) ""))) - (is (thrown? RuntimeException (run (never) "abc")))) + (is (thrown? ExceptionInfo (run (never) ""))) + (is (thrown? ExceptionInfo (run (never) "abc")))) (deftest test-either (testing "first parser succeeds" @@ -32,80 +40,80 @@ (is (parser-result? 5 (either (never) (always 5)) ""))) (testing "when neither succeed, errors are merged" - (is (thrown-with-msg? RuntimeException #"Unexpected token 'c', Unexpected token 'c'" - (run (either (char \a) (char \b)) "c"))))) + (is (thrown-with-msg? ExceptionInfo #"Unexpected token 'c', Unexpected token 'c'" + (run (either (ch \a) (ch \b)) "c"))))) (deftest test-attempt (testing "success returns value of p" - (is (parser-result? \a (attempt (char \a)) "a"))) + (is (parser-result? \a (attempt (ch \a)) "a"))) (testing "failure is same as never" - (is (thrown? RuntimeException (run (attempt (char \a)) "b"))) - (is (parser-result? \c (either (attempt (>> (char \a) (char \b))) - (>> (char \a) (char \c))) "ac")))) + (is (thrown? ExceptionInfo (run (attempt (ch \a)) "b"))) + (is (parser-result? \c (either (attempt (>> (ch \a) (ch \b))) + (>> (ch \a) (ch \c))) "ac")))) (deftest test-token (testing "throws error on empty input" - (is (thrown-with-msg? RuntimeException #"Unexpected end of input" + (is (thrown-with-msg? ExceptionInfo #"Unexpected end of input" (run (token (constantly true)) "")))) (testing "consume? determines parser's behavior, show-f used in error message" (is (parser-result? \a (token (constantly true)) "a")) - (is (thrown-with-msg? RuntimeException #"Unexpected token 'a'" + (is (thrown-with-msg? ExceptionInfo #"Unexpected token 'a'" (run (token (constantly false)) "a"))))) (deftest test-many (testing "throws an exception if parser does not consume" - (is (thrown-with-msg? RuntimeException #"Combinator '\*' is applied to a parser that accepts an empty string" + (is (thrown-with-msg? ExceptionInfo #"Combinator '\*' is applied to a parser that accepts an empty string" (run (many (always 5)) "")))) (testing "returns empty list when no input consumed" - (is (parser-result? [] (many (char \a)) ""))) + (is (parser-result? [] (many (ch \a)) ""))) (testing "parser returns list of consumed items" (is (parser-result? [\a \a \b \a \b \b] - (many (either (char \a) - (char \b))) + (many (either (ch \a) + (ch \b))) "aababbc"))) (testing "does not blow the stack" (is (parser-result? (take 1000 (repeat \a)) - (many (char \a)) + (many (ch \a)) (apply str (take 1000 (repeat \a))))))) (deftest test-times (testing "0 times returns [], and does not consume" - (is (parser-result? [] (times 0 (char \a)) ""))) + (is (parser-result? [] (times 0 (ch \a)) ""))) (testing "throws an error (from underlying parser) if fewer than specified" - (are [input] (thrown-with-msg? RuntimeException #"Unexpected end of input" - (run (times 3 (char \a)) input)) + (are [input] (thrown-with-msg? ExceptionInfo #"Unexpected end of input" + (run (times 3 (ch \a)) input)) "" "a" "aa")) (testing "returns a list with the results" - (is (parser-result? [\a \a \a] (times 3 (char \a)) "aaa")) + (is (parser-result? [\a \a \a] (times 3 (ch \a)) "aaa")) (is (parser-result? [5 5 5] (times 3 (always 5)) ""))) (testing "does not blow the stack" (is (parser-result? (take 10000 (repeat \a)) - (times 10000 (char \a)) + (times 10000 (ch \a)) (apply str (take 10000 (repeat \a))))))) (deftest test-lookahead (testing "returns value of p on success" - (is (parser-result? \a (lookahead (char \a)) "a"))) + (is (parser-result? \a (lookahead (ch \a)) "a"))) (testing "does not consume input on success" - (is (parser-result? \a (>> (lookahead (char \a)) (char \a)) "a")))) + (is (parser-result? \a (>> (lookahead (ch \a)) (ch \a)) "a")))) (deftest test-choice (testing "choice with no choices throws an exception" - (is (thrown? RuntimeException (run (choice) "")))) + (is (thrown? ExceptionInfo (run (choice) "")))) (testing "first parser to succeed returns result" - (are [input] (parser-result? (first input) (choice (char \a) (char \b) (char \c)) input) + (are [input] (parser-result? (first input) (choice (ch \a) (ch \b) (ch \c)) input) "a" "b" "c"))) @@ -113,10 +121,12 @@ (deftest test-eof (testing "parser succeeds, returns nil when no more input left" (is (parser-result? nil (eof) "")) - (is (parser-result? nil (>> (char \a) (eof)) "a"))) + (is (parser-result? nil (>> (ch \a) (eof)) "a"))) (testing "parser fails with message when input if left" - (is (thrown-with-msg? RuntimeException #"Expected end of input" + (is (thrown-with-msg? ExceptionInfo #"Expected end of input" (run (eof) "a"))) - (is (thrown-with-msg? RuntimeException #"Expected end of input" - (run (>> (char \a) (eof)) "ab"))))) + (is (thrown-with-msg? ExceptionInfo #"Expected end of input" + (run (>> (ch \a) (eof)) "ab"))))) + +#?(:cljs (run-tests)) diff --git a/test/clj/parsatron/test_trampoline.clj b/test/parsatron/test_trampoline.cljc similarity index 61% rename from test/clj/parsatron/test_trampoline.clj rename to test/parsatron/test_trampoline.cljc index d50edf6..203c399 100644 --- a/test/clj/parsatron/test_trampoline.clj +++ b/test/parsatron/test_trampoline.cljc @@ -1,8 +1,12 @@ (ns parsatron.test-trampoline - (:refer-clojure :exclude [char]) - (:use [the.parsatron] - [clojure.test]) - (:import (the.parsatron Continue Ok))) + #?(:clj + (:require [the.parsatron :refer [always bind]] + [clojure.test :refer [deftest testing is]]) + :cljs + (:require [the.parsatron :refer [always bind Continue Ok]] + [clojure.test :refer [run-tests] :refer-macros [deftest testing is]])) + #?(:clj + (:import (the.parsatron Continue Ok)))) (deftest test-always (testing "always is a fn" @@ -20,4 +24,6 @@ (let [q-continue ((:fn p-continue))] (is (instance? Continue q-continue)) (let [result ((:fn q-continue))] - (is (= (Ok. 7) result))))))) \ No newline at end of file + (is (= (Ok. 7) result))))))) + +#?(:cljs (run-tests))