Skip to content

Commit cda6742

Browse files
authored
Merge pull request #792 from frenchy64/frenchy64-recursive-non-termination
Fix recursive `:+` generators
2 parents 7b40b3e + 3e49983 commit cda6742

File tree

5 files changed

+221
-22
lines changed

5 files changed

+221
-22
lines changed

src/malli/generator.cljc

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
;; See also `malli.generator-ast` for viewing generators as data
2+
;; Note: use `::mg/foo` instead of `::foo` in this namespace
3+
;; to assist in generating `malli.generator-ast`.
14
(ns malli.generator
25
(:require [clojure.spec.gen.alpha :as ga]
36
[clojure.string :as str]
@@ -8,6 +11,7 @@
811
[clojure.test.check.rose-tree :as rose]
912
[malli.core :as m]
1013
[malli.registry :as mr]
14+
[malli.generator :as-alias mg]
1115
#?(:clj [borkdude.dynaload :as dynaload])))
1216

1317
(declare generator generate -create)
@@ -48,7 +52,7 @@
4852

4953
(defn -never-gen
5054
"Return a generator of no values that is compatible with -unreachable-gen?."
51-
[{::keys [original-generator-schema] :as _options}]
55+
[{::mg/keys [original-generator-schema] :as _options}]
5256
(with-meta (gen/such-that (fn [_]
5357
(throw (ex-info
5458
(str "Cannot generate values due to infinitely expanding schema: "
@@ -58,12 +62,12 @@
5862
(cond-> {}
5963
original-generator-schema (assoc :schema (m/form original-generator-schema))))))
6064
gen/any)
61-
{::never-gen true
62-
::original-generator-schema original-generator-schema}))
65+
{::mg/never-gen true
66+
::mg/original-generator-schema original-generator-schema}))
6367

6468
(defn -unreachable-gen?
6569
"Returns true iff generator g generators no values."
66-
[g] (-> (meta g) ::never-gen boolean))
70+
[g] (-> (meta g) ::mg/never-gen boolean))
6771

6872
(defn -not-unreachable [g] (when-not (-unreachable-gen? g) g))
6973

@@ -80,9 +84,9 @@
8084
(defn -min-max [schema options]
8185
(let [{:keys [min max] gen-min :gen/min gen-max :gen/max} (m/properties schema options)]
8286
(when (and min gen-min (< gen-min min))
83-
(m/-fail! ::invalid-property {:key :gen/min, :value gen-min, :min min}))
87+
(m/-fail! ::mg/invalid-property {:key :gen/min, :value gen-min, :min min}))
8488
(when (and max gen-max (> gen-max max))
85-
(m/-fail! ::invalid-property {:key :gen/max, :value gen-min, :max min}))
89+
(m/-fail! ::mg/invalid-property {:key :gen/max, :value gen-min, :max min}))
8690
{:min (or gen-min min)
8791
:max (or gen-max max)}))
8892

@@ -286,12 +290,12 @@
286290

