Skip to content

Commit

Permalink
allow pattern matching segment variables to take predicates (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
sritchie authored Jan 26, 2023
1 parent dc3cd96 commit 23248a9
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 91 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@

## [unreleased]

- #100:

- Implements predicate support for `segment`, `entire-segment` and
`reverse-segment` in `emmy.pattern.match`. This support bubbles up to forms
in rules like `(?? x pred1 pred2)`.

- Removes the `:emmy.pattern/ignored-restriction` linter keyword, and all
clj-kondo code warning that restrictions aren't supported on segment binding
forms.

- #96 renames `#sicm/{bigint, quaternion, complex, ratio}` to `#emmy/{bigint,
quaternion, complex, ratio}`.

Expand Down
22 changes: 0 additions & 22 deletions doc/linters.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,28 +136,6 @@ symbol.

*Example message:*: `Binding variable "x" must be a non-namespaced symbol.`

### Ignored Segment Restriction

*Keyword:* `:emmy.pattern/ignored-restriction`

*Description:* warn when a segment binding form like `(?? x)` or `($$ x)`
contain restrictions like `(?? x all-odd?)`. These don't error but aren't
currently used.

*Default level:* `:warning`

*Example trigger:*

`.clj-kondo/config.edn`:

``` clojure
(require '[emmy.rule :as r])

(r/rule (+ (?? x odd?) ?y) => "match!")
```

*Example message*: `Restrictions are (currently) ignored on ?? binding forms: odd?`

### Invalid Restriction in Consequence

*Keyword:* `:emmy.pattern/consequence-restriction`
Expand Down
3 changes: 0 additions & 3 deletions resources/clj-kondo.exports/org.mentat/emmy/config.edn
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
{:linters
{:emmy.pattern/binding-sym {:level :error}
:emmy.pattern/ignored-restriction {:level :warning}
:emmy.pattern/consequence-restriction {:level :error}
:emmy.pattern/ruleset-args {:level :error}

:emmy.abstract.function/invalid-binding {:level :error}

:emmy.calculus.coordinate/invalid-binding {:level :error}}

:lint-as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,24 +126,14 @@ forms (`:unquote` or `:unquote-splicing`, false otherwise.)"}
[[lint-binding-form!]] returns nil for any input."
[node]
(when (binding-form? node)
(let [[sym binding & restrictions] (:children node)]
(let [[_ binding] (:children node)]
(when-not (or (simple-symbol? (:value binding))
(any-unquote? binding))
(reg-binding-sym! binding))

(when (segment-marker? sym)
(doseq [r restrictions]
(api/reg-finding!
(assoc (meta r)
:message
(str "Restrictions are (currently) ignored on "
(:value sym) " binding forms: "
(pr-str (api/sexpr r)))
:type :emmy.pattern/ignored-restriction)))))))
(reg-binding-sym! binding)))))

(defn pattern-vec
"Given a node representing a pattern form, (and, optionally, a node representing
a predicate function `f`), returns a vector of all checkable entries in the
a predicate function `pred`), returns a vector of all checkable entries in the
pattern.
These are
Expand Down
112 changes: 64 additions & 48 deletions src/emmy/pattern/match.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -290,26 +290,31 @@
arguments; the new frame and the remaining elements in `xs`. This is a
different contract than all other matchers, making `segment` appropriate for
use inside `sequence`."
[sym]
(as-segment-matcher
(fn segment-match [frame xs succeed]
(let [xs (core/or xs [])]
(when (sequential? xs)
(if-let [binding (core/and
(core/not (s/wildcard? sym))
(frame sym))]
(let [binding-count (count binding)]
(when (= (take binding-count xs) binding)
(succeed frame (drop binding-count xs))))
(loop [prefix []
suffix xs]
(let [new-frame (if (s/wildcard? sym)
frame
(assoc frame sym prefix))]
(core/or (succeed new-frame suffix)
(core/and (seq suffix)
(recur (conj prefix (first suffix))
(next suffix))))))))))))
([sym]
(segment sym (constantly true)))
([sym pred]
(as-segment-matcher
(fn segment-match [frame xs succeed]
(let [xs (core/or xs [])]
(when (sequential? xs)
(if-let [binding (core/and
(core/not (s/wildcard? sym))
(frame sym))]
(when (pred binding)
(let [binding-count (count binding)]
(when (= (take binding-count xs) binding)
(succeed frame (drop binding-count xs)))))
(loop [prefix []
suffix xs]
(core/or
(core/and (pred prefix)
(let [new-frame (if (s/wildcard? sym)
frame
(assoc frame sym prefix))]
(succeed new-frame suffix)))
(core/and (seq suffix)
(recur (conj prefix (first suffix))
(next suffix))))))))))))

(defn- entire-segment
"Similar to [[segment]], but matches the entire remaining sequential argument
Expand All @@ -320,17 +325,19 @@
introduces NO new bindings.
Calls its continuation with the new frame and `nil`, always."
[sym]
(as-segment-matcher
(fn entire-segment-match [frame xs succeed]
(let [xs (core/or xs [])]
(when (sequential? xs)
(if (s/wildcard? sym)
(succeed frame nil)
(if-let [binding (frame sym)]
(when (= xs binding)
(succeed frame nil))
(succeed (assoc frame sym xs) nil))))))))
([sym]
(entire-segment sym (constantly true)))
([sym pred]
(as-segment-matcher
(fn entire-segment-match [frame xs succeed]
(let [xs (core/or xs [])]
(when (core/and (sequential? xs) (pred xs))
(if (s/wildcard? sym)
(succeed frame nil)
(if-let [binding (frame sym)]
(when (= xs binding)
(succeed frame nil))
(succeed (assoc frame sym xs) nil)))))))))

(defn reverse-segment
"Returns a matcher that takes a binding variable `sym`, and succeeds if it's
Expand All @@ -343,17 +350,20 @@
- `sym` is bound to something other than a vector prefix created by `segment`
- the data argument does not have a prefix matching the reverse of vector
bound to `sym`."
[sym]
(as-segment-matcher
(fn reverse-segment-match [frame xs succeed]
(let [xs (core/or xs [])]
(when (sequential? xs)
(when-let [binding (frame sym)]
(when (vector? binding)
(let [binding-count (count binding)
reversed (rseq binding)]
(when (= (take binding-count xs) reversed)
(succeed frame (drop binding-count xs)))))))))))
([sym]
(reverse-segment sym (constantly true)))
([sym pred]
(as-segment-matcher
(fn reverse-segment-match [frame xs succeed]
(let [xs (core/or xs [])]
(when (sequential? xs)
(when-let [binding (frame sym)]
(when (vector? binding)
(let [binding-count (count binding)
reversed (rseq binding)]
(when (core/and (= (take binding-count xs) reversed)
(pred xs))
(succeed frame (drop binding-count xs))))))))))))

(defn sequence*
"Version of [[sequence]] that takes an explicit sequence of `patterns`, vs the
Expand Down Expand Up @@ -410,8 +420,8 @@

(defn pattern->combinators
"Given a pattern (built using the syntax elements described in
`emmy.pattern.syntax`), returns a matcher combinator that will successfully match
data structures described by the input pattern, and fail otherwise."
`emmy.pattern.syntax`), returns a matcher combinator that will successfully
match data structures described by the input pattern, and fail otherwise."
[pattern]
(cond (fn? pattern) pattern

Expand All @@ -420,10 +430,14 @@
(s/restriction pattern))

(s/segment? pattern)
(segment (s/variable-name pattern))
(segment
(s/variable-name pattern)
(s/restriction pattern))

(s/reverse-segment? pattern)
(reverse-segment (s/reverse-segment-name pattern))
(reverse-segment
(s/reverse-segment-name pattern)
(s/restriction pattern))

(s/wildcard? pattern) pass

Expand All @@ -435,7 +449,9 @@
(concat (map pattern->combinators (butlast pattern))
(let [p (last pattern)]
[(if (s/segment? p)
(entire-segment (s/variable-name p))
(entire-segment
(s/variable-name p)
(s/restriction p))
(pattern->combinators p))]))))

:else (eq pattern)))
Expand Down Expand Up @@ -500,7 +516,7 @@
(let [match (pattern->combinators pattern)
success (fn [frame]
(when-let [m (pred frame)]
(when (and m (not (failed? m)))
(when (core/and m (not (failed? m)))
(if (map? m)
(merge frame m)
frame))))]
Expand Down
10 changes: 7 additions & 3 deletions src/emmy/pattern/syntax.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
;; introduce a new binding from that symbol to the matched value.
;;
;; - `(? <binding> <predicates...>)` triggers a binding iff all of the predicate
;; functions appearing after the binding pass for the candidate
;; functions appearing after the binding pass for the candidate.
;;
;; - `(?? <binding>)` or `??x` inside of a sequence matches a _segment_ of the
;; list whose length isn't fixed. Segments will attempt to succed with
Expand All @@ -26,6 +26,10 @@
;; already succeeded with a segment. If it has, - this will match a segment
;; equal to the _reverse_ of the already-bound segment.
;;
;; - `(?? <binding> <predicates...>)` and `($$ <binding> <predicates...>)` are
;; similar, but only pass iff all of the predicate functions appearing after
;; the binding pass for the candidate sequence.
;;
;; - Any sequential entry, like a list or a vector, triggers a `sequence` match.
;; This will attempt to match a sequence, and only pass if its matcher
;; arguments are able to match all entries in the sequence.
Expand Down Expand Up @@ -65,7 +69,7 @@
A segment binding variable is either:
- A symbol starting with `??`
- A sequence of the form `(?? <binding>)`."
- A sequence of the form `(?? <binding> ...)`."
[pattern]
(or (and (simple-symbol? pattern)
(u/re-matches? #"^\?\?[^\?].*" (name pattern)))
Expand All @@ -80,7 +84,7 @@
A reverse-segment binding variable is either:
- A symbol starting with `$$`
- A sequence of the form `(:$$ <binding>)`."
- A sequence of the form `(:$$ <binding> ...)`."
[pattern]
(or (and (simple-symbol? pattern)
(u/re-matches? #"^\$\$[^\$].*" (name pattern)))
Expand Down
58 changes: 57 additions & 1 deletion test/emmy/pattern/match_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,63 @@
(m/all-results
find-two-ints
'(1.1 [1 3] 2.3 3 6.5 x [3 5] 4 22)))
"segments accomplish searches")))
"segments accomplish searches"))

(letfn [(ends-with-odd [xs]
(when (seq xs)
(odd? (peek xs))))
(starts-with-odd [xs]
(when (seq xs)
(odd? (first xs))))]
(is (= [{:xs [1], :ys [2 3 4 5 6 7 8]}
{:xs [1 2 3], :ys [4 5 6 7 8]}
{:xs [1 2 3 4 5], :ys [6 7 8]}
{:xs [1 2 3 4 5 6 7], :ys [8]}]
(-> (m/sequence
(m/segment :xs ends-with-odd)
(m/segment :ys))
(m/all-results
'(1 2 3 4 5 6 7 8))))
"segment predicates work")

(is (= [{:xs [1], :ys [3 2 4], :zs [3 2 4]}
{:xs [1 3], :ys [2 4], :zs [2 4]}]
(-> (m/sequence
(m/segment :xs seq)
(m/segment :ys)
(m/segment :xs ends-with-odd)
(m/segment :zs))
(m/all-results
'(1 3 2 4 1 3 2 4))))
"predicates stack (first seq, then ends-with-odd)")

(let [input '(1 2 3 4 4 3 2 1)]
(is (= '[{x [1]}
{x [1 2]}
{x [1 2 3]}
{x [1 2 3 4]}]
(-> '[(?? x) (?? _) ($$ x)]
(m/all-results input)))
"match all prefix and reverse suffixes")

(is (= '[{x [1]}
{x [1 2 3]}]
(-> ['(?? x) '(?? _) (list '$$ 'x starts-with-odd)]
(m/all-results input))
(-> [(list '?? 'x ends-with-odd) '(?? _) '($$ x)]
(m/all-results input)))
"pass when the reversed segments END with odd, or the non-reversed
start with odd.")

(is (= '[{x (1 2 3 4 4 3 2 1)}
{x (3 4 4 3 2 1)}
{x (3 2 1)}
{x (1)}]
(-> ['(?? _) (list '?? 'x (fn [xs]
(when (seq xs)
(odd? (first xs)))))]
(m/all-results input)))
"pass when the final segment passes the predicate."))))

(testing "twin, equal-binding segments"
(let [xs-xs (m/sequence
Expand Down
10 changes: 9 additions & 1 deletion test/emmy/pattern/rule_test.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@
(is (r/failed? (r/fail x)))
(is (r/failed? ((r/rule ?x !=> ?x) x))))


(let [check (r/rule (+ (? x odd?) (? x #{1})) => (+ 2))]
(is (= '(+ 2) (check '(+ 1 1)))
"x is both odd and #{1}")
(is (r/failed? (check '(+ 3 3)))
"only passing ONE predicate fails!"))


(testing "pattern with spliced bindings"
(let [z 'x]
(is (= {'x [1 2], 'z 3}
Expand Down Expand Up @@ -169,7 +177,7 @@
(let [z 2
R (r/rule
(~(m/eq '+) () ~(m/match-when odd? (m/bind '?a))
?a ??b)
?a ??b)
=> (* ~@[z] ?a ??b))]
(is (= '(* 2 3 y z)
(R '(+ () 3 3 y z)))))
Expand Down

0 comments on commit 23248a9

Please sign in to comment.