1 // Written in the D programming language.
2 /**
3 Haystack filter.
4 
5 Copyright: Copyright (c) 2017, Radu Racariu <radu.racariu@gmail.com>
6 License:   $(LINK2 www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
7 Authors:   Radu Racariu
8 **/
9 module haystack.filter;
10 
11 import std.algorithm    : move;
12 import std.functional   : equalTo, lessThan, greaterThan;
13 
14 import haystack.tag;
15 import haystack.util.misc : isCharInputRange, Own;
16 import haystack.zinc.lexer;
17 
18 /// Filter parsing exception
19 class FilterException : Exception
20 {
21     immutable this(string msg)
22     {
23         super(msg);
24     }
25 }
26 
27 /// An empty resolver
28 alias EmptyResolver = Path.emptyResolver!Dict;
29 
30 /// The default string based haystack filter
31 alias HaystackFilter = Filter!string;
32 
33 /**
34 Haystack filter
35 */
36 struct Filter(Range)
37 if (isCharInputRange!Range)
38 {
39     alias Lexer = ZincLexer!Range;
40 
41     this(Range r)
42     {
43         auto lexer = Lexer(r);
44         if (lexer.empty)
45             throw InvalidFilterException;
46         or = parseOr(lexer);
47     }
48     
49     this(ref return scope typeof(this) other)
50     {
51         this.or = Or(other.or);
52     }
53 
54     @disable this();
55 
56     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
57     {
58         return or.eval(obj, resolver);
59     }
60 
61     size_t toHash() const nothrow
62     {
63         return or.toHash();
64     }
65 
66     bool opEquals()(auto ref const Filter other) const
67     {
68         return or == other.or;
69     }
70 
71 private:
72     Or or; // start node
73 
74     // parse `or` expression
75     Or parseOr(ref Lexer lexer, bool group = false)
76     {
77         auto a = parseAnd(lexer);
78         for (; !lexer.empty; lexer.popFront())
79         {
80             if (lexer.front.isSpace)
81                 continue;
82             if (lexer.front.isOf(TokenType.id, "or".tag))
83             {
84                 lexer.popFront();
85                 auto b = parseOr(lexer, group);
86                 return Or(move(a), move(b));
87             }
88             else if (lexer.front.isEmpty && !group)
89             {
90                 throw InvalidFilterException;
91             }
92             else
93             {
94                 break;
95             }
96         }
97         return Or(move(a));
98     }
99 
100     // parse `and` expression
101     And parseAnd(ref Lexer lexer)
102     {
103         auto a = parseTerm(lexer);
104         for (; !lexer.empty; lexer.popFront())
105         {
106             if (lexer.front.isSpace)
107                 continue;
108             if (lexer.front.isOf(TokenType.id, "and".tag))
109             {
110                 lexer.popFront();
111                 auto b = parseAnd(lexer);
112                 return And(move(a), move(b));
113             }
114             else
115             {
116                 break;
117             }
118         }
119         return And(move(a));
120     }
121 
122     // parse a term
123     Term parseTerm(ref Lexer lexer)
124     {
125         import std.traits   : EnumMembers;
126 
127         enum State { parens, has, missing, cmp }
128         State state;
129         Path crtPath = void;
130 
131         for (; !lexer.empty; lexer.popFront())
132         {
133             parseStart:
134             if (lexer.front.isSpace)
135                 continue;
136             switch (state)
137             {
138                 case State.parens:
139                     if (!lexer.front.hasChr('('))
140                     {
141                         state   = State.has;
142                         goto parseStart;
143                     }
144                     else
145                     {
146                         lexer.popFront();
147                         auto term = Term(parseOr(lexer, true));
148                         if (!lexer.front.hasChr(')'))
149                             throw InvalidFilterException;
150                         lexer.popFront();
151                         return move(term);
152                     }
153 
154                 case State.has:
155                     if (!lexer.front.isId)
156                     {
157                         throw InvalidFilterException;
158                     }
159                     else
160                     {
161                         auto name = lexer.front.value!Str;
162                         if (name == "not")
163                         {
164                             state = State.missing;
165                             continue;
166                         }
167                         crtPath = parsePath(lexer);
168                         state = State.cmp;
169                         goto parseStart;
170                     }
171                 
172                 case State.missing:
173                     auto path = parsePath(lexer);
174                     return Term(Missing(path));
175 
176                 case State.cmp:
177                     auto chr = lexer.front.isEmpty ?  lexer.front.curChar : dchar.init;
178                     bool hasEq; 
179                     if (chr == '<' || chr == '>' || chr == '=' || chr == '!')
180                     {
181                         if (lexer.empty)
182                              throw InvalidFilterException;
183                         lexer.popFront();
184                         if (lexer.front.hasChr('='))
185                         {
186                             hasEq = true;
187                             lexer.popFront();
188                         }
189                         if (lexer.empty || (chr == '=' && !hasEq))
190                             throw InvalidFilterException;
191                         
192                         for (; !lexer.empty; lexer.popFront())
193                         {
194                             if (lexer.front.isSpace)
195                                 continue;
196                             if (lexer.front.isScalar || lexer.front.type == TokenType.id)
197                             {
198                                 Tag tag = cast(Tag) lexer.front.tag;
199                                 if (tag.hasValue!Num)
200                                 {
201                                     auto num = tag.get!Num; 
202                                     if (num.isNaN || num.isINF)
203                                         throw InvalidFilterException;
204                                 }
205                                 if (lexer.front.type == TokenType.id) // parse true - false
206                                 {
207                                     auto id = tag.get!Str;
208                                     if (id == "true")
209                                         tag = true.tag;
210                                     else if (id == "false")
211                                         tag = false.tag;
212                                     else
213                                         throw InvalidFilterException;
214                                 }
215                                 lexer.popFront();
216 
217                                 string op;
218                                 foreach (opEnum; EnumMembers!(Cmp.Op))
219                                 {
220                                     if (!hasEq)
221                                     {
222                                         if (opEnum[0] == chr)
223                                         {
224                                             op = opEnum;
225                                             break;
226                                         }
227                                     }
228                                     else
229                                     {
230                                         if (opEnum[0] == chr && opEnum[$ - 1] == '=')
231                                         {
232                                             op = opEnum;
233                                             break;
234                                         }
235                                     }
236                                 }
237                                 return Term(Cmp(crtPath, op, tag));
238                             }
239                             else // invalid term
240                                 break;
241                         }
242                         throw InvalidFilterException;
243                     }
244                     else
245                     {
246                         return Term(Has(crtPath));
247                     }
248 
249                 default:
250                     throw InvalidFilterException;
251             }
252         }
253         return Term.makeEmpty();
254     }
255 
256     // parse a Path
257     Path parsePath(ref Lexer lexer)
258     {
259         import std.array : appender;
260 
261         auto buf = appender!(string[])();
262         enum State { id, sep }
263         State state;
264 
265         loop:
266         for (; !lexer.empty; lexer.popFront())
267         {
268             switch (state)
269             {
270                 case State.id:
271                     if (!lexer.front.isId)
272                         throw InvalidFilterException;
273                     
274                     auto name = lexer.front.value!Str;
275                     buf.put(name.val);
276                     state = State.sep;
277                     break;
278 
279                 case State.sep:
280                     if (!lexer.front.hasChr('-'))
281                         break loop;
282                     if (lexer.empty)
283                         throw InvalidFilterException;
284                     lexer.popFront();
285                     if (lexer.front.hasChr('>') && !lexer.empty)
286                         state = State.id;
287                     else
288                         throw InvalidFilterException;
289                     break;
290 
291                 default:
292                     throw InvalidFilterException;
293             }
294         }
295         return Path(buf.data);
296     }
297 
298     static InvalidFilterException = new immutable FilterException("Invalid filter input.");
299 }
300 
301 unittest
302 {
303     alias StrFilter = Filter!(string);
304     
305     auto filter = StrFilter("id or bar");
306     assert(filter.eval(["id": marker], &EmptyResolver));
307     assert(filter.eval(["bar": marker], &EmptyResolver));
308 
309     filter = StrFilter("not bar");
310     assert(filter.eval(["id": marker], &EmptyResolver));
311     assert(!filter.eval(["bar": marker], &EmptyResolver));
312 
313     filter = StrFilter("test == true");
314     assert(filter.eval(["test": true.tag], &EmptyResolver));
315 
316     import std.exception : assertThrown;
317     assertThrown(StrFilter("test = ").eval(["foo": marker], &EmptyResolver));
318     assertThrown(StrFilter("test('call')").eval(["null": marker], &EmptyResolver));
319     
320 
321     filter = StrFilter("age == 6");
322     assert(filter.eval(["age": 6.tag], &EmptyResolver));
323     assert(!filter.eval(["bar": marker], &EmptyResolver));
324 
325     filter = StrFilter("age == 6 and foo");
326     assert(filter.eval(["age": 6.tag, "foo": marker], &EmptyResolver));
327 
328     filter = StrFilter("(age and foo)");
329     assert(filter.eval(["age": 6.tag, "foo": marker], &EmptyResolver));
330 
331     filter = StrFilter(`name == "foo bar"`);
332     assert(filter.eval(["name": "foo bar".tag], &EmptyResolver));
333 
334     filter = StrFilter(`name >= "foo bar"`);
335     assert(filter.eval(["name": "foo bar".tag], &EmptyResolver));
336 
337     filter = StrFilter(`a and b or foo`);
338     assert(filter.eval(["foo": marker], &EmptyResolver));
339 
340     filter = StrFilter(`a or b or foo`);
341     assert(filter.eval(["foo": marker], &EmptyResolver));
342 
343     filter = StrFilter(`a and b and c`);
344     assert(filter.eval(["a": marker, "b": marker, "c": marker], &EmptyResolver));
345 
346     filter = StrFilter(`a and b and c or d`);
347     assert(filter.eval(["d": marker], &EmptyResolver));
348 
349     filter = StrFilter(`(a or b) and c`);
350     assert(filter.eval(["b": marker, "c": marker], &EmptyResolver));
351 
352     assert(StrFilter(`a or b`) == StrFilter(`a or b`));
353 
354     filter  = StrFilter("age == 6 and foo");
355     StrFilter* filterCopy = new StrFilter(filter);
356     assert(filter.eval(["foo": marker, "age": 6.tag], &EmptyResolver));
357     assert(filterCopy.eval(["foo": marker, "age": 6.tag], &EmptyResolver));
358 
359     assert(filterCopy.toHash() == filter.toHash());
360     assert(*filterCopy == filter);
361 
362     static struct PtrWrap
363     {
364         StrFilter* p;
365         
366         size_t toHash() const nothrow
367         {
368             return p.toHash();
369         }
370 
371         bool opEquals()(auto ref const scope PtrWrap other) const scope
372         in (p !is null && other.p !is null)
373         {
374             return *p == *other.p;
375         }
376     }
377 
378     string[PtrWrap] map;
379     map[PtrWrap(filterCopy)] = "a";
380     assert(PtrWrap(&filter) in map);
381 }
382 
383 /**
384 Or condition
385 */
386 struct Or
387 {
388     And a;
389     Own!Or b;
390 
391     this(And a)
392     {
393         this.a = move(a);
394     }
395 
396     this(And a, Or b)
397     {
398         this.a = move(a);
399         this.b = move(b);
400     }
401 
402     this(ref return scope Or other)
403     {
404         this.a = And(other.a);
405         if (other.b.isNull)
406             return;
407         this.b = Or(*other.b);
408     }
409 
410     @disable this(this);
411 
412     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
413     {
414         assert(a.isValid, "Invalid 'or' expression.");
415         if (!b.isNull)
416             return a.eval(obj, resolver) || b.eval(obj, resolver);
417         else 
418             return a.eval(obj, resolver);
419     }
420     
421     size_t toHash() const nothrow @trusted
422     {
423         enum prime  = 31;
424         size_t hash = prime * a.toHash();
425         return prime * hash + (b.isNull ? 0 : b.toHash());
426     }
427 
428     bool opEquals()(auto ref const Or other) const nothrow
429     {
430         return a == other.a && b == other.b;
431     }
432 }
433 
434 /**
435 And condition
436 */
437 struct And
438 {
439     Term a;
440     Own!And b;
441 
442     this(Term a)
443     {
444         this.a = move(a);
445     }
446 
447     this(Term a, And b)
448     {
449         this.a = move(a);
450         this.b = move(b);
451     }
452 
453     this(ref return scope And other)
454     {
455         this.a = Term(other.a);
456         if (other.b.isNull)
457             return;
458         this.b = And(*other.b);
459     }
460 
461     @disable this(this);
462 
463     @property bool isValid() const
464     {
465         return a.isValid;
466     }
467 
468     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
469     {
470         assert(a.isValid, "Invalid 'and' expression.");
471         if (!b.isNull && b.isValid)
472             return a.eval(obj, resolver) && b.eval(obj, resolver);
473         else 
474             return a.eval(obj, resolver);
475     }
476 
477     @safe size_t toHash() const nothrow
478     {
479         enum prime  = 31;
480         size_t hash = prime * a.toHash();
481         return prime * hash + (b.isNull ? 0 : b.toHash());
482     }
483 
484     bool opEquals()(auto ref const And other) const nothrow
485     {
486         return a == other.a && b == other.b;
487     }
488 }
489 
490 /**
491 A filter term
492 */
493 struct Term
494 {
495     enum Type
496     {
497         or,
498         has,
499         missing,
500         cmp,
501         empty
502     }
503 
504     this(Or or)
505     {
506         type    = Type.or;
507         val.or  = or.move();
508     }
509 
510     this(Has has)
511     {
512         type    = Type.has;
513         val.has = has;
514     }
515 
516     this(Missing missing)
517     {
518         type        = Type.missing;
519         val.missing = missing;
520     }
521 
522     this(Cmp cmp)
523     {
524         type    = Type.cmp;
525         val.cmp = cmp;
526     }
527 
528     this(ref return scope Term other)
529     {
530         this.type   = other.type;
531         final switch (type)
532         {
533             case Type.or:
534                 this.val.or         = Or(*other.val.or);
535                 break;
536             case Type.has:
537                 this.val.has        = Has(other.val.has);
538                 break;
539             case Type.missing:
540                 this.val.missing    = Missing(other.val.missing);
541                 break;
542             case Type.cmp:
543                 this.val.cmp        = Cmp(other.val.cmp);
544                 break;
545             case Type.empty:
546                 break;
547         }
548     }
549 
550     static Term makeEmpty()
551     {
552         return Term(Type.empty);
553     }
554     
555     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
556     {
557         final switch (type)
558         {
559             case Type.or:
560                 return val.or.eval(obj, resolver);
561             case Type.has:
562                 return val.has.eval(obj, resolver);
563             case Type.missing:
564                 return val.missing.eval(obj, resolver);
565             case Type.cmp:
566                 return val.cmp.eval(obj, resolver);
567             case Type.empty:
568                 return false;
569         }
570     }
571 
572     @trusted size_t toHash() const nothrow
573     {
574         final switch (type)
575         {
576             case Type.or:
577                 return val.or.toHash();
578             case Type.has:
579                 return val.has.toHash();
580             case Type.missing:
581                 return val.missing.toHash();
582             case Type.cmp:
583                 return val.cmp.toHash();
584             case Type.empty:
585                 return 31;
586         }
587     }
588 
589     bool opEquals()(auto ref const Term other) const nothrow
590     {
591         if (other.type != type)
592             return false;
593 
594         final switch (type)
595         {
596             case Type.or:
597                 return val.or == other.val.or;
598             case Type.has:
599                 return val.has == other.val.has;
600             case Type.missing:
601                 return val.missing == other.val.missing;
602             case Type.cmp:
603                 return val.cmp == other.val.cmp;
604             case Type.empty:
605                 return true;
606         }
607     }
608 
609     @property bool isValid() const
610     {
611         return type != Type.empty;
612     }
613     
614     ~this()
615     {
616         final switch (type)
617         {
618             case Type.or:
619                 val.or.destroy(); break;
620             case Type.has:
621                 val.has.destroy(); break;
622             case Type.missing:
623                 val.missing.destroy(); break;
624             case Type.cmp:
625                 val.cmp.destroy(); break;
626             case Type.empty:
627                 break;
628         }
629     }
630 
631 private:
632 
633     @disable this();
634     @disable this(this);
635 
636     this(Type type)
637     {
638         this.type = type;
639     }
640 
641     union Val
642     {
643         Own!Or or       = void;
644         Has has         = void;
645         Missing missing = void;
646         Cmp cmp         = void;
647     }
648     
649     Type type   = Type.empty;
650     Val val     = void;
651 }
652 
653 /**
654 A filter path.
655 Can be a simple path that resolves to a dict,
656 or a chained path that resolves across multiple dicts
657 */
658 struct Path
659 {
660     this (string name)
661     {
662         this._segments = [name];
663     }
664 
665     this(string[] segments)
666     {
667         this._segments = segments;
668     }
669     @disable this();
670 
671     Tag resolve(Obj, Resolver)(Obj obj, Resolver resolver) const
672     {
673         import std.traits : Unqual, isAssociativeArray, KeyType, ValueType;
674 
675         static if (isAssociativeArray!Obj)
676             alias Type = Unqual!(ValueType!Obj)[Unqual!(KeyType!Obj)];
677         else
678             alias Type = Unqual!Obj;
679         
680         if (segments.length == 0 || segments[0].length == 0)
681             return Tag.init;
682 
683         static if (is(Type : Dict))
684         {
685             if (segments.length == 1)
686                 return dictResolver(obj);
687             else if (resolver !is null)
688                 return resolver(obj, this);
689             else 
690                 return Tag.init;
691         }
692         else
693         {
694             return resolver(obj, this);
695         }
696     }
697 
698     @property string root() const
699     {
700         return _segments[0];
701     }
702     
703     @property ref const(string[]) segments() const
704     {
705         return _segments;
706     }
707 
708     static Tag emptyResolver(Obj)(Obj, ref const(Path))
709     {
710         return Tag.init;
711     }
712 
713     size_t toHash() const nothrow
714     {
715         enum prime  = 31;
716         size_t hash = prime;
717         foreach (seg; _segments)
718             foreach (c; seg)
719                 hash = (hash * prime) + c;
720         return hash;
721     }
722 
723     bool opEquals()(auto ref const Path other) const nothrow
724     {
725         return _segments == other._segments;
726     }
727     
728 private:
729 
730     Tag dictResolver(const(Dict) dict) const
731     in (segments.length)
732     {
733         return dict.get(root, Tag.init);
734     }
735 
736     string[] _segments;
737 }
738 unittest
739 {
740     auto path = Path("test");
741     auto dictResolver = &Path.emptyResolver!Dict;
742     assert(path.resolve(["test": marker], dictResolver) == marker);
743     auto range = ["test": marker].byKeyValue();
744     Tag rangeResolver (typeof(range) obj, ref const(Path) path)
745     {
746         import std.algorithm : find;
747         return obj.find!(kv => kv.key == path.root).front.value;
748     }
749     assert(path.resolve(range, &rangeResolver) == marker);
750 
751     Dict equip = ["id":"equip".tag, "name": "foobar".tag];
752     
753     Tag resolver(ref const(Dict) dict, ref const(Path) path)
754     {
755         foreach (i, ref p; path.segments)
756         {
757             if (i == 0)
758             {
759                 if (!dict.has(p) || equip["id"] != dict[p])
760                     break;
761             }
762             if (i == 1)
763             {
764                 return equip.get(p, Tag.init);
765             }
766         }
767 
768         return Tag.init;
769     }
770     path = Path(["equipRef", "name"]);
771     assert(path.resolve(["equipRef": "equip".tag], &resolver) == "foobar".tag);
772 
773 }
774 
775 ///////////////////////////////////////////////////////////////
776 // Basic predicates
777 ///////////////////////////////////////////////////////////////
778 
779 /**
780 Dict has the path
781 */
782 struct Has
783 {
784     this(string path)
785     {
786         this.path = Path(path);
787     }
788 
789     this(Path path)
790     {
791         this.path = path;
792     }
793 
794     this(ref return scope Has other)
795     {
796         this.path = other.path;
797     }
798     @disable this();
799 
800     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
801     {
802         return path.resolve(obj, resolver).hasValue;
803     }
804 
805     @property Dict tags()
806     {
807         return [path.root: marker];
808     }
809 
810     size_t toHash() const nothrow
811     {
812         return 31 * path.toHash();
813     }
814 
815     bool opEquals()(auto ref const Has other) const nothrow
816     {
817         return path == other.path;
818     }
819     
820 private:
821     Path path;
822 }
823 unittest
824 {
825     auto has = Has("foo");
826     assert(has.eval(["foo":marker], &EmptyResolver));
827     assert(has.tags == ["foo":marker]);
828 }
829 
830 /**
831 Dict missing the path
832 */
833 struct Missing
834 {
835     this(string path)
836     {
837         this.has = Path(path);
838     }
839 
840     this(Path path)
841     {
842         this.has = path;
843     }
844 
845     this(ref return scope Missing other)
846     {
847         this.has = Has(other.has);
848     }
849     @disable this();
850 
851     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
852     {
853         return !has.eval(obj, resolver);
854     }
855 
856     alias has this;
857     Has has;
858 }
859 unittest
860 {
861     auto missing = Missing("foo");
862     assert(missing.eval(["bar": marker], &EmptyResolver));
863     assert(missing.tags == ["foo": marker]);
864 }
865 
866 /**
867 Dict has the path that satisfies the predicate
868 */
869 struct Cmp
870 {
871     enum Op : string
872     {
873         eq = "==",
874         notEq = "!=",
875         less = "<",
876         lessOrEq = "<=",
877         greater = ">",
878         greaterOrEq = ">="
879     }
880 
881     this(string path, string op, Tag constant)
882     {
883         this(Path(path), cast(Op) op, constant);
884     }
885 
886     this(Path path, string op, Tag constant)
887     {
888         this(path, cast(Op) op, constant);
889     }
890 
891     this(Path path, Op op, Tag constant)
892     {
893         this.path       = path;
894         this.op         = op;
895         this.constant   = constant;
896     }
897 
898     this(ref return scope Cmp other)
899     {
900         this.path       = other.path;
901         this.op         = other.op;
902         this.constant   = other.constant;
903     }
904     @disable this();
905 
906     bool eval(Obj, Resolver)(Obj obj, Resolver resolver) const
907     {
908         auto val = path.resolve(obj, resolver);
909         return predicate(val);
910     }
911 
912     @property Dict tags()
913     {
914         foreach (Type; Tag.AllowedTypes)
915         {
916             if (constant.hasValue!Type)
917                 return [path.segments[$ - 1]: Tag(Type.init)];
918         }
919         return [path.segments[$]: marker];
920     }
921 
922     size_t toHash() const nothrow
923     {
924         enum prime  = 31;
925         size_t hash = prime * path.toHash();
926         foreach (c; op)
927             hash = (hash * prime) + c;
928         return prime * hash + constant.toHash();
929     }
930 
931     bool opEquals()(auto ref const Cmp other) const nothrow
932     {
933         if (op != other.op || path != other.path)
934             return false;
935         try
936             return constant == other.constant;
937         catch (Exception e)
938             return false;
939     }
940 
941 private:
942 
943     bool predicate(ref const(Tag) cmp) const
944     {
945         if (!cmp.hasValue)
946             return false;
947 
948         final switch(op)
949         {
950             case Op.eq:
951                 return equalTo(cmp, constant);
952             case Op.notEq:
953                 return !equalTo(cmp, constant);
954             case Op.less:
955                 return lessThan(cmp, constant);
956             case Op.lessOrEq:
957                 return lessThan(cmp, constant) || equalTo(cmp, constant); 
958             case Op.greater:
959                 return greaterThan(cmp, constant);
960             case Op.greaterOrEq:
961                 return greaterThan(cmp, constant) || equalTo(cmp, constant);
962         }
963     }
964 
965     Path path;
966     Op op;
967     Tag constant;
968 }
969 unittest
970 {
971     auto cmp = Cmp("val", "==", true.tag);
972     assert(cmp.eval(["val": true.tag], &EmptyResolver));
973     assert(cmp.tags == ["val": Bool.init.tag]);
974     
975     cmp = Cmp("val", "!=", true.tag);
976     assert(cmp.eval(["val": false.tag], &EmptyResolver));
977 
978     cmp = Cmp("val", ">", false.tag);
979     assert(cmp.eval(["val": true.tag], &EmptyResolver));
980 
981     cmp = Cmp("val", ">", false.tag);
982     assert(cmp.eval(["val": true.tag], &EmptyResolver));
983 
984     cmp = Cmp("val", "==", 1.tag);
985     assert(cmp.eval(["val": 1.tag], &EmptyResolver));
986 
987     cmp = Cmp("val", "!=", 1.tag);
988     assert(cmp.eval(["val": 0.tag], &EmptyResolver));
989 
990     cmp = Cmp("val", ">", 100.tag);
991     assert(cmp.eval(["val": 999.tag], &EmptyResolver));
992 
993     cmp = Cmp("val", "<=", 100.tag);
994     assert(cmp.eval(["val": 100.tag], &EmptyResolver));
995 
996     cmp = Cmp("val", "<", 100.tag);
997     assert(cmp.eval(["val": 99.tag], &EmptyResolver));
998 
999     cmp = Cmp("val", ">=", 99.tag);
1000     assert(cmp.eval(["val": 99.tag], &EmptyResolver));
1001 
1002     cmp = Cmp("val", ">=", "foo".tag);
1003     assert(cmp.eval(["val": "foo".tag], &EmptyResolver));
1004 
1005     cmp = Cmp("val", ">", "fo".tag);
1006     assert(cmp.eval(["val": "foo".tag], &EmptyResolver));
1007 }