287291
(defn -ref-gen [schema options]
288292
(let [ref-id (-identify-ref-schema schema)]
289-
(or (force (get-in options [::rec-gen ref-id]))
293+
(or (force (get-in options [::mg/rec-gen ref-id]))
290294
(let [scalar-ref-gen (delay (-never-gen options))
291295
dschema (m/deref schema)]
292-
(cond->> (generator dschema (assoc-in options [::rec-gen ref-id] scalar-ref-gen))
296+
(cond->> (generator dschema (assoc-in options [::mg/rec-gen ref-id] scalar-ref-gen))
293297
(realized? scalar-ref-gen) (gen/recursive-gen
294-
#(generator dschema (assoc-in options [::rec-gen ref-id] %))))))))
298+
#(generator dschema (assoc-in options [::mg/rec-gen ref-id] %))))))))
295299

296300
(defn -=>-gen [schema options]
297301
(let [output-generator (generator (:output (m/-function-info schema)) options)]
@@ -334,12 +338,21 @@
334338
(gen/return ()))))
335339

336340
(defn -*-gen [schema options]
337-
(let [child (m/-get schema 0 nil)]
341+
(let [child (m/-get schema 0 nil)
342+
mode (::mg/-*-gen-mode options :*)
343+
options (dissoc options ::mg/-*-gen-mode)]
338344
(if-some [g (-not-unreachable (generator child options))]
339345
(cond->> (gen/vector g)
340346
(m/-regex-op? child)
341347
(gen/fmap #(apply concat %)))
342-
(gen/return ()))))
348+
(case mode
349+
:* (gen/return ())
350+
:+ (-never-gen options)))))
351+
352+
(defn -+-gen [schema options]
353+
(let [g (-*-gen schema (assoc options ::mg/-*-gen-mode :+))]
354+
(cond-> g
355+
(-not-unreachable g) gen/not-empty)))
343356

344357
(defn -repeat-gen [schema options]
345358
(let [child (m/-get schema 0 nil)]
@@ -360,9 +373,9 @@
360373
(defn -qualified-symbol-gen [schema]
361374
(-qualified-ident-gen schema symbol gen/symbol qualified-symbol? gen/symbol-ns))
362375

363-
(defmulti -schema-generator (fn [schema options] (m/type schema options)) :default ::default)
376+
(defmulti -schema-generator (fn [schema options] (m/type schema options)) :default ::mg/default)
364377

365-
(defmethod -schema-generator ::default [schema options] (ga/gen-for-pred (m/validator schema options)))
378+
(defmethod -schema-generator ::mg/default [schema options] (ga/gen-for-pred (m/validator schema options)))
366379

367380
(defmethod -schema-generator :> [schema options] (-double-gen {:min (-> schema (m/children options) first inc)}))
368381
(defmethod -schema-generator :>= [schema options] (-double-gen {:min (-> schema (m/children options) first)}))
@@ -432,7 +445,7 @@
432445

433446
(defmethod -schema-generator :? [schema options] (-?-gen schema options))
434447
(defmethod -schema-generator :* [schema options] (-*-gen schema options))
435-
(defmethod -schema-generator :+ [schema options] (gen/not-empty (-*-gen schema options)))
448+
(defmethod -schema-generator :+ [schema options] (-+-gen schema options))
436449
(defmethod -schema-generator :repeat [schema options] (-repeat-gen schema options))
437450

438451
;;
@@ -448,7 +461,7 @@
448461
(when-not (:gen/elements props)
449462
(if (satisfies? Generator schema)
450463
(-generator schema options)
451-
(-schema-generator schema (assoc options ::original-generator-schema schema))))))
464+
(-schema-generator schema (assoc options ::mg/original-generator-schema schema))))))
452465

453466
(defn- -create-from-schema [props options]
454467
(some-> (:gen/schema props) (generator options)))
@@ -468,8 +481,8 @@
468481
(-create-from-elements props)
469482
(-create-from-schema props options)
470483
(-create-from-gen props schema options)
471-
(m/-fail! ::no-generator {:options options
472-
:schema schema}))))
484+
(m/-fail! ::mg/no-generator {:options options
485+
:schema schema}))))
473486

474487
;;
475488
;; public api
@@ -479,7 +492,7 @@
479492
([?schema]
480493
(generator ?schema nil))
481494
([?schema options]
482-
(if (::rec-gen options)
495+
(if (::mg/rec-gen options)
483496
;; disable cache while calculating recursive schemas. caches don't distinguish options.
484497
(-create (m/schema ?schema options) options)
485498
(m/-cached (m/schema ?schema options) :generator #(-create % options)))))
@@ -507,7 +520,7 @@
507520

508521
(defn function-checker
509522
([?schema] (function-checker ?schema nil))
510-
([?schema {::keys [=>iterations] :or {=>iterations 100} :as options}]
523+
([?schema {::mg/keys [=>iterations] :or {=>iterations 100} :as options}]
511524
(let [schema (m/schema ?schema options)
512525
check (fn [schema]
513526
(let [{:keys [input output]} (m/-function-info schema)
@@ -524,15 +537,15 @@
524537
(try (apply f smallest) (catch #?(:clj Exception, :cljs js/Error) e e)))
525538
explain-output (when-not explain-input (m/explain output response))]
526539
(cond-> shrunk
527-
explain-input (assoc ::explain-input explain-input)
528-
explain-output (assoc ::explain-output explain-output)
540+
explain-input (assoc ::mg/explain-input explain-input)
541+
explain-output (assoc ::mg/explain-output explain-output)
529542
(ex-message result) (-> (update :result ex-message)
530543
(dissoc :result-data)))))))))]
531544
(condp = (m/type schema)
532545
:=> (check schema)
533546
:function (let [checkers (map #(function-checker % options) (m/-children schema))]
534547
(fn [x] (->> checkers (keep #(% x)) (seq))))
535-
(m/-fail! ::invalid-function-schema {:type (m/-type schema)})))))
548+
(m/-fail! ::mg/invalid-function-schema {:type (m/-type schema)})))))
536549

537550
(defn check
538551
([?schema f] (check ?schema f nil))

test/malli/generator_ast.clj

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
(ns malli.generator-ast
2+
"For inspecting a malli's generator as data. See `generator-ast`"
3+
(:require [clojure.java.io :as io]
4+
[clojure.string :as str]
5+
[clojure.walk :as walk]
6+
[malli.generator :as mg]))
7+
8+
(let [s (-> (slurp (io/resource "malli/generator.cljc"))
9+
;; change the namespace
10+
(str/replace-first "(ns malli.generator" "(ns malli.generator-ast")
11+
;; change the `gen` alias to the AST version
12+
(str/replace-first "clojure.test.check.generators" "malli.generator-debug"))]
13+
;; eval ns form first so keywords can be resolved in the right namespace
14+
(eval (read-string {:read-cond :allow :features #{:clj}} s))
15+
(eval (read-string {:read-cond :allow :features #{:clj}} (str "(do " s ")"))))
16+
17+
(defn generator-ast
18+
"Return a malli schema's generator as an AST."
19+
([?schema]
20+
(generator-ast ?schema nil))
21+
([?schema options]
22+
(walk/postwalk
23+
(fn [g]
24+
(if (mg/-unreachable-gen? g)
25+
{:op :unreachable}
26+
g))
27+
(generator ?schema (assoc options ::mg/generator-ast true)))))

test/malli/generator_ast_test.clj

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
(ns malli.generator-ast-test
2+
(:require [clojure.pprint :refer [pprint]]
3+
[clojure.test :refer [are deftest is testing]]
4+
[malli.generator-ast :as ast]))
5+
6+
(deftest generator-ast-test
7+
(is (= '{:op :recursive-gen,
8+
:rec-gen
9+
{:op :one-of,
10+
:generators
11+
[{:op :boolean}
12+
{:op :tuple,
13+
:generators [{:op :elements, :coll [:not]} {:op :boolean}]}
14+
{:op :tuple,
15+
:generators
16+
[{:op :elements, :coll [:and]}
17+
{:op :vector, :generator {:op :recur}}]}
18+
{:op :tuple,
19+
:generators
20+
[{:op :elements, :coll [:or]}
21+
{:op :vector, :generator {:op :recur}}]}]},
22+
:scalar-gen
23+
{:op :one-of,
24+
:generators
25+
[{:op :boolean}
26+
{:op :tuple,
27+
:generators [{:op :elements, :coll [:not]} {:op :boolean}]}
28+
{:op :tuple,
29+
:generators
30+
[{:op :elements, :coll [:and]} {:op :return, :value ()}]}
31+
{:op :tuple,
32+
:generators
33+
[{:op :elements, :coll [:or]} {:op :return, :value ()}]}]}}
34+
(ast/generator-ast
35+
[:schema
36+
{:registry
37+
{::formula
38+
[:or
39+
:boolean
40+
[:tuple [:enum :not] :boolean]
41+
[:tuple [:enum :and] [:* [:ref ::formula]]]
42+
[:tuple [:enum :or] [:* [:ref ::formula]]]]}}
43+
[:ref ::formula]])))
44+
(is (= '{:op :recursive-gen,
45+
:rec-gen
46+
{:op :one-of,
47+
:generators
48+
[{:op :boolean}
49+
{:op :tuple,
50+
:generators [{:op :elements, :coll [:not]} {:op :boolean}]}
51+
{:op :tuple,
52+
:generators
53+
[{:op :elements, :coll [:and]}
54+
{:op :not-empty, :gen {:op :vector, :generator {:op :recur}}}]}
55+
{:op :tuple,
56+
:generators
57+
[{:op :elements, :coll [:or]}
58+
{:op :not-empty, :gen {:op :vector, :generator {:op :recur}}}]}]},
59+
:scalar-gen
60+
{:op :one-of,
61+
:generators
62+
[{:op :boolean}
63+
{:op :tuple,
64+
:generators [{:op :elements, :coll [:not]} {:op :boolean}]}]}}
65+
(ast/generator-ast
66+
[:schema
67+
{:registry
68+
{::formula
69+
[:or
70+
:boolean
71+
[:tuple [:enum :not] :boolean]
72+
[:tuple [:enum :and] [:+ [:ref ::formula]]]
73+
[:tuple [:enum :or] [:+ [:ref ::formula]]]]}}
74+
[:ref ::formula]]))))

test/malli/generator_debug.cljc

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
(ns malli.generator-debug
2+
"Drop-in replacement for clojure.test.check.generators that returns AST's
3+
instead of generators."
4+
(:refer-clojure :exclude [vector char keyword boolean not-empty symbol]))
5+
6+
(defmacro such-that [& args] (let [args (vec args)] `{:op :such-that :args-form '~args :args ~args}))
7+
(def any {:op :any})
8+
(def any-printable {:op :any-printable})
9+
(defn double* [& args] {:op :double* :args args})
10+
(defmacro fmap [& args] (let [args (vec args)] `{:op :fmap :args-form '~args :args ~args}))
11+
(defmacro vector
12+
([generator] {:op :vector :generator generator})
13+
([generator num-elements] {:op :vector :generator generator :num-elements num-elements})
14+
([generator min-elements max-elements]
15+
{:op :vector :generator generator :min-elements min-elements :max-elements max-elements}))
16+
(defmacro vector-distinct [& args] (let [args (vec args)] `{:op :vector-distinct :args-form '~args :args ~args}))
17+
(def char {:op :char})
18+
(def nat {:op :nat})
19+
(def char-alphanumeric {:op :char-alphanumeric})
20+
(def string-alphanumeric {:op :string-alphanumeric})
21+
(defn sized [& args] {:op :sized :args args})
22+
(defn return [value] {:op :return :value value})
23+
(defn one-of [generators] {:op :one-of :generators generators})
24+
(defn tuple [& generators] {:op :tuple :generators (vec generators)})
25+
(defn recursive-gen [rec scalar]
26+
{:op :recursive-gen
27+
:rec-gen (rec {:op :recur})
28+
:scalar-gen scalar})
29+
(def keyword {:op :keyword})
30+
(def keyword-ns {:op :keyword-ns})
31+
(def symbol {:op :symbol})
32+
(def symbol-ns {:op :symbol-ns})
33+
(def s-pos-int {:op :s-pos-int})
34+
(def s-neg-int {:op :s-neg-int})
35+
(defn elements [coll] {:op :elements :coll coll})
36+
(defn large-integer* [& args] {:op :large-integer* :args args})
37+
(def boolean {:op :boolean})
38+
(def uuid {:op :uuid})
39+
(defn not-empty [gen] {:op :not-empty :gen gen})
40+
(defn generator? [& args] (assert nil "no stub for generator?"))
41+
(defn call-gen [& args] (assert nil "no stub for call-gen"))
42+
(defn make-size-range-seq [& args] (assert nil "no stub for make-size-range-seq"))
43+
(defn lazy-random-states [& args] (assert nil "no stub for lazy-random-states"))

test/malli/generator_test.cljc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,48 @@
624624
(gen/tuple (gen/return :B))
625625
(gen/tuple (gen/return :C))
626626
(gen/tuple (gen/return :D))])))
627+
{:seed 0})))
628+
(is (= '([:not true] [:not false] [:and [[:not false]]] [:or [[:not false]]] false [:and [true [:not true]]] [:and ()] [:or [[:or ()] false]] [:not true] [:and [[:not true]]])
629+
(mg/sample [:schema
630+
{:registry
631+
{::formula
632+
[:or
633+
:boolean
634+
[:tuple [:enum :not] :boolean]
635+
[:tuple [:enum :and] [:* [:ref ::formula]]]
636+
[:tuple [:enum :or] [:* [:ref ::formula]]]]}}
637+
[:ref ::formula]]
638+
{:seed 0})
639+
(mg/sample (gen/recursive-gen
640+
(fn [formula]
641+
(gen/one-of [gen/boolean
642+
(gen/tuple (gen/elements [:not]) gen/boolean)
643+
(gen/tuple (gen/elements [:and]) (gen/vector formula))
644+
(gen/tuple (gen/elements [:or]) (gen/vector formula))]))
645+
(gen/one-of [gen/boolean
646+
(gen/tuple (gen/elements [:not]) gen/boolean)
647+
(gen/tuple (gen/elements [:and]) (gen/return ()))
648+
(gen/tuple (gen/elements [:or]) (gen/return ()))]))
649+
{:seed 0})))
650+
(is (= '([:not true] [:not false] [:and [true]] [:or [[:not false] true]] false [:and [[:not true]]] [:not false] [:or [[:not false] [:not true]]] [:not true] [:and [[:not false] [:and [[:not false] [:not true]]] [:and [[:not true]]]]])
651+
(mg/sample [:schema
652+
{:registry
653+
{::formula
654+
[:or
655+
:boolean
656+
[:tuple [:enum :not] :boolean]
657+
[:tuple [:enum :and] [:+ [:ref ::formula]]]
658+
[:tuple [:enum :or] [:+ [:ref ::formula]]]]}}
659+
[:ref ::formula]]
660+
{:seed 0})
661+
(mg/sample (gen/recursive-gen
662+
(fn [formula]
663+
(gen/one-of [gen/boolean
664+
(gen/tuple (gen/elements [:not]) gen/boolean)
665+
(gen/tuple (gen/elements [:and]) (gen/not-empty (gen/vector formula)))
666+
(gen/tuple (gen/elements [:or]) (gen/not-empty (gen/vector formula)))]))
667+
(gen/one-of [gen/boolean
668+
(gen/tuple (gen/elements [:not]) gen/boolean)]))
627669
{:seed 0}))))
628670

629671
(deftest infinite-generator-test

0 commit comments

Comments
 (0)