Repository: microsoft/rego-cpp (language: python)

Issue:
`strings` built-ins
- `concat`
- `contains`
- `endswith`
- `format_int`
- `indexof`
- `indexof_n`
- `lower`
- `replace`
- `split`
- `sprintf`
- `startswith`
- `strings.any_prefix_match`
- `strings.any_suffix_match`
- `strings.replace_n`
- `strings.reverse`
- `substring`
- `trim`
- `trim_left`
- `trim_prefix`
- `trim_right`
- `trim_space`
- `trim_suffix`
- `upper`


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml
index e046ced7..b3420469 100644
--- a/.github/workflows/pr_gate.yml
+++ b/.github/workflows/pr_gate.yml
@@ -12,8 +12,8 @@ jobs:
       matrix:
         path: 
           - check: src
-            exclude: ''
-          - check: test
+            exclude: (/base64/)
+          - check: tests
             exclude: ''
           - check: tools
             exclude: ''
diff --git a/CHANGELOG b/CHANGELOG
index aada045b..dccff454 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,38 @@
 # Changelog
 
-## [2023-08-12 - Version 0.2.5](https://github.com/microsoft/scenepic/releases/tag/v0.2.5)
+## 2023-08-12 - Version 0.2.6
+Adding the `aggregates`, `arrays`, and `strings` builtins.
+
+**New Features**
+- `aggregates` builtins
+- `arrays` builtins
+- `strings` builtins
+- The library now supports arbitrary precision integers
+
+**New Test Coverage**
+- `aggregates`
+- `array`
+- `comparisonexpr`
+- `compositebasedereference`
+- `helloworld`
+- `indexing`
+- `intersection`
+- `replacen`
+- `sprintf`
+- `strings`
+- `trim`
+- `trimleft`
+- `trimprefix`
+- `trimright`
+- `trimspace`
+- `trimsuffix`
+- `union`
+
+**Bug Fixes**
+- Multiple implementation details exposed by testing
+
+
+## 2023-08-12 - Version 0.2.5
 Adding the `numbers` built-ins.
 
 **New Features**
@@ -17,7 +49,7 @@ Adding the `numbers` built-ins.
 - Multiple implementation details exposed by testing
 
 
-## [2023-08-12 - Version 0.2.4](https://github.com/microsoft/scenepic/releases/tag/v0.2.4)
+## 2023-08-12 - Version 0.2.4
 Adding a test driver that can execute OPA Rego-style YAML test files against the
 `regocpp` interpreter.
 
diff --git a/README.md b/README.md
index a351fa04..e4deb26f 100644
--- a/README.md
+++ b/README.md
@@ -154,14 +154,16 @@ LF     Line Feed
 At the moment only support a few builtins, but are actively working on adding
 all the standard builtins. The following builtins are currently supported:
 
+- `aggregates`
+- `arrays`
 - `numbers`
 - `sets`
+- `strings`
 - miscellaneous
     - `print`
-    - `startswith`
-    - `endswith`
-    - `count`
     - `to_number`
+    - `base64_encode`
+    - `base64_decode`
 
 ### Compatibility with the OPA Rego Go implementation
 
@@ -170,11 +172,28 @@ implementation. We have developed a test driver which runs the same tests
 and validates that we produce the same outputs. We currently pass the following
 test case suites:
 
+- `aggregates`
 - `arithmetic`
+- `array`
+- `comparisonexpr`
+- `compositebasedereference`
+- `helloworld`
+- `indexing`
+- `intersection`
 - `numbersrange`
 - `rand`
+- `replacen`
 - `sets`
-    
+- `sprintf`
+- `strings`
+- `trim`
+- `trimleft`
+- `trimprefix`
+- `trimright`
+- `trimspace`
+- `trimsuffix`
+- `union` 
+
 ## Contributing
 
 This project welcomes contributions and suggestions.  Most contributions require you to agree to a
diff --git a/VERSION b/VERSION
index 28af839c..a53741c0 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.2.5
\ No newline at end of file
+0.2.6
\ No newline at end of file
diff --git a/include/rego/builtins.h b/include/rego/builtins.h
index 72ce5f82..17b40e99 100644
--- a/include/rego/builtins.h
+++ b/include/rego/builtins.h
@@ -24,10 +24,13 @@ namespace rego
   class BuiltIns
   {
   public:
+    BuiltIns() noexcept;
     bool is_builtin(const Location& name) const;
     Node call(const Location& name, const Nodes& args) const;
     BuiltIns& register_builtin(const BuiltIn& built_in);
     const BuiltIn& at(const Location& name) const;
+    bool strict_errors() const;
+    BuiltIns& strict_errors(bool strict_errors);
 
     template <typename T>
     BuiltIns& register_builtins(const T& built_ins)
@@ -46,5 +49,6 @@ namespace rego
 
   private:
     std::map<Location, BuiltIn> m_builtins;
+    bool m_strict_errors;
   };
 }
\ No newline at end of file
diff --git a/include/rego/errors.h b/include/rego/errors.h
new file mode 100644
index 00000000..c879a8bb
--- /dev/null
+++ b/include/rego/errors.h
@@ -0,0 +1,10 @@
+#pragma once
+
+#include <string>
+
+namespace rego
+{
+  const std::string EvalTypeError = "eval_type_error";
+  const std::string EvalBuiltInError = "eval_builtin_error";
+  const std::string RegoTypeError = "rego_type_error";
+}
\ No newline at end of file
diff --git a/include/rego/lang.h b/include/rego/lang.h
index db2dcbe7..37fccce6 100644
--- a/include/rego/lang.h
+++ b/include/rego/lang.h
@@ -41,8 +41,7 @@ namespace rego
   inline const auto Object = TokenDef("object", flag::symtab);
   inline const auto Set = TokenDef("set");
   inline const auto EmptySet = TokenDef("empty-set");
-  inline const auto ObjectItem = TokenDef("object-item", flag::lookdown);
-  inline const auto RefObjectItem = TokenDef("ref-object-item");
+  inline const auto ObjectItem = TokenDef("object-item");
   inline const auto RawString = TokenDef("raw-string", flag::print);
   inline const auto JSONString = TokenDef("STRING", flag::print);
   inline const auto JSONInt = TokenDef("INT", flag::print);
@@ -68,9 +67,9 @@ namespace rego
   inline const auto Default = TokenDef("default");
   inline const auto Some = TokenDef("some");
   inline const auto SomeDecl = TokenDef("some-decl");
-  inline const auto IfTruthy = TokenDef("if");
-  inline const auto InSome = TokenDef("in-some");
-  inline const auto MemberOf = TokenDef("member-of");
+  inline const auto SomeExpr = TokenDef("some-expr");
+  inline const auto If = TokenDef("if");
+  inline const auto IsIn = TokenDef("in");
   inline const auto Contains = TokenDef("contains");
   inline const auto Else = TokenDef("else");
   inline const auto As = TokenDef("as");
@@ -79,6 +78,7 @@ namespace rego
   inline const auto ArrayCompr = TokenDef("array-compr");
   inline const auto ObjectCompr = TokenDef("object-compr");
   inline const auto SetCompr = TokenDef("set-compr");
+  inline const auto Membership = TokenDef("membership");
 
   // intermediate tokens
   inline const auto UnifyBody = TokenDef("unify-body");
@@ -89,11 +89,12 @@ namespace rego
   inline const auto DataItemSeq = TokenDef("data-item-seq");
   inline const auto DataItem = TokenDef("data-item", flag::lookdown);
   inline const auto DataObject = TokenDef("data-object", flag::symtab);
+  inline const auto DataObjectItem =
+    TokenDef("data-object-item", flag::lookdown);
   inline const auto DataArray = TokenDef("data-array");
   inline const auto DataSet = TokenDef("data-set");
   inline const auto Submodule = TokenDef("submodule", flag::lookdown);
   inline const auto DataTerm = TokenDef("data-term");
-  inline const auto ObjectItemHead = TokenDef("object-item-head");
   inline const auto ObjectItemSeq = TokenDef("object-item-seq");
   inline const auto ImportSeq = TokenDef("import-seq");
   inline const auto VarSeq = TokenDef("var-seq");
@@ -101,6 +102,7 @@ namespace rego
   inline const auto WithSeq = TokenDef("with-seq");
   inline const auto SkipSeq = TokenDef("skip-seq");
   inline const auto ItemSeq = TokenDef("item-seq");
+  inline const auto ItemSeq1 = TokenDef("item-seq-1");
 
   // data and input
   inline const auto Input = TokenDef("input", flag::symtab | flag::lookup);
@@ -146,6 +148,10 @@ namespace rego
   inline const auto Id = TokenDef("id");
   inline const auto Head = TokenDef("head");
   inline const auto Tail = TokenDef("tail");
+  inline const auto Head1 = TokenDef("head-1");
+  inline const auto Tail1 = TokenDef("tail-1");
+  inline const auto Head2 = TokenDef("head-2");
+  inline const auto Tail2 = TokenDef("tail-2");
   inline const auto Lhs = TokenDef("lhs");
   inline const auto Rhs = TokenDef("rhs");
   inline const auto Key = TokenDef("key", flag::print);
@@ -172,11 +178,13 @@ namespace rego
   inline const auto Keyword =
     TokenDef("keyword", flag::lookdown | flag::lookup);
   inline const auto Idx = TokenDef("idx");
+  inline const auto Idx1 = TokenDef("idx-1");
   inline const auto Skip = TokenDef("skip", flag::lookup);
   inline const auto NestedBody = TokenDef("nested-body", flag::symtab);
   inline const auto BuiltInHook = TokenDef("builtin-hook", flag::lookup);
   inline const auto RuleRef = TokenDef("rule-ref");
   inline const auto Item = TokenDef("item");
+  inline const auto Item1 = TokenDef("item-1");
   inline const auto Enumerate = TokenDef("enumerate");
   inline const auto Compr = TokenDef("compr");
   inline const auto Merge = TokenDef("merge");
@@ -198,6 +206,7 @@ namespace rego
   inline const auto Square = TokenDef("square");
   inline const auto Paren = TokenDef("paren");
   inline const auto Not = TokenDef("not");
+  inline const auto Comma = TokenDef("comma");
   inline const auto Placeholder = TokenDef("_");
 
   inline const std::set<std::string> Keywords(
diff --git a/include/rego/passes.h b/include/rego/passes.h
index cbaffec5..e343160b 100644
--- a/include/rego/passes.h
+++ b/include/rego/passes.h
@@ -20,7 +20,10 @@ namespace rego
   const inline auto StringToken = T(JSONString) / T(RawString);
   const inline auto ExprToken = T(Term) / ArithToken / BoolToken / StringToken /
     T(Expr) / ScalarToken / TermToken / T(JSONString) / T(Array) / T(Set) /
-    T(Object) / T(Paren) / T(Not) / T(Dot) / T(MemberOf) / T(And) / T(Or);
+    T(Object) / T(Paren) / T(Not) / T(Dot) / T(And) / T(Or);
+  const auto inline MembershipToken = ScalarToken / T(JSONString) /
+    T(RawString) / T(Var) / T(Object) / T(Array) / T(Set) / T(Dot) / T(Paren) /
+    ArithToken / BoolToken / T(And) / T(Or);
 
   PassDef input_data();
   PassDef modules();
@@ -31,6 +34,7 @@ namespace rego
   PassDef elses();
   PassDef rules();
   PassDef build_calls();
+  PassDef membership();
   PassDef build_refs();
   PassDef structure();
   PassDef strings();
@@ -45,13 +49,13 @@ namespace rego
   PassDef compr();
   PassDef absolute_refs();
   PassDef merge_modules();
-  PassDef skips(const BuiltIns& builtins);
+  PassDef skips();
   PassDef unary();
   PassDef multiply_divide();
   PassDef add_subtract();
   PassDef comparison();
   PassDef assign(const BuiltIns& builtins);
-  PassDef skip_refs();
+  PassDef skip_refs(const BuiltIns& builtins);
   PassDef simple_refs();
   PassDef implicit_enums();
   PassDef init();
diff --git a/include/rego/resolver.h b/include/rego/resolver.h
index 41a907b2..29836536 100644
--- a/include/rego/resolver.h
+++ b/include/rego/resolver.h
@@ -19,6 +19,7 @@ namespace rego
     {
       Node node;
       PrintNode printer;
+      std::string str() const;
     };
 
     static BigInt get_int(const Node& node);
@@ -54,15 +55,34 @@ namespace rego
     static Node set_union(const Node& lhs, const Node& rhs);
     static Node set_difference(const Node& lhs, const Node& rhs);
     static Nodes resolve_varseq(const Node& varseq);
+    static Node unwrap(
+      const Node& term,
+      const Token& type,
+      const std::string& error_prefix,
+      const std::string& error_code,
+      bool specify_number = false);
+    static std::optional<Node> maybe_unwrap(
+      const Node& term, const std::set<Token>& types);
     static std::optional<Node> maybe_unwrap_number(const Node& term);
     static std::optional<Node> maybe_unwrap_string(const Node& term);
     static std::optional<Node> maybe_unwrap_bool(const Node& term);
+    static std::optional<Node> maybe_unwrap_array(const Node& term);
     static std::optional<Node> maybe_unwrap_set(const Node& term);
     static bool is_falsy(const Node& node);
     static bool is_truthy(const Node& node);
     static bool is_undefined(const Node& node);
     static Nodes object_lookdown(const Node& object, const Node& query);
     static Node inject_args(const Node& rulefunc, const Nodes& args);
+    static Node membership(
+      const Node& index, const Node& item, const Node& itemseq);
+    static Node membership(const Node& item, const Node& itemseq);
+    static std::vector<std::string> array_find(
+      const Node& array, const std::string& search);
+    static std::vector<std::string> object_find(
+      const Node& object, const std::string& search);
+    static std::string type_name(
+      const Token& type, bool specify_number = false);
+    static std::string type_name(const Node& node, bool specify_number = false);
   };
 
   std::ostream& operator<<(
diff --git a/include/rego/unifier.h b/include/rego/unifier.h
index a047072c..3502121a 100644
--- a/include/rego/unifier.h
+++ b/include/rego/unifier.h
@@ -43,20 +43,32 @@ namespace rego
       WithStack with_stack,
       const BuiltIns& builtins,
       UnifierCache cache);
-    friend std::ostream& operator<<(std::ostream& os, const Unifier& unifier);
 
   private:
+    struct Dependency
+    {
+      std::string name;
+      std::set<std::size_t> dependencies;
+      std::size_t score;
+    };
+
     Unifier rule_unifier(const Location& rule, const Node& rulebody);
-    void init_from_body(const Node& rulebody, std::vector<Node>& statements);
-    void add_variable(const Node& local);
-    void add_unifyexpr(const Node& unifyexpr);
+    void init_from_body(
+      const Node& rulebody, std::vector<Node>& statements, std::size_t root);
+    std::size_t add_variable(const Node& local);
+    std::size_t add_unifyexpr(const Node& unifyexpr);
     void add_withpush(const Node& withpush);
     void add_withpop(const Node& withpop);
-    void compute_dependency_scores();
     void reset();
-    std::size_t compute_dependency_score(const Node& unifyexpr);
+
+    void compute_dependency_scores();
     std::size_t compute_dependency_score(
-      Variable& var, std::set<Location>& visited);
+      std::size_t index, std::set<size_t>& visited);
+    std::size_t dependency_score(const Variable& var) const;
+    std::size_t dependency_score(const Node& expr) const;
+    std::size_t detect_cycles() const;
+    bool has_cycle(std::size_t id) const;
+
     Values evaluate(const Location& var, const Node& value);
     Values evaluate_function(
       const Location& var, const std::string& func_name, const Args& args);
@@ -77,7 +89,6 @@ namespace rego
     std::size_t scan_vars(const Node& expr, std::vector<Location>& locals);
     void pass();
     void execute_statements(Nodes::iterator begin, Nodes::iterator end);
-    void mark_invalid_values();
     void remove_invalid_values();
     Variable& get_variable(const Location& name);
     bool is_variable(const Location& name) const;
@@ -100,5 +111,7 @@ namespace rego
     std::size_t m_retries;
     Token m_parent_type;
     UnifierCache m_cache;
+    NodeMap<std::size_t> m_expr_ids;
+    std::vector<Dependency> m_dependency_graph;
   };
 }
\ No newline at end of file
diff --git a/include/rego/variable.h b/include/rego/variable.h
index bef95795..8c85124a 100644
--- a/include/rego/variable.h
+++ b/include/rego/variable.h
@@ -8,35 +8,24 @@ namespace rego
   class Variable
   {
   public:
-    Variable(const Node& local);
+    Variable(const Node& local, std::size_t id);
     bool unify(const Values& others);
     std::string str() const;
     bool remove_invalid_values();
-    void mark_invalid_values();
     void mark_valid_values();
     Values valid_values() const;
-    std::size_t increase_dependency_score(std::size_t amount);
     Node to_term() const;
-    const std::set<Location>& dependencies() const;
     Node bind();
     void reset();
 
-    std::size_t dependency_score() const;
-    Variable& dependency_score(std::size_t score);
     bool is_unify() const;
     bool is_user_var() const;
     Location name() const;
+    std::size_t id() const;
 
-    static std::size_t detect_cycles(std::map<Location, Variable>& variables);
     static bool is_unify(const std::string_view& name);
     static bool is_user_var(const std::string_view& name);
 
-    template <typename T>
-    void insert_dependencies(T start, T end)
-    {
-      m_dependencies.insert(start, end);
-    }
-
     friend std::ostream& operator<<(std::ostream& os, const Variable& variable);
     friend std::ostream& operator<<(
       std::ostream& os, const std::map<Location, Variable>& variables);
@@ -44,14 +33,12 @@ namespace rego
   private:
     bool intersect_with(const Values& others);
     bool initialize(const Values& others);
-    bool has_cycle(const std::map<Location, Variable>& variables) const;
 
     Node m_local;
-    std::set<Location> m_dependencies;
     ValueMap m_values;
     bool m_unify;
     bool m_user_var;
     bool m_initialized;
-    std::size_t m_dependency_score;
+    std::size_t m_id;
   };
 }
\ No newline at end of file
diff --git a/include/rego/wf.h b/include/rego/wf.h
index 1fa3a527..1dbc32ef 100644
--- a/include/rego/wf.h
+++ b/include/rego/wf.h
@@ -15,7 +15,7 @@ namespace rego
   inline const auto wf_bin_op = And | Or | Subtract;
 
   inline const auto wf_bool_op = Equals | NotEquals | LessThan |
-    LessThanOrEquals | GreaterThan | GreaterThanOrEquals | Not | MemberOf;
+    LessThanOrEquals | GreaterThan | GreaterThanOrEquals | Not;
 
   inline const auto wf_assign_op = Assign | Unify;
 
@@ -93,19 +93,19 @@ namespace rego
     ;
   // clang-format off    
 
-  inline const auto wf_keywords_tokens = wf_imports_tokens | InSome | Contains | Every;
+  inline const auto wf_keywords_tokens = wf_imports_tokens | IsIn | Contains | Every;
 
   // clang-format off
   inline const auto wf_pass_keywords =
     wf_pass_imports
-    | (Group <<= (wf_keywords_tokens | IfTruthy)++[1])
+    | (Group <<= (wf_keywords_tokens | If)++[1])
     ;
-  // clang-format off   
+  // clang-format off
 
   inline const auto wf_lists_tokens = wf_json | wf_arith_op | wf_bool_op | wf_bin_op |
     Paren | Var | Set | UnifyBody | ObjectItemSeq | Array | Dot | Assign |
     Unify | Object | RawString | Default | SomeDecl | Else | With |
-    InSome | Contains | ExprEvery | ObjectCompr | SetCompr | ArrayCompr | Undefined;
+    IsIn | Contains | ExprEvery | ObjectCompr | SetCompr | ArrayCompr | Comma | Undefined;
 
   // clang-format off
   inline const auto wf_pass_lists =
@@ -118,7 +118,7 @@ namespace rego
     | (UnifyBody <<= (SomeDecl | Group)++)
     | (Input <<= Var * ObjectItemSeq)[Var]
     | (Data <<= ObjectItemSeq)
-    | (Group <<= (wf_lists_tokens | IfTruthy)++[1])
+    | (Group <<= (wf_lists_tokens | If)++[1])
     | (List <<= Group++)
     | (SomeDecl <<= VarSeq * Group)
     | (ExprEvery <<= VarSeq * UnifyBody * EverySeq)
@@ -146,8 +146,8 @@ namespace rego
 
   inline const auto wf_rules_tokens = wf_json | wf_arith_op | wf_bool_op |
     wf_bin_op | Paren | Var | Set | UnifyBody | ObjectItemSeq | Array | Dot |
-    Assign | Unify | Object | RawString | SomeDecl | With | InSome | Contains |
-    ExprEvery | ObjectCompr | SetCompr | ArrayCompr | Undefined;
+    Assign | Unify | Object | RawString | SomeDecl | With | IsIn | Contains |
+    ExprEvery | ObjectCompr | SetCompr | ArrayCompr | Comma | Undefined;
 
   // clang-format off
   inline const auto wf_pass_rules =
@@ -179,11 +179,25 @@ namespace rego
     ;
   // clang-format on
 
-  inline const auto wf_refs_tokens = wf_call_tokens | Ref;
+  inline const auto wf_membership_tokens = wf_json | wf_arith_op | wf_bool_op |
+    wf_bin_op | Paren | Var | Set | UnifyBody | ObjectItemSeq | Array | Dot |
+    Assign | Unify | Object | RawString | SomeDecl | With | IsIn | Contains |
+    ExprEvery | ObjectCompr | SetCompr | ArrayCompr | ExprCall | Membership |
+    Undefined;
 
   // clang-format off
-  inline const auto wf_pass_build_refs =
+  inline const auto wf_pass_membership =
     wf_pass_build_calls
+    | (Membership <<= (Idx >>= Group | Undefined) * (Item >>= Group) * Group)
+    | (Group <<= wf_membership_tokens++[1])
+    ;
+  // clang-format off
+
+  inline const auto wf_refs_tokens = wf_membership_tokens | Ref;
+
+  // clang-format off
+  inline const auto wf_pass_build_refs =
+    wf_pass_membership
     | (Ref <<= RefHead * RefArgSeq)
     | (RefHead <<= Var | Array | Object | Set | ArrayCompr | ObjectCompr | SetCompr | ExprCall)
     | (RefArgSeq <<= (RefArgDot | RefArgBrack)++)
@@ -222,16 +236,17 @@ namespace rego
     | (RuleHeadObj <<= Expr * AssignOperator * Expr)
     | (RuleArgs <<= Term++[1])
     | (UnifyBody <<= (Literal | LiteralWith)++[1])
-    | (Literal <<= Expr | SomeDecl)
+    | (Literal <<= Expr | SomeDecl | SomeExpr)
     | (LiteralWith <<= UnifyBody * WithSeq)
     | (WithSeq <<= With++)
     | (With <<= VarSeq * Expr)
-    | (SomeDecl <<= VarSeq * InSome)
+    | (SomeDecl <<= VarSeq)
+    | (SomeExpr <<= (Idx >>= Expr | Undefined) * (Item >>= Expr) * IsIn)
     | (VarSeq <<= Var++)
-    | (InSome <<= Expr | Undefined)
-    | (Expr <<= (Term | wf_arith_op | wf_bin_op | wf_bool_op | wf_assign_op | Dot | ExprCall | ExprEvery | Expr)++[1])
+    | (IsIn <<= Expr | Undefined)
+    | (Expr <<= (Term | wf_arith_op | wf_bin_op | wf_bool_op | wf_assign_op | Dot | ExprCall | ExprEvery | Membership | Expr)++[1])
     | (ExprCall <<= VarSeq * ArgSeq)
-    | (ExprEvery <<= VarSeq * UnifyBody * InSome)
+    | (ExprEvery <<= VarSeq * UnifyBody * IsIn)
     | (VarSeq <<= Var++[1])
     | (ArgSeq <<= Expr++[1])
     | (AssignOperator <<= wf_assign_op)
@@ -246,11 +261,11 @@ namespace rego
     | (Array <<= Expr++)
     | (Set <<= Expr++)
     | (Object <<= ObjectItem++)
-    | (ObjectItem <<= ObjectItemHead * Expr)
-    | (ObjectItemHead <<= (Var | Ref | Scalar)++[1])
+    | (ObjectItem <<= (Key >>= Expr) * (Val >>= Expr))
     | (ObjectCompr <<= Expr * Expr * (Body >>= UnifyBody))
     | (ArrayCompr <<= Expr * (Body >>= UnifyBody))
     | (SetCompr <<= Expr * (Body >>= UnifyBody))
+    | (Membership <<= (Idx >>= Expr | Undefined) * (Item >>= Expr) * (ItemSeq >>= Expr))
     ;
   // clang-format on
 
@@ -272,7 +287,8 @@ namespace rego
     | (DataTerm <<= Scalar | DataArray | DataObject | DataSet)
     | (DataArray <<= DataTerm++)
     | (DataSet <<= DataTerm++)
-    | (DataObject <<= DataItem++)
+    | (DataObject <<= DataObjectItem++)
+    | (DataObjectItem <<= (Key >>= DataTerm) * (Val >>= DataTerm))
     ;
   // clang-format on
 
@@ -290,18 +306,18 @@ namespace rego
     | (LiteralEnum <<= (Item >>= Var) * (ItemSeq >>= Expr))
     | (Query <<= (Body >>= UnifyBody))
     | (Local <<= Var * Undefined)[Var]
-    | (Literal <<= Expr)
+    | (Literal <<= Expr | SomeExpr)
     | (ArgVar <<= Var * Undefined)[Var]
     | (ArgVal <<= Scalar | Array | Object | Set)
     | (DefaultRule <<= Var * (Val >>= Term))[Var]
-    | (Object <<= (ObjectItem | RefObjectItem)++)
-    | (ObjectItem <<= Key * (Val >>= Expr))[Key]
-    | (RefObjectItem <<= (Key >>= RefTerm) * (Val >>= Expr))
     | (Term <<= Scalar | Array | Object | Set | ArrayCompr | SetCompr | ObjectCompr)
+    | (ObjectCompr <<= Expr * Expr * (Body >>= NestedBody))
+    | (ArrayCompr <<= Expr * (Body >>= NestedBody))
+    | (SetCompr <<= Expr * (Body >>= NestedBody))
     | (RefTerm <<= Ref | Var)
     | (NumTerm <<= JSONInt | JSONFloat)
     | (RefArgBrack <<= RefTerm | Scalar | Object | Array | Set)
-    | (Expr <<= (RefTerm | NumTerm | Term | wf_arith_op | wf_bin_op | wf_bool_op | Unify | Expr | ExprCall | ExprEvery | Set | SetCompr)++[1])
+    | (Expr <<= (RefTerm | NumTerm | Term | wf_arith_op | wf_bin_op | wf_bool_op | Unify | Expr | ExprCall | ExprEvery | Set | SetCompr | Membership)++[1])
     | (Import <<= Var * Ref)[Var]
     | (ExprEvery <<= VarSeq * NestedBody)
     | (NestedBody <<= Key * (Val >>= UnifyBody))
@@ -312,6 +328,7 @@ namespace rego
   inline const auto wf_pass_replace_argvals =
     wf_pass_symbols
     | (RuleArgs <<= ArgVar++[1])
+    | (Literal <<= Expr)
     ;
   //
 
@@ -379,7 +396,7 @@ namespace rego
     wf_pass_skips
     | (UnaryExpr <<= ArithArg)
     | (ArithArg <<= (Expr | RefTerm | NumTerm | UnaryExpr | ExprCall))
-    | (Expr <<= (RefTerm | NumTerm | Term | Unify | Expr | UnaryExpr |ExprCall | ExprEvery | Set | SetCompr | UnaryExpr | wf_arith_op | wf_bin_op | wf_bool_op)++[1])
+    | (Expr <<= (RefTerm | NumTerm | Term | Unify | Expr | UnaryExpr |ExprCall | ExprEvery | Set | SetCompr | Membership | UnaryExpr | wf_arith_op | wf_bin_op | wf_bool_op)++[1])
     ;
   // clang-format on
 
@@ -397,7 +414,7 @@ namespace rego
     | (BinInfix <<= BinArg * (Op >>= And) * BinArg)
     | (BinArg <<= (Expr | wf_bin_tokens)++[1])
     | (UnaryExpr <<= ArithArg)
-    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery | Set | SetCompr | Add | Subtract | Or | wf_bool_op)++[1])
+    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery | Set | SetCompr | Membership | Add | Subtract | Or | wf_bool_op)++[1])
     ;
   // clang-format on
 
@@ -408,7 +425,7 @@ namespace rego
     | (ArithArg <<= Expr | wf_math_tokens)
     | (BinInfix <<= BinArg * (Op >>= wf_bin_op) * BinArg)
     | (BinArg <<= Expr | wf_bin_tokens)
-    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery | Set | SetCompr | wf_bool_op)++[1])
+    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery | Set | SetCompr | Membership | wf_bool_op)++[1])
     ;
   // clang-format on
 
@@ -421,7 +438,7 @@ namespace rego
     | (BinArg <<= wf_bin_tokens)
     | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralNot)++[1])
     | (LiteralNot <<= Expr)
-    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr| UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate | Set | SetCompr)++[1])
+    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr| UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate | Membership | Set | SetCompr)++[1])
     | (Enumerate <<= Expr)
     ;
   // clang-format on
@@ -430,8 +447,8 @@ namespace rego
   inline const auto wf_pass_assign =
     wf_pass_comparison
     | (AssignInfix <<= AssignArg * AssignArg)
-    | (AssignArg <<= wf_math_tokens | Term | BinInfix | BoolInfix | Enumerate)
-    | (Expr <<= (NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | AssignInfix | ExprCall | ExprEvery)++[1])
+    | (AssignArg <<= wf_math_tokens | Term | BinInfix | BoolInfix | Enumerate | Membership)
+    | (Expr <<= (NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | AssignInfix | ExprCall | ExprEvery | Membership)++[1])
     | (ExprEvery <<= VarSeq * NestedBody)
     ;
   // clang-format on
@@ -443,7 +460,7 @@ namespace rego
     wf_pass_skip_refs
     | (RefTerm <<= Var | SimpleRef)
     | (SimpleRef <<= Var * (Op >>= RefArgDot | RefArgBrack))
-    | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | AssignInfix | ExprCall | ExprEvery)
+    | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | AssignInfix | ExprCall | ExprEvery | Membership)
     | (ExprCall <<= Var * ArgSeq)
     | (RefHead <<= Var)
     ;
@@ -464,7 +481,7 @@ namespace rego
     wf_pass_init
     | (Module <<= (Import | RuleComp | DefaultRule | RuleFunc | RuleSet | RuleObj | Submodule)++)
     | (UnifyExpr <<= Var * (Val >>= Expr))
-    | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate)
+    | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate | Membership)
     | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprCompr | UnifyExprEnum | UnifyExprNot)++[1])
     | (UnifyExprWith <<= UnifyBody * WithSeq)
     | (UnifyExprCompr <<= Var * (Val >>= ArrayCompr | SetCompr | ObjectCompr) * NestedBody)
@@ -481,7 +498,7 @@ namespace rego
   inline const auto wf_pass_lift_to_rule =
     wf_pass_rulebody
     | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprNot)++[1])
-    | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate | ArrayCompr | SetCompr | ObjectCompr | Merge | ToValues)
+    | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate | Membership | ArrayCompr | SetCompr | ObjectCompr | Merge | ToValues)
     | (Merge <<= Var)
     | (ToValues <<= Expr)
     ;
@@ -496,7 +513,7 @@ namespace rego
     | (Array <<= Term++)
     | (Set <<= Term++)
     | (Object <<= ObjectItem++)
-    | (ObjectItem <<= Key * (Val >>= Term))[Key]
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))[Key]
     | (DataItem <<= Key * (Val >>= Module | Term))[Key]
     | (RuleComp <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * JSONInt)[Var]
     | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term) * JSONInt)[Var]
@@ -508,7 +525,7 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_unify =
     wf_pass_functions
-    | (Query <<= (Term | Binding | Undefined)++[1])
+    | (Query <<= (Term | Binding)++)
     | (Binding <<= Var * Term)[Var]
     | (Term <<= Scalar | Array | Object | Set)
     ;
@@ -517,7 +534,7 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_query =
     wf_pass_unify
-    | (Top <<= (Binding | Term | Undefined)++[1])
+    | (Top <<= (Binding | Term)++)
     ;
   // clang-format on
 }
\ No newline at end of file
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 318f5460..c388e56b 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -18,6 +18,7 @@ passes/modules.cc
 passes/lists.cc
 passes/imports.cc
 passes/keywords.cc
+passes/membership.cc
 passes/if_else.cc
 passes/rules.cc
 passes/build_calls.cc
@@ -46,8 +47,15 @@ passes/lift_to_rule.cc
 passes/functions.cc
 passes/unify.cc
 passes/query.cc
+builtins/aggregates.cc
+builtins/arrays.cc
+builtins/encoding.cc
 builtins/numbers.cc
 builtins/sets.cc
+builtins/strings.cc
+builtins/utf8.cc
+# third-party
+builtins/base64/base64.cpp
 )
 
 add_library(rego STATIC ${SOURCES})
@@ -65,6 +73,11 @@ target_precompile_headers(rego
     ${CMAKE_CURRENT_SOURCE_DIR}/../include/rego/wf.h
 )
 
+
+if (MSVC)
+  target_compile_options(rego PUBLIC "/Zc:__cplusplus")    
+endif()
+
 target_include_directories( rego
   PUBLIC
     $<INSTALL_INTERFACE:include>
diff --git a/src/builtins.cc b/src/builtins.cc
index 7722d013..210356af 100644
--- a/src/builtins.cc
+++ b/src/builtins.cc
@@ -8,64 +8,6 @@ namespace
 {
   using namespace rego;
 
-  Node startswith(const Nodes& args)
-  {
-    auto maybe_search = Resolver::maybe_unwrap_string(args[0]);
-    auto maybe_base = Resolver::maybe_unwrap_string(args[1]);
-    if (!maybe_search.has_value() || !maybe_base.has_value())
-    {
-      return err(args[0], "startswith: expected string arguments");
-    }
-
-    std::string search = Resolver::get_string(*maybe_search);
-    std::string base = Resolver::get_string(*maybe_base);
-    return Resolver::scalar(search.starts_with(base));
-  }
-
-  Node endswith(const Nodes& args)
-  {
-    auto maybe_search = Resolver::maybe_unwrap_string(args[0]);
-    auto maybe_base = Resolver::maybe_unwrap_string(args[1]);
-    if (!maybe_search.has_value() || !maybe_base.has_value())
-    {
-      return err(args[0], "endswith: expected string arguments");
-    }
-
-    std::string search = Resolver::get_string(*maybe_search);
-    std::string base = Resolver::get_string(*maybe_base);
-    return Resolver::scalar(search.ends_with(base));
-  }
-
-  Node count(const Nodes& args)
-  {
-    Node collection = args[0];
-    if (collection->type() == Term)
-    {
-      collection = collection->front();
-    }
-
-    if (
-      collection->type() == Object || collection->type() == Array ||
-      collection->type() == Set)
-    {
-      return Resolver::scalar(BigInt(collection->size()));
-    }
-
-    if (collection->type() == Scalar)
-    {
-      collection = collection->front();
-    }
-
-    if (collection->type() == JSONString)
-    {
-      std::string collection_str =
-        strip_quotes(std::string(collection->location().view()));
-      return Resolver::scalar(BigInt(collection_str.size()));
-    }
-
-    return err(args[0], "count: expected collection");
-  }
-
   Node to_number(const Nodes& args)
   {
     auto maybe_number = Resolver::maybe_unwrap_string(args[0]);
@@ -119,6 +61,19 @@ namespace rego
     return std::make_shared<BuiltInDef>(BuiltInDef{name, arity, behavior});
   }
 
+  BuiltIns::BuiltIns() noexcept : m_strict_errors(false) {}
+
+  bool BuiltIns::strict_errors() const
+  {
+    return m_strict_errors;
+  }
+
+  BuiltIns& BuiltIns::strict_errors(bool strict_errors)
+  {
+    m_strict_errors = strict_errors;
+    return *this;
+  }
+
   bool BuiltIns::is_builtin(const Location& name) const
   {
     return m_builtins.contains(name);
@@ -126,7 +81,31 @@ namespace rego
 
   Node BuiltIns::call(const Location& name, const Nodes& args) const
   {
-    return m_builtins.at(name)->behavior(args);
+    if (!is_builtin(name))
+    {
+      return err(args[0], "unknown builtin");
+    }
+
+    auto& builtin = m_builtins.at(name);
+    if (builtin->arity != AnyArity && builtin->arity != args.size())
+    {
+      return err(args[0], "wrong number of arguments");
+    }
+
+    Node result = builtin->behavior(args);
+    if (result->type() == Error)
+    {
+      if (m_strict_errors)
+      {
+        return result;
+      }
+      else
+      {
+        return Undefined;
+      }
+    }
+
+    return result;
   }
 
   BuiltIns& BuiltIns::register_builtin(const BuiltIn& built_in)
@@ -137,15 +116,15 @@ namespace rego
 
   BuiltIns& BuiltIns::register_standard_builtins()
   {
-    register_builtin(
-      BuiltInDef::create(Location("startswith"), 2, ::startswith));
-    register_builtin(BuiltInDef::create(Location("endswith"), 2, ::endswith));
-    register_builtin(BuiltInDef::create(Location("count"), 1, ::count));
     register_builtin(BuiltInDef::create(Location("to_number"), 1, ::to_number));
     register_builtin(BuiltInDef::create(Location("print"), AnyArity, ::print));
 
-    register_builtins(builtins::sets());
+    register_builtins(builtins::aggregates());
+    register_builtins(builtins::arrays());
+    register_builtins(builtins::encoding());
     register_builtins(builtins::numbers());
+    register_builtins(builtins::sets());
+    register_builtins(builtins::strings());
 
     return *this;
   }
diff --git a/src/builtins/aggregates.cc b/src/builtins/aggregates.cc
new file mode 100644
index 00000000..c847593d
--- /dev/null
+++ b/src/builtins/aggregates.cc
@@ -0,0 +1,178 @@
+#include "errors.h"
+#include "register.h"
+#include "resolver.h"
+#include "utf8.h"
+
+namespace
+{
+  using namespace rego;
+
+  Node count(const Nodes& args)
+  {
+    Node collection = args[0];
+    if (collection->type() == Term)
+    {
+      collection = collection->front();
+    }
+
+    if (
+      collection->type() == Object || collection->type() == Array ||
+      collection->type() == Set)
+    {
+      return Resolver::scalar(BigInt(collection->size()));
+    }
+
+    if (collection->type() == Scalar)
+    {
+      collection = collection->front();
+    }
+
+    if (collection->type() == JSONString)
+    {
+      std::string collection_str =
+        strip_quotes(std::string(collection->location().view()));
+      runestring collection_runes = utf8_to_runestring(collection_str);
+      return Resolver::scalar(BigInt(collection_runes.size()));
+    }
+
+    std::string collection_type = std::string(collection->type().str());
+    if (collection_type == "INT" || collection_type == "FLOAT")
+    {
+      collection_type = "number";
+    }
+    return err(
+      args[0],
+      "operand 1 must be one of {array, object, set, string} but got " +
+        collection_type,
+      EvalTypeError);
+  }
+
+  Node max(const Nodes& args)
+  {
+    Node collection = args[0];
+    if (collection->type() == Term)
+    {
+      collection = collection->front();
+    }
+
+    if (collection->type() == Array || collection->type() == Set)
+    {
+      auto it = std::max_element(
+        collection->begin(),
+        collection->end(),
+        [](const Node& a, const Node& b) { return to_json(a) < to_json(b); });
+
+      return *it;
+    }
+
+    return err(args[0], "max: expected collection");
+  }
+
+  Node min(const Nodes& args)
+  {
+    Node collection = args[0];
+    if (collection->type() == Term)
+    {
+      collection = collection->front();
+    }
+
+    if (collection->type() == Array || collection->type() == Set)
+    {
+      auto it = std::min_element(
+        collection->begin(),
+        collection->end(),
+        [](const Node& a, const Node& b) { return to_json(a) < to_json(b); });
+
+      return *it;
+    }
+
+    return err(args[0], "max: expected collection");
+  }
+
+  Node sort(const Nodes& args)
+  {
+    Node collection = args[0];
+    if (collection->type() == Term)
+    {
+      collection = collection->front();
+    }
+
+    if (collection->type() == Array || collection->type() == Set)
+    {
+      Node sorted = collection->clone();
+      std::sort(
+        sorted->begin(), sorted->end(), [](const Node& a, const Node& b) {
+          return to_json(a) < to_json(b);
+        });
+
+      return sorted;
+    }
+
+    return err(args[0], "max: expected collection");
+  }
+
+  Node sum(const Nodes& args)
+  {
+    Node collection = args[0];
+    if (collection->type() == Term)
+    {
+      collection = collection->front();
+    }
+
+    if (collection->type() == Array || collection->type() == Set)
+    {
+      Node sum = std::accumulate(
+        collection->begin(),
+        collection->end(),
+        JSONInt ^ "0",
+        [](const Node& a, const Node& b) {
+          return Resolver::arithinfix(Add ^ "+", a, b);
+        });
+
+      return Term << (Scalar << sum);
+    }
+
+    return err(args[0], "sum: expected collection");
+  }
+
+  Node product(const Nodes& args)
+  {
+    Node collection = args[0];
+    if (collection->type() == Term)
+    {
+      collection = collection->front();
+    }
+
+    if (collection->type() == Array || collection->type() == Set)
+    {
+      Node sum = std::accumulate(
+        collection->begin(),
+        collection->end(),
+        JSONInt ^ "1",
+        [](const Node& a, const Node& b) {
+          return Resolver::arithinfix(Multiply ^ "*", a, b);
+        });
+
+      return Term << (Scalar << sum);
+    }
+
+    return err(args[0], "sum: expected collection");
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> aggregates()
+    {
+      return std::vector<BuiltIn>{
+        BuiltInDef::create(Location("count"), 1, count),
+        BuiltInDef::create(Location("max"), 1, max),
+        BuiltInDef::create(Location("min"), 1, min),
+        BuiltInDef::create(Location("sort"), 1, sort),
+        BuiltInDef::create(Location("sum"), 1, sum),
+        BuiltInDef::create(Location("product"), 1, product)};
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/arrays.cc b/src/builtins/arrays.cc
new file mode 100644
index 00000000..9f609c63
--- /dev/null
+++ b/src/builtins/arrays.cc
@@ -0,0 +1,117 @@
+#include "errors.h"
+#include "register.h"
+#include "resolver.h"
+
+namespace
+{
+  using namespace rego;
+
+  Node concat(const Nodes& args)
+  {
+    Node x = Resolver::unwrap(
+      args[0], Array, "array.concat: operand 1 ", EvalTypeError);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    Node y = Resolver::unwrap(
+      args[1], Array, "array.concat: operand 2 ", EvalTypeError);
+    if (y->type() == Error)
+    {
+      return y;
+    }
+
+    Node z = x->clone();
+    y = y->clone();
+    z->insert(z->end(), y->begin(), y->end());
+    return z;
+  }
+
+  Node reverse(const Nodes& args)
+  {
+    Node arr = Resolver::unwrap(
+      args[0], Array, "array.reverse: operand 1 ", EvalTypeError);
+    if (arr->type() == Error)
+    {
+      return arr;
+    }
+
+    Node rev = NodeDef::create(Array);
+    if (arr->size() == 0)
+    {
+      return rev;
+    }
+
+    for (auto it = arr->rbegin(); it != arr->rend(); ++it)
+    {
+      Node node = *it;
+      rev->push_back(node->clone());
+    }
+
+    return rev;
+  }
+
+  Node slice(const Nodes& args)
+  {
+    Node arr = Resolver::unwrap(
+      args[0], Array, "array.slice: operand 1 ", EvalTypeError);
+    Node start_number = Resolver::unwrap(
+      args[1], JSONInt, "array.slice: operand 2 ", EvalTypeError);
+    Node end_number = Resolver::unwrap(
+      args[2], JSONInt, "array.slice: operand 3 ", EvalTypeError);
+
+    std::int64_t raw_start = BigInt(start_number->location()).to_int();
+    std::int64_t raw_end = BigInt(end_number->location()).to_int();
+    std::size_t start, end;
+    if (raw_start < 0)
+    {
+      start = 0;
+    }
+    else
+    {
+      start = std::min(static_cast<std::size_t>(raw_start), arr->size());
+    }
+
+    if (raw_end < 0)
+    {
+      end = 0;
+    }
+    else
+    {
+      end = std::min(static_cast<std::size_t>(raw_end), arr->size());
+    }
+
+    Node array = NodeDef::create(Array);
+
+    if (start >= end)
+    {
+      return array;
+    }
+
+    auto start_it = arr->begin() + start;
+    auto end_it = arr->begin() + end;
+    for (auto it = start_it; it != end_it; ++it)
+    {
+      Node node = *it;
+      array->push_back(node->clone());
+    }
+
+    return array;
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> arrays()
+    {
+      return {
+        BuiltInDef::create(Location("array.concat"), 2, concat),
+        BuiltInDef::create(Location("array.reverse"), 1, reverse),
+        BuiltInDef::create(Location("array.slice"), 3, slice),
+      };
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/base64/base64.cpp b/src/builtins/base64/base64.cpp
new file mode 100644
index 00000000..ced57d2b
--- /dev/null
+++ b/src/builtins/base64/base64.cpp
@@ -0,0 +1,282 @@
+/*
+   base64.cpp and base64.h
+
+   base64 encoding and decoding with C++.
+   More information at
+     https://renenyffenegger.ch/notes/development/Base64/Encoding-and-decoding-base-64-with-cpp
+
+   Version: 2.rc.09 (release candidate)
+
+   Copyright (C) 2004-2017, 2020-2022 René Nyffenegger
+
+   This source code is provided 'as-is', without any express or implied
+   warranty. In no event will the author be held liable for any damages
+   arising from the use of this software.
+
+   Permission is granted to anyone to use this software for any purpose,
+   including commercial applications, and to alter it and redistribute it
+   freely, subject to the following restrictions:
+
+   1. The origin of this source code must not be misrepresented; you must not
+      claim that you wrote the original source code. If you use this source code
+      in a product, an acknowledgment in the product documentation would be
+      appreciated but is not required.
+
+   2. Altered source versions must be plainly marked as such, and must not be
+      misrepresented as being the original source code.
+
+   3. This notice may not be removed or altered from any source distribution.
+
+   René Nyffenegger rene.nyffenegger@adp-gmbh.ch
+
+*/
+
+#include "base64.h"
+
+#include <algorithm>
+#include <stdexcept>
+
+ //
+ // Depending on the url parameter in base64_chars, one of
+ // two sets of base64 characters needs to be chosen.
+ // They differ in their last two characters.
+ //
+static const char* base64_chars[2] = {
+             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+             "abcdefghijklmnopqrstuvwxyz"
+             "0123456789"
+             "+/",
+
+             "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+             "abcdefghijklmnopqrstuvwxyz"
+             "0123456789"
+             "-_"};
+
+static unsigned int pos_of_char(const unsigned char chr) {
+ //
+ // Return the position of chr within base64_encode()
+ //
+
+    if      (chr >= 'A' && chr <= 'Z') return chr - 'A';
+    else if (chr >= 'a' && chr <= 'z') return chr - 'a' + ('Z' - 'A')               + 1;
+    else if (chr >= '0' && chr <= '9') return chr - '0' + ('Z' - 'A') + ('z' - 'a') + 2;
+    else if (chr == '+' || chr == '-') return 62; // Be liberal with input and accept both url ('-') and non-url ('+') base 64 characters (
+    else if (chr == '/' || chr == '_') return 63; // Ditto for '/' and '_'
+    else
+ //
+ // 2020-10-23: Throw std::exception rather than const char*
+ //(Pablo Martin-Gomez, https://github.com/Bouska)
+ //
+    throw std::runtime_error("Input is not valid base64-encoded data.");
+}
+
+static std::string insert_linebreaks(std::string str, size_t distance) {
+ //
+ // Provided by https://github.com/JomaCorpFX, adapted by me.
+ //
+    if (!str.length()) {
+        return "";
+    }
+
+    size_t pos = distance;
+
+    while (pos < str.size()) {
+        str.insert(pos, "\n");
+        pos += distance + 1;
+    }
+
+    return str;
+}
+
+template <typename String, unsigned int line_length>
+static std::string encode_with_line_breaks(String s) {
+  return insert_linebreaks(base64_encode(s, false), line_length);
+}
+
+template <typename String>
+static std::string encode_pem(String s) {
+  return encode_with_line_breaks<String, 64>(s);
+}
+
+template <typename String>
+static std::string encode_mime(String s) {
+  return encode_with_line_breaks<String, 76>(s);
+}
+
+template <typename String>
+static std::string encode(String s, bool url) {
+  return base64_encode(reinterpret_cast<const unsigned char*>(s.data()), s.length(), url);
+}
+
+std::string base64_encode(unsigned char const* bytes_to_encode, size_t in_len, bool url) {
+
+    size_t len_encoded = (in_len +2) / 3 * 4;
+
+    unsigned char trailing_char = url ? '.' : '=';
+
+ //
+ // Choose set of base64 characters. They differ
+ // for the last two positions, depending on the url
+ // parameter.
+ // A bool (as is the parameter url) is guaranteed
+ // to evaluate to either 0 or 1 in C++ therefore,
+ // the correct character set is chosen by subscripting
+ // base64_chars with url.
+ //
+    const char* base64_chars_ = base64_chars[url];
+
+    std::string ret;
+    ret.reserve(len_encoded);
+
+    unsigned int pos = 0;
+
+    while (pos < in_len) {
+        ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0xfc) >> 2]);
+
+        if (pos+1 < in_len) {
+           ret.push_back(base64_chars_[((bytes_to_encode[pos + 0] & 0x03) << 4) + ((bytes_to_encode[pos + 1] & 0xf0) >> 4)]);
+
+           if (pos+2 < in_len) {
+              ret.push_back(base64_chars_[((bytes_to_encode[pos + 1] & 0x0f) << 2) + ((bytes_to_encode[pos + 2] & 0xc0) >> 6)]);
+              ret.push_back(base64_chars_[  bytes_to_encode[pos + 2] & 0x3f]);
+           }
+           else {
+              ret.push_back(base64_chars_[(bytes_to_encode[pos + 1] & 0x0f) << 2]);
+              ret.push_back(trailing_char);
+           }
+        }
+        else {
+
+            ret.push_back(base64_chars_[(bytes_to_encode[pos + 0] & 0x03) << 4]);
+            ret.push_back(trailing_char);
+            ret.push_back(trailing_char);
+        }
+
+        pos += 3;
+    }
+
+
+    return ret;
+}
+
+template <typename String>
+static std::string decode(String const& encoded_string, bool remove_linebreaks) {
+ //
+ // decode(…) is templated so that it can be used with String = const std::string&
+ // or std::string_view (requires at least C++17)
+ //
+
+    if (encoded_string.empty()) return std::string();
+
+    if (remove_linebreaks) {
+
+       std::string copy(encoded_string);
+
+       copy.erase(std::remove(copy.begin(), copy.end(), '\n'), copy.end());
+
+       return base64_decode(copy, false);
+    }
+
+    size_t length_of_string = encoded_string.length();
+    size_t pos = 0;
+
+ //
+ // The approximate length (bytes) of the decoded string might be one or
+ // two bytes smaller, depending on the amount of trailing equal signs
+ // in the encoded string. This approximation is needed to reserve
+ // enough space in the string to be returned.
+ //
+    size_t approx_length_of_decoded_string = length_of_string / 4 * 3;
+    std::string ret;
+    ret.reserve(approx_length_of_decoded_string);
+
+    while (pos < length_of_string) {
+    //
+    // Iterate over encoded input string in chunks. The size of all
+    // chunks except the last one is 4 bytes.
+    //
+    // The last chunk might be padded with equal signs or dots
+    // in order to make it 4 bytes in size as well, but this
+    // is not required as per RFC 2045.
+    //
+    // All chunks except the last one produce three output bytes.
+    //
+    // The last chunk produces at least one and up to three bytes.
+    //
+
+       size_t pos_of_char_1 = pos_of_char(encoded_string.at(pos+1) );
+
+    //
+    // Emit the first output byte that is produced in each chunk:
+    //
+       ret.push_back(static_cast<std::string::value_type>( ( (pos_of_char(encoded_string.at(pos+0)) ) << 2 ) + ( (pos_of_char_1 & 0x30 ) >> 4)));
+
+       if ( ( pos + 2 < length_of_string  )       &&  // Check for data that is not padded with equal signs (which is allowed by RFC 2045)
+              encoded_string.at(pos+2) != '='     &&
+              encoded_string.at(pos+2) != '.'         // accept URL-safe base 64 strings, too, so check for '.' also.
+          )
+       {
+       //
+       // Emit a chunk's second byte (which might not be produced in the last chunk).
+       //
+          unsigned int pos_of_char_2 = pos_of_char(encoded_string.at(pos+2) );
+          ret.push_back(static_cast<std::string::value_type>( (( pos_of_char_1 & 0x0f) << 4) + (( pos_of_char_2 & 0x3c) >> 2)));
+
+          if ( ( pos + 3 < length_of_string )     &&
+                 encoded_string.at(pos+3) != '='  &&
+                 encoded_string.at(pos+3) != '.'
+             )
+          {
+          //
+          // Emit a chunk's third byte (which might not be produced in the last chunk).
+          //
+             ret.push_back(static_cast<std::string::value_type>( ( (pos_of_char_2 & 0x03 ) << 6 ) + pos_of_char(encoded_string.at(pos+3))   ));
+          }
+       }
+
+       pos += 4;
+    }
+
+    return ret;
+}
+
+std::string base64_decode(std::string const& s, bool remove_linebreaks) {
+   return decode(s, remove_linebreaks);
+}
+
+std::string base64_encode(std::string const& s, bool url) {
+   return encode(s, url);
+}
+
+std::string base64_encode_pem (std::string const& s) {
+   return encode_pem(s);
+}
+
+std::string base64_encode_mime(std::string const& s) {
+   return encode_mime(s);
+}
+
+#if __cplusplus >= 201703L
+//
+// Interface with std::string_view rather than const std::string&
+// Requires C++17
+// Provided by Yannic Bonenberger (https://github.com/Yannic)
+//
+
+std::string base64_encode(std::string_view s, bool url) {
+   return encode(s, url);
+}
+
+std::string base64_encode_pem(std::string_view s) {
+   return encode_pem(s);
+}
+
+std::string base64_encode_mime(std::string_view s) {
+   return encode_mime(s);
+}
+
+std::string base64_decode(std::string_view s, bool remove_linebreaks) {
+   return decode(s, remove_linebreaks);
+}
+
+#endif  // __cplusplus >= 201703L
\ No newline at end of file
diff --git a/src/builtins/base64/base64.h b/src/builtins/base64/base64.h
new file mode 100644
index 00000000..7d040213
--- /dev/null
+++ b/src/builtins/base64/base64.h
@@ -0,0 +1,35 @@
+//
+//  base64 encoding and decoding with C++.
+//  Version: 2.rc.09 (release candidate)
+//
+
+#ifndef BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A
+#define BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A
+
+#include <string>
+
+#if __cplusplus >= 201703L
+#  include <string_view>
+#endif // __cplusplus >= 201703L
+
+std::string base64_encode(std::string const& s, bool url = false);
+std::string base64_encode_pem(std::string const& s);
+std::string base64_encode_mime(std::string const& s);
+
+std::string base64_decode(std::string const& s, bool remove_linebreaks = false);
+std::string base64_encode(unsigned char const*, size_t len, bool url = false);
+
+#if __cplusplus >= 201703L
+//
+// Interface with std::string_view rather than const std::string&
+// Requires C++17
+// Provided by Yannic Bonenberger (https://github.com/Yannic)
+//
+std::string base64_encode(std::string_view s, bool url = false);
+std::string base64_encode_pem(std::string_view s);
+std::string base64_encode_mime(std::string_view s);
+
+std::string base64_decode(std::string_view s, bool remove_linebreaks = false);
+#endif // __cplusplus >= 201703L
+
+#endif /* BASE64_H_C0CE2A47_D10E_42C9_A27C_C883944E704A */
\ No newline at end of file
diff --git a/src/builtins/encoding.cc b/src/builtins/encoding.cc
new file mode 100644
index 00000000..3c99ca0e
--- /dev/null
+++ b/src/builtins/encoding.cc
@@ -0,0 +1,49 @@
+#include "base64/base64.h"
+#include "register.h"
+#include "resolver.h"
+
+namespace
+{
+  using namespace rego;
+
+  Node base64_encode_(const Nodes& args)
+  {
+    auto maybe_x_string = Resolver::maybe_unwrap_string(args[0]);
+    if (!maybe_x_string.has_value())
+    {
+      return err(args[0], "Not a string");
+    }
+
+    std::string x_string =
+      std::string(maybe_x_string.value()->location().view());
+    std::string encoded = ::base64_encode(x_string);
+    return JSONString ^ encoded;
+  }
+
+  Node base64_decode_(const Nodes& args)
+  {
+    auto maybe_x_string = Resolver::maybe_unwrap_string(args[0]);
+    if (!maybe_x_string.has_value())
+    {
+      return err(args[0], "Not a string");
+    }
+
+    std::string x_string =
+      strip_quotes(std::string(maybe_x_string.value()->location().view()));
+    std::string decoded = ::base64_decode(x_string);
+    return JSONString ^ decoded;
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> encoding()
+    {
+      return {
+        BuiltInDef::create(Location("base64.encode"), 1, base64_encode_),
+        BuiltInDef::create(Location("base64.decode"), 1, base64_decode_)};
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/numbers.cc b/src/builtins/numbers.cc
index 0d8319a5..31bd36ad 100644
--- a/src/builtins/numbers.cc
+++ b/src/builtins/numbers.cc
@@ -1,3 +1,4 @@
+#include "errors.h"
 #include "register.h"
 #include "resolver.h"
 
@@ -99,6 +100,19 @@ namespace
 
   Node numbers_range(const Nodes& args)
   {
+    Node lhs_number = Resolver::unwrap(
+      args[0], JSONInt, "numbers.range: operand 1 ", EvalTypeError, true);
+    if (lhs_number->type() == Error)
+    {
+      return lhs_number;
+    }
+
+    Node rhs_number = Resolver::unwrap(
+      args[1], JSONInt, "numbers.range: operand 2 ", EvalTypeError, true);
+    if (rhs_number->type() == Error)
+    {
+      return rhs_number;
+    }
     auto maybe_lhs_number = Resolver::maybe_unwrap_number(args[0]);
     if (!maybe_lhs_number.has_value())
     {
@@ -110,26 +124,6 @@ namespace
       return err(args[1], "Not a number");
     }
 
-    Node lhs_number = maybe_lhs_number.value();
-    Node rhs_number = maybe_rhs_number.value();
-    if (lhs_number->type() != JSONInt)
-    {
-      return err(
-        args[0],
-        "numbers.range: operand 1 must be integer number but got "
-        "floating-point number",
-        "eval_type_error");
-    }
-
-    if (rhs_number->type() != JSONInt)
-    {
-      return err(
-        args[1],
-        "numbers.range: operand 2 must be integer number but got "
-        "floating-point number",
-        "eval_type_error");
-    }
-
     BigInt lhs = Resolver::get_int(lhs_number);
     BigInt rhs = Resolver::get_int(rhs_number);
     Node array = Array ^ args[0];
diff --git a/src/builtins/register.h b/src/builtins/register.h
index b3b33f45..e1b271b2 100644
--- a/src/builtins/register.h
+++ b/src/builtins/register.h
@@ -6,7 +6,11 @@ namespace rego
 {
   namespace builtins
   {
+    std::vector<BuiltIn> aggregates();
+    std::vector<BuiltIn> arrays();
+    std::vector<BuiltIn> encoding();
     std::vector<BuiltIn> numbers();
     std::vector<BuiltIn> sets();
+    std::vector<BuiltIn> strings();
   }
 }
\ No newline at end of file
diff --git a/src/builtins/sets.cc b/src/builtins/sets.cc
index a59db013..17be640c 100644
--- a/src/builtins/sets.cc
+++ b/src/builtins/sets.cc
@@ -1,3 +1,4 @@
+#include "errors.h"
 #include "register.h"
 #include "resolver.h"
 
@@ -7,12 +8,63 @@ namespace
 
   Node intersection(const Nodes& args)
   {
-    return Resolver::set_intersection(args[0], args[1]);
+    Node xs =
+      Resolver::unwrap(args[0], Set, "intersection: operand 1 ", EvalTypeError);
+    if (xs->type() == Error)
+    {
+      return xs;
+    }
+
+    if (xs->size() == 0)
+    {
+      return NodeDef::create(Set);
+    }
+
+    Node y = Resolver::unwrap(
+      xs->at(0), Set, "intersection: operand 1 is set of sets ", EvalTypeError);
+    if (y->type() == Error)
+    {
+      return y;
+    }
+    for (std::size_t i = 1; i < xs->size(); i++)
+    {
+      Node x = Resolver::unwrap(
+        xs->at(i),
+        Set,
+        "intersection: operand 1 is set of sets ",
+        EvalTypeError);
+      if (x->type() == Error)
+      {
+        return x;
+      }
+      y = Resolver::set_intersection(y, x);
+    }
+
+    return y;
   }
 
   Node union_(const Nodes& args)
   {
-    return Resolver::set_union(args[0], args[1]);
+    Node xs =
+      Resolver::unwrap(args[0], Set, "union: operand 1 ", EvalTypeError);
+    if (xs->type() == Error)
+    {
+      return xs;
+    }
+
+    Node y = NodeDef::create(Set);
+    for (Node x : *xs)
+    {
+      x = Resolver::unwrap(
+        x, Set, "union: operand 1 is set of sets ", EvalTypeError);
+      if (x->type() == Error)
+      {
+        return x;
+      }
+      y = Resolver::set_union(y, x);
+    }
+
+    return y;
   }
 
   Node difference(const Nodes& args)
@@ -28,8 +80,8 @@ namespace rego
     std::vector<BuiltIn> sets()
     {
       return {
-        BuiltInDef::create(Location("intersection"), 2, intersection),
-        BuiltInDef::create(Location("union"), 2, union_),
+        BuiltInDef::create(Location("intersection"), 1, intersection),
+        BuiltInDef::create(Location("union"), 1, union_),
         BuiltInDef::create(Location("set_diff"), 2, difference)};
     }
   }
diff --git a/src/builtins/strings.cc b/src/builtins/strings.cc
new file mode 100644
index 00000000..04760e24
--- /dev/null
+++ b/src/builtins/strings.cc
@@ -0,0 +1,1019 @@
+#include "errors.h"
+#include "register.h"
+#include "resolver.h"
+#include "utf8.h"
+
+#include <bitset>
+
+namespace
+{
+  using namespace rego;
+
+  Node unwrap_strings(const Node& collection, std::vector<std::string>& items)
+  {
+    for (auto term : *collection)
+    {
+      auto maybe_item = Resolver::maybe_unwrap_string(term);
+      if (!maybe_item.has_value())
+      {
+        return term;
+      }
+      items.push_back(Resolver::get_string(*maybe_item));
+    }
+
+    return {};
+  }
+
+  Node concat(const Nodes& args)
+  {
+    Node delimiter = Resolver::unwrap(
+      args[0], JSONString, "concat: operand 1 ", EvalTypeError);
+    if (delimiter->type() == Error)
+    {
+      return delimiter;
+    }
+
+    auto maybe_collection = Resolver::maybe_unwrap(args[1], {Array, Set});
+    if (!maybe_collection.has_value())
+    {
+      return Undefined;
+    }
+
+    Node collection = *maybe_collection;
+
+    auto delim_str = Resolver::get_string(delimiter);
+    std::vector<std::string> items;
+    Node bad_term = unwrap_strings(collection, items);
+    if (bad_term)
+    {
+      return err(
+        bad_term,
+        "concat: operand 2 must be array of strings but got array containing " +
+          Resolver::type_name(bad_term),
+        EvalTypeError);
+    }
+
+    std::ostringstream result_str;
+    std::string sep = "";
+    for (auto& item : items)
+    {
+      result_str << sep << item;
+      sep = delim_str;
+    }
+
+    return Resolver::scalar(result_str.str());
+  }
+
+  Node startswith(const Nodes& args)
+  {
+    auto maybe_search = Resolver::maybe_unwrap_string(args[0]);
+    auto maybe_base = Resolver::maybe_unwrap_string(args[1]);
+    if (!maybe_search.has_value() || !maybe_base.has_value())
+    {
+      return err(
+        args[0], "startswith: expected string arguments", EvalTypeError);
+    }
+
+    std::string search = Resolver::get_string(*maybe_search);
+    std::string base = Resolver::get_string(*maybe_base);
+    return Resolver::scalar(search.starts_with(base));
+  }
+
+  Node endswith(const Nodes& args)
+  {
+    auto maybe_search = Resolver::maybe_unwrap_string(args[0]);
+    auto maybe_base = Resolver::maybe_unwrap_string(args[1]);
+    if (!maybe_search.has_value() || !maybe_base.has_value())
+    {
+      return err(args[0], "endswith: expected string arguments", EvalTypeError);
+    }
+
+    std::string search = Resolver::get_string(*maybe_search);
+    std::string base = Resolver::get_string(*maybe_base);
+    return Resolver::scalar(search.ends_with(base));
+  }
+
+  Node contains(const Nodes& args)
+  {
+    Node haystack = Resolver::unwrap(
+      args[0], JSONString, "contains: operand 1 ", EvalTypeError);
+    if (haystack->type() == Error)
+    {
+      return haystack;
+    }
+
+    Node needle = Resolver::unwrap(
+      args[1], JSONString, "contains: operand 2 ", EvalTypeError);
+    if (needle->type() == Error)
+    {
+      return needle;
+    }
+
+    std::string haystack_str = Resolver::get_string(haystack);
+    std::string needle_str = Resolver::get_string(needle);
+    return Resolver::scalar(haystack_str.find(needle_str) != std::string::npos);
+  }
+
+  Node format_int(const Nodes& args)
+  {
+    auto maybe_number = Resolver::maybe_unwrap_number(args[0]);
+    if (!maybe_number.has_value())
+    {
+      return err(
+        args[0], "format_int: expected number argument", EvalTypeError);
+    }
+
+    auto base = Resolver::unwrap(
+      args[1], JSONInt, "format_int: operand 2 ", EvalTypeError);
+    if (base->type() == Error)
+    {
+      return base;
+    }
+
+    Node number = *maybe_number;
+    std::int64_t value;
+    if (number->type() == JSONFloat)
+    {
+      value =
+        static_cast<std::int64_t>(std::floor(Resolver::get_double(number)));
+    }
+    else
+    {
+      value = Resolver::get_int(number).to_int();
+    }
+
+    std::ostringstream result;
+    std::size_t base_size = Resolver::get_int(base).to_size();
+    switch (base_size)
+    {
+      case 2:
+        result << std::bitset<2>(value);
+        break;
+      case 8:
+        result << std::oct << value;
+        break;
+      case 10:
+        result << value;
+        break;
+      case 16:
+        result << std::hex << value;
+        break;
+      default:
+        return err(
+          args[1], "operand 2 must be one of {2, 8, 10, 16}", EvalTypeError);
+    }
+
+    return Resolver::scalar(result.str());
+  }
+
+  Node indexof(const Nodes& args)
+  {
+    Node haystack = Resolver::unwrap(
+      args[0], JSONString, "indexof: operand 1 ", EvalTypeError);
+    if (haystack->type() == Error)
+    {
+      return haystack;
+    }
+
+    Node needle = Resolver::unwrap(
+      args[1], JSONString, "indexof: operand 2 ", EvalTypeError);
+    if (needle->type() == Error)
+    {
+      return needle;
+    }
+
+    runestring haystack_runes =
+      utf8_to_runestring(Resolver::get_string(haystack));
+    runestring needle_runes = utf8_to_runestring(Resolver::get_string(needle));
+    auto pos = haystack_runes.find(needle_runes);
+    if (pos == haystack_runes.npos)
+    {
+      return JSONInt ^ "-1";
+    }
+
+    return JSONInt ^ std::to_string(pos);
+  }
+
+  Node indexof_n(const Nodes& args)
+  {
+    Node haystack = Resolver::unwrap(
+      args[0], JSONString, "indexof: operand 1 ", EvalTypeError);
+    if (haystack->type() == Error)
+    {
+      return haystack;
+    }
+
+    Node needle = Resolver::unwrap(
+      args[1], JSONString, "indexof: operand 2 ", EvalTypeError);
+    if (needle->type() == Error)
+    {
+      return needle;
+    }
+
+    runestring haystack_runes =
+      utf8_to_runestring(Resolver::get_string(haystack));
+    runestring needle_runes = utf8_to_runestring(Resolver::get_string(needle));
+    Node array = NodeDef::create(Array);
+    auto pos = haystack_runes.find(needle_runes);
+    while (pos != haystack_runes.npos)
+    {
+      array->push_back(JSONInt ^ std::to_string(pos));
+      pos = haystack_runes.find(needle_runes, pos + 1);
+    }
+
+    return array;
+  }
+
+  Node lower(const Nodes& args)
+  {
+    Node x =
+      Resolver::unwrap(args[0], JSONString, "lower: operand 1 ", EvalTypeError);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_str = Resolver::get_string(x);
+    std::transform(
+      x_str.begin(), x_str.end(), x_str.begin(), [](const char& c) {
+        return static_cast<char>(std::tolower(c));
+      });
+    return Resolver::scalar(x_str);
+  }
+
+  Node upper(const Nodes& args)
+  {
+    Node x =
+      Resolver::unwrap(args[0], JSONString, "upper: operand 1 ", EvalTypeError);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_str = Resolver::get_string(x);
+    std::transform(
+      x_str.begin(), x_str.end(), x_str.begin(), [](const char& c) {
+        return static_cast<char>(std::toupper(c));
+      });
+    return Resolver::scalar(x_str);
+  }
+
+  void replace(std::string& x, const std::string& old, const std::string& new_)
+  {
+    auto pos = x.find(old);
+    while (pos != x.npos)
+    {
+      x.replace(pos, old.size(), new_);
+      pos = x.find(old, pos + new_.size());
+    }
+  }
+
+  Node replace(const Nodes& args)
+  {
+    Node x = Resolver::unwrap(
+      args[0], JSONString, "replace: operand 1 ", EvalTypeError);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    Node old = Resolver::unwrap(
+      args[1], JSONString, "replace: operand 2 ", EvalTypeError);
+    if (old->type() == Error)
+    {
+      return old;
+    }
+
+    Node new_ = Resolver::unwrap(
+      args[2], JSONString, "replace: operand 3 ", EvalTypeError);
+    if (new_->type() == Error)
+    {
+      return new_;
+    }
+
+    std::string x_str = Resolver::get_string(x);
+    std::string old_str = Resolver::get_string(old);
+    std::string new_str = Resolver::get_string(new_);
+    replace(x_str, old_str, new_str);
+
+    return Resolver::scalar(x_str);
+  }
+
+  Node split(const Nodes& args)
+  {
+    Node x =
+      Resolver::unwrap(args[0], JSONString, "split: operand 1 ", EvalTypeError);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    Node delimiter =
+      Resolver::unwrap(args[1], JSONString, "split: operand 2 ", EvalTypeError);
+    if (delimiter->type() == Error)
+    {
+      return delimiter;
+    }
+
+    std::string x_str = Resolver::get_string(x);
+    std::string delimiter_str = Resolver::get_string(delimiter);
+    Node array = NodeDef::create(Array);
+    std::size_t start = 0;
+    std::size_t pos = x_str.find(delimiter_str);
+    while (pos != x_str.npos)
+    {
+      array->push_back(JSONString ^ x_str.substr(start, pos - start));
+      start = pos + delimiter_str.size();
+      pos = x_str.find(delimiter_str, start);
+    }
+
+    array->push_back(JSONString ^ x_str.substr(start));
+    return array;
+  }
+
+  enum class PrintVerbType
+  {
+    Literal,
+    Value,
+    Boolean,
+    Binary,
+    Integer,
+    Double,
+    String,
+  };
+
+  struct PrintVerb
+  {
+    PrintVerbType type;
+    std::string format;
+  };
+
+  std::vector<PrintVerb> parse_format(const std::string& format)
+  {
+    std::vector<PrintVerb> verbs;
+    std::size_t start = 0;
+    std::size_t pos = format.find('%');
+    while (pos < format.size())
+    {
+      verbs.push_back(
+        {PrintVerbType::Literal, format.substr(start, pos - start)});
+      std::size_t verb_start = pos;
+      bool in_verb = true;
+      while (pos < format.size() && in_verb)
+      {
+        ++pos;
+        in_verb = false;
+        switch (format[pos])
+        {
+          case 't':
+            verbs.push_back(
+              {PrintVerbType::Boolean,
+               format.substr(verb_start, pos - verb_start + 1)});
+            break;
+
+          case 'b':
+            verbs.push_back(
+              {PrintVerbType::Binary,
+               format.substr(verb_start, pos - verb_start + 1)});
+            break;
+
+          case 'd':
+          case 'x':
+          case 'X':
+          case 'o':
+          case 'O':
+            verbs.push_back(
+              {PrintVerbType::Integer,
+               format.substr(verb_start, pos - verb_start + 1)});
+            break;
+
+          case 'e':
+          case 'E':
+          case 'f':
+          case 'F':
+          case 'g':
+          case 'G':
+            verbs.push_back(
+              {PrintVerbType::Double,
+               format.substr(verb_start, pos - verb_start + 1)});
+            break;
+
+          case 's':
+            verbs.push_back(
+              {PrintVerbType::String,
+               format.substr(verb_start, pos - verb_start + 1)});
+            break;
+
+          case '%':
+            verbs.push_back({PrintVerbType::Literal, "%"});
+            break;
+
+          case 'v':
+            verbs.push_back(
+              {PrintVerbType::Value,
+               format.substr(verb_start, pos - verb_start + 1)});
+            break;
+
+          default:
+            in_verb = true;
+        }
+      }
+      start = pos + 1;
+      pos = format.find('%', start);
+    }
+
+    if (start < format.size())
+    {
+      verbs.push_back({PrintVerbType::Literal, format.substr(start)});
+    }
+
+    return verbs;
+  }
+
+  Node sprintf(const Nodes& args)
+  {
+    Node format = Resolver::unwrap(
+      args[0], JSONString, "sprintf: operand 1 ", EvalTypeError);
+    if (format->type() == Error)
+    {
+      return format;
+    }
+
+    Node values =
+      Resolver::unwrap(args[1], Array, "sprintf: operand 2 ", EvalTypeError);
+    if (values->type() == Error)
+    {
+      return values;
+    }
+
+    std::string format_str = Resolver::get_string(format);
+    std::vector<PrintVerb> verbs = parse_format(format_str);
+    std::ostringstream result;
+    auto it = values->begin();
+    for (auto& verb : verbs)
+    {
+      if (verb.type == PrintVerbType::Literal)
+      {
+        result << verb.format;
+        continue;
+      }
+
+      if (it == values->end())
+      {
+        return err(args[1], "sprintf: not enough arguments", EvalTypeError);
+      }
+
+      const int buf_size = 1024;
+      char buf[buf_size];
+      Node node = *it;
+      Node error;
+      ++it;
+      switch (verb.type)
+      {
+        case PrintVerbType::Value:
+          result << to_json(node);
+          break;
+
+        case PrintVerbType::Boolean:
+          result << std::boolalpha << Resolver::is_truthy(node);
+          break;
+
+        case PrintVerbType::Binary:
+          result << std::bitset<8>(Resolver::get_int(values).to_int());
+          break;
+
+        case PrintVerbType::Integer:
+          node = Resolver::unwrap(
+            node, JSONInt, "sprintf: integer argument ", EvalTypeError);
+          if (node->type() == Error)
+          {
+            return node;
+          }
+          std::snprintf(
+            buf,
+            buf_size,
+            verb.format.c_str(),
+            Resolver::get_int(node).to_int());
+          result << buf;
+          break;
+
+        case PrintVerbType::Double:
+          node = Resolver::unwrap(
+            node,
+            JSONFloat,
+            "sprintf: floating-point argument ",
+            EvalTypeError);
+          if (node->type() == Error)
+          {
+            return node;
+          }
+          std::snprintf(
+            buf, buf_size, verb.format.c_str(), Resolver::get_double(node));
+          result << buf;
+          break;
+
+        case PrintVerbType::String:
+          result << Resolver::get_string(node->front());
+          break;
+
+        case PrintVerbType::Literal:
+          // Should never happen
+          break;
+      }
+    }
+
+    return JSONString ^ result.str();
+  }
+
+  Node any_prefix_match(const Nodes& args)
+  {
+    auto maybe_search =
+      Resolver::maybe_unwrap(args[0], {JSONString, Array, Set});
+    if (!maybe_search.has_value())
+    {
+      std::string type_name = Resolver::type_name(args[0]);
+      return err(
+        args[0],
+        "strings.any_prefix_match: operand 1 must be one of {string, set, "
+        "array} but got " +
+          type_name,
+        EvalTypeError);
+    }
+
+    auto maybe_base = Resolver::maybe_unwrap(args[1], {JSONString, Array, Set});
+    if (!maybe_base.has_value())
+    {
+      std::string type_name = Resolver::type_name(args[1]);
+      return err(
+        args[1],
+        "strings.any_prefix_match: operand 2 must be one of {string, set, "
+        "array} but got " +
+          type_name,
+        EvalTypeError);
+    }
+
+    Node search = *maybe_search;
+    std::vector<std::string> search_strings;
+    if (search->type() == JSONString)
+    {
+      search_strings.push_back(Resolver::get_string(search));
+    }
+    else
+    {
+      Node bad_term = unwrap_strings(search, search_strings);
+      if (bad_term)
+      {
+        return err(
+          bad_term,
+          "strings.any_prefix_match: operand 1 must be array of strings but "
+          "got array containing " +
+            Resolver::type_name(bad_term),
+          EvalTypeError);
+      }
+    }
+
+    Node base = *maybe_base;
+    std::vector<std::string> base_strings;
+    if (base->type() == JSONString)
+    {
+      base_strings.push_back(Resolver::get_string(base));
+    }
+    else
+    {
+      Node bad_term = unwrap_strings(base, base_strings);
+      if (bad_term)
+      {
+        return err(
+          bad_term,
+          "strings.any_prefix_match: operand 2 must be array of strings but "
+          "got array containing " +
+            Resolver::type_name(bad_term),
+          EvalTypeError);
+      }
+    }
+
+    for (auto& search_str : search_strings)
+    {
+      for (auto& base_str : base_strings)
+      {
+        if (search_str.starts_with(base_str))
+        {
+          return JSONTrue ^ "true";
+        }
+      }
+    }
+
+    return JSONFalse ^ "false";
+  }
+
+  Node any_suffix_match(const Nodes& args)
+  {
+    auto maybe_search =
+      Resolver::maybe_unwrap(args[0], {JSONString, Array, Set});
+    if (!maybe_search.has_value())
+    {
+      std::string type_name = Resolver::type_name(args[0]);
+      return err(
+        args[0],
+        "strings.any_suffix_match: operand 1 must be one of {string, set, "
+        "array} but got " +
+          type_name,
+        EvalTypeError);
+    }
+
+    auto maybe_base = Resolver::maybe_unwrap(args[1], {JSONString, Array, Set});
+    if (!maybe_base.has_value())
+    {
+      std::string type_name = Resolver::type_name(args[1]);
+      return err(
+        args[1],
+        "strings.any_suffix_match: operand 2 must be one of {string, set, "
+        "array} but got " +
+          type_name,
+        EvalTypeError);
+    }
+
+    Node search = *maybe_search;
+    std::vector<std::string> search_strings;
+    if (search->type() == JSONString)
+    {
+      search_strings.push_back(Resolver::get_string(search));
+    }
+    else
+    {
+      Node bad_term = unwrap_strings(search, search_strings);
+      if (bad_term)
+      {
+        return err(
+          bad_term,
+          "strings.any_suffix_match: operand 1 must be array of strings but "
+          "got array containing " +
+            Resolver::type_name(bad_term),
+          EvalTypeError);
+      }
+    }
+
+    Node base = *maybe_base;
+    std::vector<std::string> base_strings;
+    if (base->type() == JSONString)
+    {
+      base_strings.push_back(Resolver::get_string(base));
+    }
+    else
+    {
+      Node bad_term = unwrap_strings(base, base_strings);
+      if (bad_term)
+      {
+        return err(
+          bad_term,
+          "strings.any_suffix_match: operand 2 must be array of strings but "
+          "got array containing " +
+            Resolver::type_name(bad_term),
+          EvalTypeError);
+      }
+    }
+
+    for (auto& search_str : search_strings)
+    {
+      for (auto& base_str : base_strings)
+      {
+        if (search_str.ends_with(base_str))
+        {
+          return JSONTrue ^ "true";
+        }
+      }
+    }
+
+    return JSONFalse ^ "false";
+  }
+
+  Node replace_n(const Nodes& args)
+  {
+    Node patterns = Resolver::unwrap(
+      args[0], Object, "strings.replace_n: operand 1 ", EvalTypeError);
+    if (patterns->type() == Error)
+    {
+      return patterns;
+    }
+
+    Node value = Resolver::unwrap(
+      args[1], JSONString, "strings.replace_n: operand 2 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+
+    std::string value_str = Resolver::get_string(value);
+    for (auto& item : *patterns)
+    {
+      auto maybe_old = Resolver::maybe_unwrap_string(item / Key);
+      if (!maybe_old.has_value())
+      {
+        return err(
+          item / Key,
+          "strings.replace_n: operand 1 non-string key found in pattern object",
+          EvalTypeError);
+      }
+
+      auto maybe_new = Resolver::maybe_unwrap_string(item / Val);
+      if (!maybe_new.has_value())
+      {
+        return err(
+          item / Val,
+          "strings.replace_n: operand 1 non-string value found in pattern "
+          "object",
+          EvalTypeError);
+      }
+
+      std::string old_str = Resolver::get_string(*maybe_old);
+      std::string new_str = Resolver::get_string(*maybe_new);
+      replace(value_str, old_str, new_str);
+    }
+
+    return JSONString ^ value_str;
+  }
+
+  Node reverse(const Nodes& args)
+  {
+    Node x = Resolver::unwrap(
+      args[0], JSONString, "reverse: operand 1 ", EvalTypeError);
+    if (x->type() == Error)
+    {
+      return x;
+    }
+
+    std::string x_str = Resolver::get_string(x);
+    std::vector<rune> x_runes = utf8_to_runes(x_str);
+    std::reverse(x_runes.begin(), x_runes.end());
+    std::ostringstream y;
+    for (auto& rune : x_runes)
+    {
+      y << rune.source;
+    }
+    return JSONString ^ y.str();
+  }
+
+  Node substring(const Nodes& args)
+  {
+    Node value = Resolver::unwrap(
+      args[0], JSONString, "substring: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+    Node offset = Resolver::unwrap(
+      args[1], JSONInt, "substring: operand 2 ", EvalTypeError);
+    if (offset->type() == Error)
+    {
+      return offset;
+    }
+    Node length = Resolver::unwrap(
+      args[2], JSONInt, "substring: operand 3 ", EvalTypeError);
+    if (length->type() == Error)
+    {
+      return length;
+    }
+
+    std::string value_str = Resolver::get_string(value);
+    std::vector<rune> value_runes = utf8_to_runes(value_str);
+    std::int64_t offset_int = Resolver::get_int(offset).to_int();
+    if (offset_int < 0)
+    {
+      return err(args[1], "negative offset", EvalBuiltInError);
+    }
+
+    std::size_t offset_size = static_cast<std::size_t>(offset_int);
+    if (offset_size >= value_runes.size())
+    {
+      return JSONString ^ "";
+    }
+
+    std::int64_t length_int = Resolver::get_int(length).to_int();
+    std::size_t length_size;
+    if (length_int < 0)
+    {
+      length_size = value_runes.size() - offset_size;
+    }
+    else
+    {
+      length_size = static_cast<std::size_t>(length_int);
+    }
+
+    if (length_size > value_runes.size() - offset_size)
+    {
+      length_size = value_runes.size() - offset_size;
+    }
+
+    std::vector<rune> output_runes(
+      value_runes.begin() + offset_size,
+      value_runes.begin() + offset_size + length_size);
+    std::ostringstream output;
+    for (auto& rune : output_runes)
+    {
+      output << rune.source;
+    }
+
+    return JSONString ^ output.str();
+  }
+
+  std::string trim(
+    const std::string& value, const std::string& cutset, bool left, bool right)
+  {
+    runestring value_runes = utf8_to_runestring(value);
+    runestring cutset_runes = utf8_to_runestring(cutset);
+
+    std::size_t start, end;
+    if (left)
+    {
+      start = value_runes.find_first_not_of(cutset_runes);
+    }
+    else
+    {
+      start = 0;
+    }
+
+    if (right)
+    {
+      end = value_runes.find_last_not_of(cutset_runes);
+    }
+    else
+    {
+      end = value_runes.size();
+    }
+
+    if (start == value_runes.npos)
+    {
+      return "";
+    }
+
+    runestring output_runes = value_runes.substr(start, end - start + 1);
+    return runestring_to_utf8(output_runes);
+  }
+
+  Node trim(const Nodes& args)
+  {
+    Node value =
+      Resolver::unwrap(args[0], JSONString, "trim: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+    Node cutset =
+      Resolver::unwrap(args[1], JSONString, "trim: operand 2 ", EvalTypeError);
+    if (cutset->type() == Error)
+    {
+      return cutset;
+    }
+
+    return JSONString ^
+      trim(Resolver::get_string(value),
+           Resolver::get_string(cutset),
+           true,
+           true);
+  }
+
+  Node trim_left(const Nodes& args)
+  {
+    Node value = Resolver::unwrap(
+      args[0], JSONString, "trim_left: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+    Node cutset = Resolver::unwrap(
+      args[1], JSONString, "trim_left: operand 2 ", EvalTypeError);
+    if (cutset->type() == Error)
+    {
+      return cutset;
+    }
+
+    return JSONString ^
+      trim(Resolver::get_string(value),
+           Resolver::get_string(cutset),
+           true,
+           false);
+  }
+
+  Node trim_right(const Nodes& args)
+  {
+    Node value = Resolver::unwrap(
+      args[0], JSONString, "trim_right: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+    Node cutset = Resolver::unwrap(
+      args[1], JSONString, "trim_right: operand 2 ", EvalTypeError);
+    if (cutset->type() == Error)
+    {
+      return cutset;
+    }
+
+    return JSONString ^
+      trim(Resolver::get_string(value),
+           Resolver::get_string(cutset),
+           false,
+           true);
+  }
+
+  Node trim_space(const Nodes& args)
+  {
+    Node value = Resolver::unwrap(
+      args[0], JSONString, "trim_space: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+
+    return JSONString ^
+      trim(Resolver::get_string(value), " \t\n\r\v\f", true, true);
+  }
+
+  Node trim_prefix(const Nodes& args)
+  {
+    Node value = Resolver::unwrap(
+      args[0], JSONString, "trim_prefix: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+    Node prefix = Resolver::unwrap(
+      args[1], JSONString, "trim_prefix: operand 2 ", EvalTypeError);
+    if (prefix->type() == Error)
+    {
+      return prefix;
+    }
+
+    std::string value_str = Resolver::get_string(value);
+    std::string prefix_str = Resolver::get_string(prefix);
+    if (value_str.starts_with(prefix_str))
+    {
+      return JSONString ^ value_str.substr(prefix_str.size());
+    }
+
+    return value;
+  }
+
+  Node trim_suffix(const Nodes& args)
+  {
+    Node value = Resolver::unwrap(
+      args[0], JSONString, "trim_suffix: operand 1 ", EvalTypeError);
+    if (value->type() == Error)
+    {
+      return value;
+    }
+    Node suffix = Resolver::unwrap(
+      args[1], JSONString, "trim_suffix: operand 2 ", EvalTypeError);
+    if (suffix->type() == Error)
+    {
+      return suffix;
+    }
+
+    std::string value_str = Resolver::get_string(value);
+    std::string suffix_str = Resolver::get_string(suffix);
+    if (value_str.ends_with(suffix_str))
+    {
+      return JSONString ^
+        value_str.substr(0, value_str.size() - suffix_str.size());
+    }
+
+    return value;
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> strings()
+    {
+      return {
+        BuiltInDef::create(Location("concat"), 2, concat),
+        BuiltInDef::create(Location("startswith"), 2, startswith),
+        BuiltInDef::create(Location("endswith"), 2, endswith),
+        BuiltInDef::create(Location("contains"), 2, contains),
+        BuiltInDef::create(Location("format_int"), 2, format_int),
+        BuiltInDef::create(Location("indexof"), 2, indexof),
+        BuiltInDef::create(Location("indexof_n"), 2, indexof_n),
+        BuiltInDef::create(Location("lower"), 1, lower),
+        BuiltInDef::create(Location("upper"), 1, upper),
+        BuiltInDef::create(Location("replace"), 3, replace),
+        BuiltInDef::create(Location("split"), 2, split),
+        BuiltInDef::create(Location("sprintf"), 2, sprintf),
+        BuiltInDef::create(
+          Location("strings.any_prefix_match"), 2, any_prefix_match),
+        BuiltInDef::create(
+          Location("strings.any_suffix_match"), 2, any_suffix_match),
+        BuiltInDef::create(Location("strings.replace_n"), 2, replace_n),
+        BuiltInDef::create(Location("strings.reverse"), 1, reverse),
+        BuiltInDef::create(Location("substring"), 3, substring),
+        BuiltInDef::create(Location("trim"), 2, trim),
+        BuiltInDef::create(Location("trim_left"), 2, trim_left),
+        BuiltInDef::create(Location("trim_right"), 2, trim_right),
+        BuiltInDef::create(Location("trim_space"), 1, trim_space),
+        BuiltInDef::create(Location("trim_prefix"), 2, trim_prefix),
+        BuiltInDef::create(Location("trim_suffix"), 2, trim_suffix),
+      };
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/utf8.cc b/src/builtins/utf8.cc
new file mode 100644
index 00000000..b672c0cb
--- /dev/null
+++ b/src/builtins/utf8.cc
@@ -0,0 +1,260 @@
+#include "utf8.h"
+
+#include <vector>
+
+namespace
+{
+  const std::uint8_t MaskX = 0b11000000;
+  const std::uint8_t Mask1 = 0b10000000;
+  const std::uint8_t Mask2 = 0b11100000;
+  const std::uint8_t Mask3 = 0b11110000;
+  const std::uint8_t Mask4 = 0b11111000;
+
+  const std::uint8_t MarkX = 0b10000000;
+  const std::uint8_t Mark1 = 0b00000000;
+  const std::uint8_t Mark2 = 0b11000000;
+  const std::uint8_t Mark3 = 0b11100000;
+  const std::uint8_t Mark4 = 0b11110000;
+
+  const std::uint8_t ValueX = 0b00111111;
+  const std::uint8_t Value1 = 0b01111111;
+  const std::uint8_t Value2 = 0b00011111;
+  const std::uint8_t Value3 = 0b00001111;
+  const std::uint8_t Value4 = 0b00000111;
+
+  const std::uint32_t Max1 = 0x007F;
+  const std::uint32_t Max2 = 0x07FF;
+  const std::uint32_t Max3 = 0xFFFF;
+  const std::uint32_t Max4 = 0x10FFFF;
+
+  const std::size_t ShiftX = 6;
+
+  const std::uint32_t Bad = 0xFFFD;
+
+  void to_utf8(std::uint32_t value, std::string& utf8)
+  {
+    if (value <= Max1)
+    {
+      switch (value)
+      {
+        case '\f':
+          utf8.push_back('\\');
+          utf8.push_back('f');
+          break;
+        case '\n':
+          utf8.push_back('\\');
+          utf8.push_back('n');
+          break;
+        case '\r':
+          utf8.push_back('\\');
+          utf8.push_back('r');
+          break;
+        case '\t':
+          utf8.push_back('\\');
+          utf8.push_back('t');
+          break;
+        case '\v':
+          utf8.push_back('\\');
+          utf8.push_back('v');
+          break;
+        case '\\':
+          utf8.push_back('\\');
+          utf8.push_back('\\');
+          break;
+        case '\'':
+          utf8.push_back('\\');
+          utf8.push_back('\'');
+          break;
+        case '\"':
+          utf8.push_back('\\');
+          utf8.push_back('\"');
+          break;
+        default:
+          char c0 = static_cast<char>(Mark1 | value);
+          utf8.push_back(c0);
+          break;
+      }
+    }
+    else if (value <= Max2)
+    {
+      char c1 = static_cast<char>((value & ValueX) | MarkX);
+      value >>= ShiftX;
+      char c0 = static_cast<char>(Mark2 | value);
+      utf8.push_back(c0);
+      utf8.push_back(c1);
+    }
+    else if (value <= Max3)
+    {
+      char c2 = static_cast<char>((value & ValueX) | MarkX);
+      value >>= ShiftX;
+      char c1 = static_cast<char>((value & ValueX) | MarkX);
+      value >>= ShiftX;
+      char c0 = static_cast<char>(Mark3 | value);
+      utf8.push_back(c0);
+      utf8.push_back(c1);
+      utf8.push_back(c2);
+    }
+    else if (value <= Max4)
+    {
+      char c3 = static_cast<char>((value & ValueX) | MarkX);
+      value >>= ShiftX;
+      char c2 = static_cast<char>((value & ValueX) | MarkX);
+      value >>= ShiftX;
+      char c1 = static_cast<char>((value & ValueX) | MarkX);
+      value >>= ShiftX;
+      char c0 = static_cast<char>(Mark4 | value);
+      utf8.push_back(c0);
+      utf8.push_back(c1);
+      utf8.push_back(c2);
+      utf8.push_back(c3);
+    }
+    else
+    {
+      // bad
+      to_utf8(Bad, utf8);
+    }
+  }
+
+  rego::rune utf8_to_rune(
+    std::string_view::const_iterator pos, std::string_view::const_iterator end)
+  {
+    std::uint8_t c0 = static_cast<std::uint8_t>(pos[0]);
+    std::size_t remaining = std::distance(pos, end);
+    if (c0 == '\\')
+    {
+      if (remaining >= 1)
+      {
+        switch (pos[1])
+        {
+          case 'f':
+            return {'\f', std::string_view(pos, pos + 2)};
+          case 'n':
+            return {'\n', std::string_view(pos, pos + 2)};
+          case 'r':
+            return {'\r', std::string_view(pos, pos + 2)};
+          case 't':
+            return {'\t', std::string_view(pos, pos + 2)};
+          case 'v':
+            return {'\v', std::string_view(pos, pos + 2)};
+          case '\\':
+            return {'\\', std::string_view(pos, pos + 2)};
+          case '\'':
+            return {'\'', std::string_view(pos, pos + 2)};
+          case '\"':
+            return {'\"', std::string_view(pos, pos + 2)};
+        }
+      }
+      if (remaining >= 3 && pos[1] == 'x')
+      {
+        std::string hex = std::string(pos + 2, pos + 4);
+        std::uint32_t value = std::stoul(hex, nullptr, 16);
+        return {value, std::string_view(pos, pos + 4)};
+      }
+      if (remaining >= 5 && pos[1] == 'u')
+      {
+        std::string hex = std::string(pos + 2, pos + 6);
+        std::uint32_t value = std::stoul(hex, nullptr, 16);
+        return {value, std::string_view(pos, pos + 6)};
+      }
+      if (remaining >= 9 && pos[1] == 'U')
+      {
+        std::string hex = std::string(pos + 2, pos + 10);
+        std::uint32_t value = std::stoul(hex, nullptr, 16);
+        return {value, std::string_view(pos, pos + 10)};
+      }
+    }
+
+    if ((c0 & Mask1) == Mark1)
+    {
+      std::uint32_t value = c0 & Value1;
+      return {value, std::string_view(pos, pos + 1)};
+    }
+
+    if ((c0 & Mask2) == Mark2 && remaining >= 2)
+    {
+      std::uint8_t c1 = static_cast<std::uint8_t>(pos[1]);
+      if ((c1 & MaskX) == MarkX)
+      {
+        std::uint32_t value = c0 & Value2;
+        value = (value << ShiftX) | (c1 & ValueX);
+        return {value, std::string_view(pos, pos + 2)};
+      }
+    }
+    else if ((c0 & Mask3) == Mark3 && remaining >= 3)
+    {
+      std::uint8_t c1 = static_cast<std::uint8_t>(pos[1]);
+      std::uint8_t c2 = static_cast<std::uint8_t>(pos[2]);
+      if ((c1 & MaskX) == MarkX && (c2 & MaskX) == MarkX)
+      {
+        std::uint32_t value = c0 & Value3;
+        value = (value << ShiftX) | (c1 & ValueX);
+        value = (value << ShiftX) | (c2 & ValueX);
+        return {value, std::string_view(pos, pos + 3)};
+      }
+    }
+    else if ((c0 & Mask4) == Mark4 && remaining >= 4)
+    {
+      std::uint8_t c1 = static_cast<std::uint8_t>(pos[1]);
+      std::uint8_t c2 = static_cast<std::uint8_t>(pos[2]);
+      std::uint8_t c3 = static_cast<std::uint8_t>(pos[3]);
+      if (
+        (c1 & MaskX) == MarkX && (c2 & MaskX) == MarkX && (c3 & MaskX) == MarkX)
+      {
+        std::uint32_t value = c0 & Value4;
+        value = (value << ShiftX) | (c1 & ValueX);
+        value = (value << ShiftX) | (c2 & ValueX);
+        value = (value << ShiftX) | (c3 & ValueX);
+        return {value, std::string_view(pos, pos + 4)};
+      }
+    }
+
+    // bad
+    return {Bad, std::string_view(pos, pos + 1)};
+  }
+
+}
+
+namespace rego
+{
+  std::vector<rune> utf8_to_runes(const std::string_view& utf8)
+  {
+    std::vector<rune> runes;
+    runes.reserve(utf8.size());
+    auto pos = utf8.begin();
+    auto end = utf8.end();
+    while (pos < end)
+    {
+      rune r = utf8_to_rune(pos, end);
+      runes.push_back(r);
+      pos = r.source.end();
+    }
+
+    return runes;
+  }
+
+  runestring utf8_to_runestring(const std::string_view& utf8)
+  {
+    runestring runes;
+    runes.reserve(utf8.size());
+    auto pos = utf8.begin();
+    auto end = utf8.end();
+    while (pos < end)
+    {
+      rune r = utf8_to_rune(pos, end);
+      runes.push_back(r.value);
+      pos = r.source.end();
+    }
+    return runes;
+  }
+
+  std::string runestring_to_utf8(const runestring_view& runes)
+  {
+    std::string utf8;
+    utf8.reserve(runes.size());
+    for (std::uint32_t r : runes)
+    {
+      to_utf8(r, utf8);
+    }
+    return utf8;
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/utf8.h b/src/builtins/utf8.h
new file mode 100644
index 00000000..f51022b1
--- /dev/null
+++ b/src/builtins/utf8.h
@@ -0,0 +1,20 @@
+#pragma once
+
+#include <cstdint>
+#include <string>
+
+namespace rego
+{
+  struct rune
+  {
+    std::uint32_t value;
+    std::string_view source;
+  };
+
+  using runestring = std::basic_string<std::uint32_t>;
+  using runestring_view = std::basic_string_view<std::uint32_t>;
+
+  std::vector<rune> utf8_to_runes(const std::string_view& utf8);
+  runestring utf8_to_runestring(const std::string_view& utf8);
+  std::string runestring_to_utf8(const runestring_view& runes);
+}
\ No newline at end of file
diff --git a/src/lang.cc b/src/lang.cc
index f274cf56..010794bb 100644
--- a/src/lang.cc
+++ b/src/lang.cc
@@ -13,9 +13,8 @@ namespace
   // clang-format off
   inline const auto wfi =
       (Binding <<= Var * Term)
-    | (ObjectItem <<= Key * (Val >>= Term))
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
     | (DataItem <<= Key * (Val >>= DataTerm))
-    | (RefObjectItem <<= (Key >>= RefTerm) * (Val >>= Expr))
     | (Term <<= Scalar | Array | Object | Set | Undefined)
     | (Scalar <<= JSONString | JSONInt | JSONFloat | JSONTrue | JSONFalse | JSONNull)
     ;
@@ -39,6 +38,7 @@ namespace rego
       {"elses", elses(), &wf_pass_elses},
       {"rules", rules(), &wf_pass_rules},
       {"build_calls", build_calls(), &wf_pass_build_calls},
+      {"membership", membership(), &wf_pass_membership},
       {"build_refs", build_refs(), &wf_pass_build_refs},
       {"structure", structure(), &wf_pass_structure},
       {"strings", strings(), &wf_pass_strings},
@@ -53,13 +53,13 @@ namespace rego
       {"compr", compr(), &wf_pass_compr},
       {"absolute_refs", absolute_refs(), &wf_pass_absolute_refs},
       {"merge_modules", merge_modules(), &wf_pass_merge_modules},
-      {"skips", skips(builtins), &wf_pass_skips},
+      {"skips", skips(), &wf_pass_skips},
       {"unary", unary(), &wf_pass_unary},
       {"multiply_divide", multiply_divide(), &wf_pass_multiply_divide},
       {"add_subtract", add_subtract(), &wf_pass_add_subtract},
       {"comparison", comparison(), &wf_pass_comparison},
       {"assign", assign(builtins), &wf_pass_assign},
-      {"skip_refs", skip_refs(), &wf_pass_skip_refs},
+      {"skip_refs", skip_refs(builtins), &wf_pass_skip_refs},
       {"simple_refs", simple_refs(), &wf_pass_simple_refs},
       {"implicit_enums", implicit_enums(), &wf_pass_implicit_enums},
       {"init", init(), &wf_pass_init},
@@ -88,6 +88,7 @@ namespace rego
         {"elses", elses(), wf_pass_elses},
         {"rules", rules(), wf_pass_rules},
         {"build_calls", build_calls(), wf_pass_build_calls},
+        {"membership", membership(), wf_pass_membership},
         {"build_refs", build_refs(), wf_pass_build_refs},
         {"structure", structure(), wf_pass_structure},
         {"strings", strings(), wf_pass_strings},
@@ -102,13 +103,13 @@ namespace rego
         {"compr", compr(), wf_pass_compr},
         {"absolute_refs", absolute_refs(), wf_pass_absolute_refs},
         {"merge_modules", merge_modules(), wf_pass_merge_modules},
-        {"skips", skips(builtins), wf_pass_skips},
+        {"skips", skips(), wf_pass_skips},
         {"unary", unary(), wf_pass_unary},
         {"multiply_divide", multiply_divide(), wf_pass_multiply_divide},
         {"add_subtract", add_subtract(), wf_pass_add_subtract},
         {"comparison", comparison(), wf_pass_comparison},
         {"assign", assign(builtins), wf_pass_assign},
-        {"skip_refs", skip_refs(), wf_pass_skip_refs},
+        {"skip_refs", skip_refs(builtins), wf_pass_skip_refs},
         {"simple_refs", simple_refs(), wf_pass_simple_refs},
         {"implicit_enums", implicit_enums(), wf_pass_implicit_enums},
         {"init", init(), wf_pass_init},
@@ -130,7 +131,16 @@ namespace rego
     }
     else if (node->type() == JSONFloat)
     {
-      buf << node->location().view();
+      try
+      {
+        double value = std::stod(std::string(node->location().view()));
+        buf << std::setprecision(std::numeric_limits<double>::max_digits10 - 1)
+            << std::noshowpoint << value;
+      }
+      catch (...)
+      {
+        buf << node->location().view();
+      }
     }
     else if (node->type() == JSONString)
     {
@@ -369,7 +379,8 @@ namespace rego
     {
       for (auto& item : *node)
       {
-        if (item->type() == RefObjectItem)
+        Node key = item / Key;
+        if (!is_constant(key->front()))
         {
           return false;
         }
diff --git a/src/parse.cc b/src/parse.cc
index fc2bb613..ad360ff1 100644
--- a/src/parse.cc
+++ b/src/parse.cc
@@ -138,7 +138,7 @@ namespace rego
           [](auto& m) { m.add(JSONFloat); },
 
         // String.
-        R"("(?:\\(?:["\\\/bfnrt]|u[a-fA-F0-9]{4})|[^"\\\0-\x1F\x7F]+)*")" >>
+        R"("(?:\\(?:["\\\/bfnrtx]|u[a-fA-F0-9]{4})|[^"\\\0-\x1F\x7F]+)*")" >>
           [](auto& m) { m.add(JSONString); },
 
         // Raw string.
@@ -147,6 +147,10 @@ namespace rego
         // Int.
         R"([[:digit:]]+\b)" >> [](auto& m) { m.add(JSONInt); },
 
+        // Float with exponent but no decimal.
+        R"([[:digit:]]+(?:e[+-]?[[:digit:]]+)?\b)" >>
+          [](auto& m) { m.add(JSONFloat); },
+
         // True.
         "true\\b" >> [](auto& m) { m.add(JSONTrue); },
 
diff --git a/src/passes/arithmetic.cc b/src/passes/arithmetic.cc
index 15fa3f72..2d6581da 100644
--- a/src/passes/arithmetic.cc
+++ b/src/passes/arithmetic.cc
@@ -1,3 +1,4 @@
+#include "errors.h"
 #include "passes.h"
 
 namespace rego
@@ -63,13 +64,13 @@ namespace rego
       In(Expr) * (ArithInfixArg * T(Subtract) * BinInfixArg[Rhs]) >>
         [](Match& _) {
           return err(
-            _(Rhs), "operand 2 must be number but got set", "eval_type_error");
+            _(Rhs), "operand 2 must be number but got set", EvalTypeError);
         },
 
       In(Expr) * (BinInfixArg * T(Subtract) * ArithInfixArg[Rhs]) >>
         [](Match& _) {
           return err(
-            _(Rhs), "operand 2 must be set but got number", "eval_type_error");
+            _(Rhs), "operand 2 must be set but got number", EvalTypeError);
         },
 
       In(Expr) * (T(Subtract) * ArithInfixArg[Val] * End) >>
diff --git a/src/passes/assign.cc b/src/passes/assign.cc
index 719cadbb..a381014c 100644
--- a/src/passes/assign.cc
+++ b/src/passes/assign.cc
@@ -45,14 +45,6 @@ namespace
       return false;
     }
 
-    Node last = argseq->back();
-    Nodes defs = last->lookup();
-    if (defs.size() != 1 || defs[0]->type() != Local)
-    {
-      cache->insert({varseq, false});
-      return false;
-    }
-
     auto builtin = builtins.at(path);
     bool result = builtin->arity == argseq->size() - 1;
 
@@ -65,7 +57,7 @@ namespace rego
 {
   const inline auto AssignInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
     T(ArithInfix) / T(BinInfix) / T(Term) / T(BoolInfix) / T(ExprCall) /
-    T(Enumerate);
+    T(Enumerate) / T(Membership);
 
   // Transforms unification expressions into AssignInfix nodes.
   PassDef assign(const BuiltIns& builtins)
diff --git a/src/passes/build_calls.cc b/src/passes/build_calls.cc
index 79e3696b..1a0a8a42 100644
--- a/src/passes/build_calls.cc
+++ b/src/passes/build_calls.cc
@@ -18,7 +18,32 @@ namespace rego
           }
           else
           {
-            argseq->push_back(paren->front());
+            // check if this was marked as a potential membership statement
+            // As calls have precedence, we may have to split up the group.
+            Node group = paren->front();
+            auto comma = group->begin();
+            for (; comma != group->end(); ++comma)
+            {
+              Node node = *comma;
+              if (node->type() == Comma)
+              {
+                break;
+              }
+            }
+
+            if (comma != group->end())
+            {
+              // split the group into two groups
+              Node head = NodeDef::create(Group);
+              Node tail = NodeDef::create(Group);
+              head->insert(head->end(), group->begin(), comma);
+              tail->insert(tail->end(), comma + 1, group->end());
+              argseq << head << tail;
+            }
+            else
+            {
+              argseq << group;
+            }
           }
 
           return ExprCall << (VarSeq << (Group << _(Var))) << argseq;
diff --git a/src/passes/comparison.cc b/src/passes/comparison.cc
index 0c86f3f3..e4968dc3 100644
--- a/src/passes/comparison.cc
+++ b/src/passes/comparison.cc
@@ -15,60 +15,25 @@ namespace rego
       In(UnifyBody) * (T(Literal) << (T(Expr) << (T(Not) * Any++[Expr]))) >>
         [](Match& _) { return LiteralNot << (Expr << _[Expr]); },
 
-      In(Literal) *
-          (T(Expr)
-           << (BoolInfixArg[Lhs](
-                 [](auto& n) { return is_in(*n.first, {UnifyBody}); }) *
-               T(Equals) * BoolInfixArg[Rhs])) >>
-        [](Match& _) {
-          Location unify0 = _.fresh({"unify"});
-          return Seq << (Lift << UnifyBody
-                              << (Local << (Var ^ unify0) << Undefined))
-                     << (Lift << UnifyBody
-                              << (Literal
-                                  << (Expr << (RefTerm << (Var ^ unify0))
-                                           << Unify << _(Lhs))))
-                     << (Expr << (RefTerm << (Var ^ unify0)) << Unify
-                              << _(Rhs));
-        },
-
       In(Expr) * (T(Expr) << (BoolInfixArg[Arg] * End)) >>
         [](Match& _) { return _(Arg); },
 
       In(Expr) * (BoolInfixArg[Lhs] * BoolToken[Op] * BoolInfixArg[Rhs]) >>
         [](Match& _) {
-          return BoolInfix << (BoolArg << _(Lhs)) << _(Op)
-                           << (BoolArg << _(Rhs));
-        },
-
-      In(Expr) *
-          (BoolInfixArg[Lhs](
-             [](auto& n) { return is_in(*n.first, {UnifyBody}); }) *
-           T(MemberOf) * BoolInfixArg[Rhs]) >>
-        [](Match& _) {
-          Location item = _.fresh({"item"});
-          Location unify =
-            _.fresh({is_in(_(Lhs), {LiteralNot}) ? "not" : "unify"});
-          Node seq = Seq
-            << (Lift << UnifyBody << (Local << (Var ^ item) << Undefined))
-            << (Lift << UnifyBody << (Local << (Var ^ unify) << Undefined))
-            << (Lift << UnifyBody
-                     << (Literal
-                         << (Expr << (RefTerm << (Var ^ item)) << Unify
-                                  << (Enumerate << (Expr << _(Rhs))))))
-            << (Lift << UnifyBody
-                     << (Literal
-                         << (Expr
-                             << (RefTerm << (Var ^ unify)) << Unify
-                             << (RefTerm
-                                 << (Ref << (RefHead << (Var ^ item))
-                                         << (RefArgSeq
-                                             << (RefArgBrack
-                                                 << (Scalar
-                                                     << (JSONInt ^ "1")))))))))
-            << (RefTerm << (Var ^ unify)) << Unify << _(Lhs);
-
-          return seq;
+          std::set<Token> set_types = {Set, SetCompr};
+          Node lhs = _(Lhs);
+          if (set_types.contains(lhs->type()))
+          {
+            lhs = Term << lhs;
+          }
+
+          Node rhs = _(Rhs);
+          if (set_types.contains(rhs->type()))
+          {
+            rhs = Term << rhs;
+          }
+
+          return BoolInfix << (BoolArg << lhs) << _(Op) << (BoolArg << rhs);
         },
 
       In(Expr) * (T(Set) / T(SetCompr))[Set] >>
@@ -91,9 +56,6 @@ namespace rego
       In(Expr) * T(Not)[Not] >>
         [](Match& _) { return err(_(Not), "Invalid not"); },
 
-      In(Expr) * T(MemberOf)[MemberOf] >>
-        [](Match& _) { return err(_(MemberOf), "Invalid in statement"); },
-
       In(BoolArg) * T(Expr)[Expr] >>
         [](Match& _) { return err(_(Expr), "Invalid boolean argument"); },
 
diff --git a/src/passes/compr.cc b/src/passes/compr.cc
index 442f8c4c..46c69276 100644
--- a/src/passes/compr.cc
+++ b/src/passes/compr.cc
@@ -36,13 +36,15 @@ namespace rego
             << (T(Var)[Var] * T(UnifyBody)[Body] * T(Expr)[Val]) >>
           [](Match& _) {
             Location value = _.fresh({"value"});
+            Location compr = _.fresh({"compr"});
+            Node body = NestedBody << (Key ^ compr) << _(Body);
             return RuleSet
               << _(Var) << Empty
               << (UnifyBody
                   << (Local << (Var ^ value) << Undefined)
                   << (Literal
                       << (Expr << (RefTerm << (Var ^ value)) << Unify
-                               << (Term << (SetCompr << _(Val) << _(Body))))));
+                               << (Term << (SetCompr << _(Val) << body)))));
           },
 
         In(Policy) *
@@ -59,13 +61,14 @@ namespace rego
                   << (Literal
                       << (Expr << (RefTerm << (Var ^ key)) << Unify << _(Key)))
                   << (Literal
-                      << (Expr << (RefTerm << (Var ^ value)) << Unify
-                               << (Expr
-                                   << (Term
-                                       << (Object
-                                           << (RefObjectItem
-                                               << (RefTerm << (Var ^ key))
-                                               << _(Val))))))));
+                      << (Expr
+                          << (RefTerm << (Var ^ value)) << Unify
+                          << (Expr
+                              << (Term
+                                  << (Object
+                                      << (ObjectItem
+                                          << (Expr << (RefTerm << (Var ^ key)))
+                                          << _(Val))))))));
           },
 
         In(Policy) *
@@ -73,11 +76,10 @@ namespace rego
              << (T(Var)[Var] * (T(UnifyBody) / T(Empty))[Body] *
                  (T(DataTerm)[Key]) * T(DataTerm)[Val])) >>
           [](Match& _) {
-            std::string key = strip_quotes(to_json(_(Key)));
             return RuleObj << _(Var) << _(Body)
                            << (DataTerm
                                << (DataObject
-                                   << (DataItem << (Key ^ key) << _(Val))));
+                                   << (DataObjectItem << _(Key) << _(Val))));
           },
 
         In(Policy) *
@@ -86,7 +88,7 @@ namespace rego
                  T(Expr)[Val])) >>
           [](Match& _) {
             Location value = _.fresh({"value"});
-            Location key = _.fresh({"key"});
+            Location compr = _.fresh({"compr"});
             Node body = _(Body);
             if (body->type() == Empty)
             {
@@ -94,11 +96,12 @@ namespace rego
                 << (Literal << (Expr << (Term << (Scalar << (JSONTrue)))));
             }
 
+            body = NestedBody << (Key ^ compr) << body;
+
             return RuleObj
               << _(Var) << Empty
               << (UnifyBody
                   << (Local << (Var ^ value) << Undefined)
-                  << (Local << (Var ^ key) << Undefined)
                   << (Literal
                       << (Expr << (RefTerm << (Var ^ value)) << Unify
                                << (Expr
@@ -108,32 +111,40 @@ namespace rego
           },
 
         (In(ArrayCompr) / In(SetCompr)) *
-            (T(Expr)[Expr] * T(UnifyBody)[UnifyBody]) >>
+            (T(Expr)[Expr] * T(NestedBody)[NestedBody]) >>
           [](Match& _) {
             Location out = _.fresh({"out"});
-            Location compr = _.fresh({"compr"});
-            _(UnifyBody)
+            Node tail = _(NestedBody) / Val;
+            while (tail->back()->type() == LiteralEnum)
+            {
+              tail = tail->back() / UnifyBody;
+            }
+            tail
               << (Literal
                   << (Expr << (RefTerm << (Var ^ out)) << Unify << _(Expr)));
-            _(UnifyBody)->push_front(Local << (Var ^ out) << Undefined);
-            return Seq << (Var ^ out)
-                       << (NestedBody << (Key ^ compr) << _(UnifyBody));
+            (_(NestedBody) / Val)
+              ->push_front(Local << (Var ^ out) << Undefined);
+            return Seq << (Var ^ out) << _(NestedBody);
           },
 
         In(ObjectCompr) *
-            (T(Expr)[Key] * T(Expr)[Val] * T(UnifyBody)[UnifyBody]) >>
+            (T(Expr)[Key] * T(Expr)[Val] * T(NestedBody)[NestedBody]) >>
           [](Match& _) {
             Location out = _.fresh({"out"});
-            Location compr = _.fresh({"compr"});
+            Node tail = _(NestedBody) / Val;
+            while (tail->back()->type() == LiteralEnum)
+            {
+              tail = tail->back() / UnifyBody;
+            }
 
-            _(UnifyBody)
+            tail
               << (Literal
                   << (Expr << (RefTerm << (Var ^ out)) << Unify
                            << (Expr << (Term << (Array << _(Key) << _(Val))))));
-            _(UnifyBody)->push_front(Local << (Var ^ out) << Undefined);
+            (_(NestedBody) / Val)
+              ->push_front(Local << (Var ^ out) << Undefined);
 
-            return Seq << (Var ^ out)
-                       << (NestedBody << (Key ^ compr) << _(UnifyBody));
+            return Seq << (Var ^ out) << _(NestedBody);
           },
 
         // errors
diff --git a/src/passes/constants.cc b/src/passes/constants.cc
index ff0cdc04..352d3a54 100644
--- a/src/passes/constants.cc
+++ b/src/passes/constants.cc
@@ -56,16 +56,31 @@ namespace rego
         [](Match& _) { return DataTerm << (Scalar << _(NumTerm)->front()); },
 
       In(DataObject) *
-          (T(ObjectItem) << (T(Key)[Key] * (T(Expr) << T(Term)[Term]))) >>
+          (T(ObjectItem)
+           << ((T(Expr) << (T(Term) / T(NumTerm))[Key]) *
+               (T(Expr) << (T(Term) / T(NumTerm))[Val]))) >>
         [](Match& _) {
-          return DataItem << _(Key) << (DataTerm << _(Term)->front());
-        },
-
-      In(DataObject) *
-          (T(ObjectItem) << (T(Key)[Key] * (T(Expr) << T(NumTerm)[NumTerm]))) >>
-        [](Match& _) {
-          return DataItem << _(Key)
-                          << (DataTerm << (Scalar << _(NumTerm)->front()));
+          Node key = _(Key);
+          if (key->type() == NumTerm)
+          {
+            key = Scalar << key->front();
+          }
+          else
+          {
+            key = key->front();
+          }
+
+          Node val = _(Val);
+          if (val->type() == NumTerm)
+          {
+            val = Scalar << val->front();
+          }
+          else
+          {
+            val = val->front();
+          }
+
+          return DataObjectItem << (DataTerm << key) << (DataTerm << val);
         },
 
       // errors
diff --git a/src/passes/functions.cc b/src/passes/functions.cc
index d00cbef1..e0af03fc 100644
--- a/src/passes/functions.cc
+++ b/src/passes/functions.cc
@@ -11,9 +11,7 @@ namespace
 
   // clang-format off
   inline const auto wfi =
-      (ObjectItem <<= Key * Expr)
-    | (RefObjectItem <<= RefTerm * Expr)
-    | (NumTerm <<= JSONInt | JSONFloat)
+    (NumTerm <<= JSONInt | JSONFloat)
     | (ArithArg <<= RefTerm | NumTerm | UnaryExpr | ArithInfix)
     | (BoolArg <<= Term | RefTerm | NumTerm | UnaryExpr | ArithInfix)
     | (RefArgDot <<= Var)
@@ -57,24 +55,27 @@ namespace rego
       In(ArgSeq) * T(Set)[Set] >> [](Match& _) { return Term << _(Set); },
 
       (In(UnifyExpr) / In(ArgSeq)) * T(ObjectItem)[ObjectItem] >>
+        [](Match& _) { return Seq << *_[ObjectItem]; },
+
+      (In(UnifyExpr) / In(ArgSeq)) * (T(Enumerate) << T(Expr)[Expr]) >>
         [](Match& _) {
-          Node seq = NodeDef::create(Seq);
-          seq->push_back(wfi / _(ObjectItem) / Key);
-          seq->push_back(wfi / _(ObjectItem) / Expr);
-          return seq;
+          return Function << (JSONString ^ "enumerate") << (ArgSeq << _(Expr));
         },
 
-      (In(UnifyExpr) / In(ArgSeq)) * T(RefObjectItem)[RefObjectItem] >>
+      (In(UnifyExpr) / In(ArgSeq)) *
+          (T(Membership)
+           << (T(Expr)[Idx] * T(Expr)[Item] * T(Expr)[ItemSeq])) >>
         [](Match& _) {
-          Node seq = NodeDef::create(Seq);
-          seq->push_back(wfi / _(RefObjectItem) / RefTerm);
-          seq->push_back(wfi / _(RefObjectItem) / Expr);
-          return seq;
+          return Function << (JSONString ^ "membership-tuple")
+                          << (ArgSeq << _(Idx) << _(Item) << _(ItemSeq));
         },
 
-      (In(UnifyExpr) / In(ArgSeq)) * (T(Enumerate) << T(Expr)[Expr]) >>
+      (In(UnifyExpr) / In(ArgSeq)) *
+          (T(Membership)
+           << (T(Undefined) * T(Expr)[Item] * T(Expr)[ItemSeq])) >>
         [](Match& _) {
-          return Function << (JSONString ^ "enumerate") << (ArgSeq << _(Expr));
+          return Function << (JSONString ^ "membership-single")
+                          << (ArgSeq << _(Item) << _(ItemSeq));
         },
 
       (In(UnifyExpr) / In(ArgSeq)) * (T(Term) << T(Array)[Array]) >>
@@ -266,8 +267,8 @@ namespace rego
       In(Term) * T(DataObject)[DataObject] >>
         [](Match& _) { return Object << *_[DataObject]; },
 
-      (In(Object) / In(ObjectItemSeq)) * T(DataItem)[DataItem] >>
-        [](Match& _) { return ObjectItem << *_[DataItem]; },
+      In(Object) * T(DataObjectItem)[DataObjectItem] >>
+        [](Match& _) { return ObjectItem << *_[DataObjectItem]; },
 
       (In(ObjectItem) / In(Array) / In(Set)) * T(DataTerm)[DataTerm] >>
         [](Match& _) { return Term << *_[DataTerm]; },
@@ -292,9 +293,6 @@ namespace rego
       In(ArgSeq) * T(Ref)[Ref] >>
         [](Match& _) { return err(_(Ref), "Invalid reference"); },
 
-      In(Object) * T(RefObjectItem)[RefObjectItem] >>
-        [](Match& _) { return err(_(RefObjectItem), "Invalid object item"); },
-
       In(ObjectItem) * T(Module)[Module] >>
         [](Match& _) {
           return err(
diff --git a/src/passes/if_else.cc b/src/passes/if_else.cc
index 0b04c236..92fb9e4c 100644
--- a/src/passes/if_else.cc
+++ b/src/passes/if_else.cc
@@ -4,7 +4,7 @@ namespace
 {
   using namespace rego;
 
-  const inline auto ExprTailToken = ExprToken / T(InSome);
+  const inline auto ExprTailToken = ExprToken / T(IsIn);
 }
 
 namespace rego
@@ -13,16 +13,16 @@ namespace rego
   PassDef ifs()
   {
     return {
-      In(Group) * (T(IfTruthy) * ExprToken[Head] * ExprTailToken++[Tail]) >>
+      In(Group) * (T(If) * ExprToken[Head] * ExprTailToken++[Tail]) >>
         [](Match& _) { return UnifyBody << (Group << _(Head) << _[Tail]); },
 
-      In(Group) * (T(IfTruthy) * T(UnifyBody)[UnifyBody]) >>
+      In(Group) * (T(If) * T(UnifyBody)[UnifyBody]) >>
         [](Match& _) { return _(UnifyBody); },
 
       // errors
 
-      In(Group) * T(IfTruthy)[IfTruthy] >>
-        [](Match& _) { return err(_(IfTruthy), "Invalid if statement"); },
+      In(Group) * T(If)[If] >>
+        [](Match& _) { return err(_(If), "Invalid if statement"); },
     };
   }
 
diff --git a/src/passes/keywords.cc b/src/passes/keywords.cc
index b35cf34f..a784cf88 100644
--- a/src/passes/keywords.cc
+++ b/src/passes/keywords.cc
@@ -35,12 +35,12 @@ namespace rego
           auto name = _(Var)->location().view();
           if (name == "if")
           {
-            return IfTruthy ^ _(Var)->location();
+            return If ^ _(Var)->location();
           }
 
           if (name == "in")
           {
-            return InSome ^ _(Var)->location();
+            return IsIn ^ _(Var)->location();
           }
 
           if (name == "contains")
diff --git a/src/passes/lift_to_rule.cc b/src/passes/lift_to_rule.cc
index 71bcb03c..f71ffe9b 100644
--- a/src/passes/lift_to_rule.cc
+++ b/src/passes/lift_to_rule.cc
@@ -23,7 +23,7 @@ namespace
       return false;
     }
 
-    return local->parent() != unifybody.get();
+    return var->common_parent(local) != unifybody;
   }
 
   void add_captures(Node body, Node node, Locs& locs)
@@ -173,8 +173,9 @@ namespace rego
               for (auto& [var, out_var] : out_map)
               {
                 rulevalue
-                  << (ObjectItem << (Key ^ var)
-                                 << (Expr << (RefTerm << (Var ^ out_var))));
+                  << (ObjectItem
+                      << (Expr << (Term << (Scalar << (JSONString ^ var))))
+                      << (Expr << (RefTerm << (Var ^ out_var))));
               }
               Location value = _.fresh({"value"});
               rulevalue = UnifyBody
diff --git a/src/passes/lists.cc b/src/passes/lists.cc
index 5ca2c65f..200d59e0 100644
--- a/src/passes/lists.cc
+++ b/src/passes/lists.cc
@@ -55,9 +55,40 @@ namespace rego
       (In(Input) / In(Data)) * (T(Brace) << End) >>
         ([](Match&) -> Node { return ObjectItemSeq; }),
 
-      In(Group) * (T(Var)[Var] * T(IfTruthy) * T(Brace)[Brace]) >>
+      In(Group) * (T(Var)[Var] * T(If) * T(Brace)[Brace]) >>
         [](Match& _) {
-          return Seq << _(Var) << IfTruthy << (UnifyBody << *_[Brace]);
+          return Seq << _(Var) << If << (UnifyBody << *_[Brace]);
+        },
+
+      In(Policy) *
+          (T(Group) << (T(Var)[Var] * T(Brace)[Head] * T(Brace)++[Tail])) >>
+        [](Match& _) {
+          Node group = Group << _(Var) << (UnifyBody << *_[Head]);
+          for (auto it = _[Tail].first; it != _[Tail].second; ++it)
+          {
+            Node unifybody = NodeDef::create(UnifyBody);
+            Node brace = *it;
+            unifybody->insert(unifybody->end(), brace->begin(), brace->end());
+            group << unifybody;
+          }
+          return group;
+        },
+
+      In(Policy) *
+          (T(Group)
+           << (T(Var)[Var] * (T(Assign) / T(Unify))[Assign] * ExprToken[Head] *
+               ExprToken++[Tail] * T(Brace)[Head1] * T(Brace)++[Tail1])) >>
+        [](Match& _) {
+          Node group = Group << _(Var) << _(Assign) << _(Head) << _[Tail]
+                             << (UnifyBody << *_[Head1]);
+          for (auto it = _[Tail1].first; it != _[Tail1].second; ++it)
+          {
+            Node unifybody = NodeDef::create(UnifyBody);
+            Node brace = *it;
+            unifybody->insert(unifybody->end(), brace->begin(), brace->end());
+            group << unifybody;
+          }
+          return group;
         },
 
       In(Group) *
@@ -173,6 +204,58 @@ namespace rego
       In(Group) * (T(Brace) << (T(List)[List] * End)) >>
         [](Match& _) { return Set << *_[List]; },
 
+      T(List)
+          << ((T(Group)
+               << (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
+                   MembershipToken++[Idx] * End)) *
+              (T(Group) << (MembershipToken++[Item] * T(IsIn) *
+                            MembershipToken++[ItemSeq] * T(IsIn) *
+                            MembershipToken++[ItemSeq1]) *
+                 End)) >>
+        [](Match& _) {
+          return Group << _[Lhs] << _(Assign)
+                       << (Paren
+                           << (Group << _[Idx] << Comma << _[Item] << IsIn
+                                     << _[ItemSeq]))
+                       << IsIn << _[ItemSeq1];
+        },
+
+      T(List)
+          << ((T(Group) << (MembershipToken++[Idx] * End)) *
+              (T(Group) << (MembershipToken++[Item] * T(IsIn) *
+                            MembershipToken++[ItemSeq] * T(IsIn) *
+                            MembershipToken++[ItemSeq1]) *
+                 End)) >>
+        [](Match& _) {
+          return Group << (Paren
+                           << (Group << _[Idx] << Comma << _[Item] << IsIn
+                                     << _[ItemSeq]))
+                       << IsIn << _[ItemSeq1];
+        },
+
+      T(List)
+          << ((T(Group)
+               << (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
+                   MembershipToken++[Idx] * End)) *
+              (T(Group)
+               << (MembershipToken++[Item] * T(IsIn)[IsIn] *
+                   MembershipToken++[ItemSeq] * End))) >>
+        [](Match& _) {
+          return Group << _[Lhs] << _(Assign) << _[Idx] << Comma << _[Item]
+                       << IsIn << _[ItemSeq];
+        },
+
+      T(List)
+          << ((T(Group) << (MembershipToken++[Idx] * End)) *
+              (T(Group)
+               << (MembershipToken++[Item] * T(IsIn)[IsIn] *
+                   MembershipToken++[ItemSeq] * End))) >>
+        [](Match& _) {
+          return Group << _[Idx] << Comma << _[Item] << IsIn << _[ItemSeq];
+        },
+
+      In(UnifyBody) * T(List)[List] >> [](Match& _) { return Seq << *_[List]; },
+
       In(Group) * (T(Some) << (T(List)[List] * End)) >>
         [](Match& _) {
           Node list = _(List);
@@ -181,42 +264,71 @@ namespace rego
             return err(list, "some must contain at least one variable");
           }
 
-          Node last_group = list->back();
-          if (last_group->size() > 1)
+          Node back = list->back();
+          auto in_pos = back->begin();
+          for (; in_pos != back->end(); ++in_pos)
           {
-            Node insome_group = NodeDef::create(Group);
-            insome_group->insert(
-              insome_group->end(), last_group->begin() + 1, last_group->end());
-            list->back() = Group << last_group->front();
-            Node varseq = NodeDef::create(VarSeq);
-            varseq->insert(varseq->end(), list->begin(), list->end());
-            return SomeDecl << varseq << insome_group;
+            Node n = *in_pos;
+            if (n->type() == IsIn)
+            {
+              break;
+            }
           }
-          else
+
+          if (in_pos != back->end())
           {
-            return SomeDecl << (VarSeq << *_[List]) << (Group << Undefined);
+            Node item = NodeDef::create(Group);
+            item->insert(item->end(), back->begin(), in_pos);
+            Node itemseq = NodeDef::create(Group);
+            itemseq->insert(itemseq->end(), in_pos, back->end());
+            if (list->size() == 2)
+            {
+              // some i, x in c
+              return SomeDecl << (VarSeq << list->front() << item) << itemseq;
+            }
+            else
+            {
+              // some x in c
+              return SomeDecl << (VarSeq << item) << itemseq;
+            }
           }
+
+          // some x, y, z, ...
+          return SomeDecl << (VarSeq << *_[List]) << (Group << Undefined);
         },
 
       In(Group) * (T(Some) << T(Group)[Group]) >>
         [](Match& _) {
           Node group = _(Group);
-          if (group->size() > 1)
+
+          auto in_pos = group->begin();
+          for (; in_pos != group->end(); ++in_pos)
           {
-            Node insome_group = NodeDef::create(Group);
-            insome_group->insert(
-              insome_group->end(), group->begin() + 1, group->end());
-            return SomeDecl << (VarSeq << (Group << group->front()))
-                            << insome_group;
+            Node n = *in_pos;
+            if (n->type() == IsIn)
+            {
+              break;
+            }
           }
-          else
+
+          if (in_pos != group->end())
           {
-            return SomeDecl << (VarSeq << _(Group)) << (Group << Undefined);
+            Node item = NodeDef::create(Group);
+            item->insert(item->end(), group->begin(), in_pos);
+            Node itemseq = NodeDef::create(Group);
+            itemseq->insert(itemseq->end(), in_pos, group->end());
+            // some x in c
+            return SomeDecl << (VarSeq << item) << itemseq;
           }
+
+          // some x
+          return SomeDecl << (VarSeq << group) << (Group << Undefined);
         },
 
       In(Group) * T(EmptySet) >> ([](Match&) -> Node { return Set; }),
 
+      In(Group) * (T(Square) << End) >> [](Match&) -> Node { return Array; },
+
       // errors
 
       (In(Input) / In(Data)) * T(Brace)[Brace] >>
@@ -275,6 +387,12 @@ namespace rego
       (In(ArrayCompr) / In(SetCompr) / In(ObjectCompr)) *
           (T(Group)[Group] << End) >>
         [](Match& _) { return err(_(Group), "Invalid comprehension"); },
+
+      In(UnifyBody) * T(ObjectItem)[ObjectItem] >>
+        [](Match& _) { return err(_(ObjectItem), "Invalid key/value pair"); },
+
+      T(Group)[Group] << End >>
+        [](Match& _) { return err(_(Group), "Syntax error: empty group"); },
     };
   }
 }
\ No newline at end of file
diff --git a/src/passes/locals.cc b/src/passes/locals.cc
index 9a4f98c1..94ea9b4b 100644
--- a/src/passes/locals.cc
+++ b/src/passes/locals.cc
@@ -82,6 +82,11 @@ namespace
   void add_locals(
     Node unifybody, std::vector<Scope>& scopes, const BuiltIns& builtins)
   {
+    if (unifybody->type() == NestedBody)
+    {
+      unifybody = unifybody / Val;
+    }
+
     if (unifybody->type() != UnifyBody)
     {
       return;
diff --git a/src/passes/membership.cc b/src/passes/membership.cc
new file mode 100644
index 00000000..8ec64589
--- /dev/null
+++ b/src/passes/membership.cc
@@ -0,0 +1,84 @@
+#include "passes.h"
+#include "trieste/rewrite.h"
+
+namespace
+{
+  using namespace rego;
+
+  const auto inline GroundToken =
+    ScalarToken / T(JSONString) / T(RawString) / T(ExprCall);
+}
+
+namespace rego
+{
+  PassDef membership()
+  {
+    return {
+      In(Group) * (T(IsIn) * (T(UnifyBody) << (T(Group)[Group] * End))) >>
+        [](Match& _) { return Seq << IsIn << (Set << _(Group)); },
+
+      In(Group) *
+          (MembershipToken++[Item] * T(IsIn) * MembershipToken++[ItemSeq] *
+           T(IsIn) * MembershipToken++[ItemSeq1]) >>
+        [](Match& _) {
+          return Seq << (Paren << (Group << _[Item] << IsIn << _[ItemSeq]))
+                     << IsIn << _[ItemSeq1];
+        },
+
+      In(Group) *
+          (MembershipToken++[Idx] * T(Comma) * MembershipToken++[Item] *
+           T(IsIn) * MembershipToken++[ItemSeq] * End) >>
+        [](Match& _) {
+          return Membership << (Group << _[Idx]) << (Group << _[Item])
+                            << (Group << _[ItemSeq]);
+        },
+
+      In(Group) *
+          (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
+           MembershipToken++[Idx] * T(Comma) * MembershipToken++[Item] *
+           T(IsIn) * MembershipToken++[ItemSeq] * End) >>
+        [](Match& _) {
+          return Seq << _[Lhs] << _(Assign)
+                     << (Membership << (Group << _[Idx]) << (Group << _[Item])
+                                    << (Group << _[ItemSeq]));
+        },
+
+      In(Group) *
+          (MembershipToken[Head] * MembershipToken++[Tail] * T(IsIn)[IsIn] *
+           MembershipToken[Head1] * MembershipToken++[Tail1] *
+           End)[Membership] >>
+        [](Match& _) {
+          return Membership << Undefined << (Group << _(Head) << _[Tail])
+                            << (Group << _(Head1) << _[Tail1]);
+        },
+
+      In(Group) *
+          (MembershipToken++[Lhs] * (T(Assign) / T(Unify))[Assign] *
+           MembershipToken[Head] * MembershipToken++[Tail] * T(IsIn)[IsIn] *
+           MembershipToken[Head1] * MembershipToken++[Tail1] *
+           End)[Membership] >>
+        [](Match& _) {
+          return Seq << _[Lhs] << _(Assign)
+                     << (Membership << Undefined
+                                    << (Group << _(Head) << _[Tail])
+                                    << (Group << _(Head1) << _[Tail1]));
+        },
+
+      In(Group) *
+          (T(SomeDecl)
+           << ((T(VarSeq) << ((T(Group) << (GroundToken[Item] * End)) * End)) *
+               (T(Group) << (T(IsIn) * MembershipToken++[ItemSeq] * End)))) >>
+        [](Match& _) {
+          return Membership << Undefined << (Group << _(Item))
+                            << (Group << _[ItemSeq]);
+        },
+
+      // errors
+      In(Group) * T(Comma)[Comma] >>
+        [](Match& _) { return err(_(Comma), "invalid membership statement"); },
+
+      T(Group)[Group] << End >>
+        [](Match& _) { return err(_(Group), "Syntax error: empty group."); },
+    };
+  }
+}
\ No newline at end of file
diff --git a/src/passes/merge_data.cc b/src/passes/merge_data.cc
index 07d1e439..945904f1 100644
--- a/src/passes/merge_data.cc
+++ b/src/passes/merge_data.cc
@@ -1,19 +1,5 @@
 #include "passes.h"
 
-namespace
-{
-  using namespace rego;
-  using namespace wf::ops;
-
-  // clang-format off
-  inline const auto wfi =
-      (ObjectItem <<= Key * Term)
-    | (Data <<= ObjectItemSeq)
-    | (DataItem <<= Key * Term)
-    ;
-  // clang-format on
-}
-
 namespace rego
 {
   // Merges all the Data nodes in DataSeq into a single Data node.
@@ -30,15 +16,23 @@ namespace rego
           (T(DataItemSeq)[Lhs] * (T(Data) << T(ObjectItemSeq)[Rhs])) >>
         [](Match& _) { return DataItemSeq << *_[Lhs] << *_[Rhs]; },
 
-      (In(DataItemSeq) / In(DataObject)) *
+      In(DataItemSeq) *
           (T(ObjectItem)
-           << ((T(ObjectItemHead) << T(Scalar)[Scalar]) *
+           << ((T(Expr) << (T(Term) << T(Scalar)[Scalar])) *
                (T(Expr) << T(Term)[Term]))) >>
         [](Match& _) {
           std::string key = strip_quotes(to_json(_(Scalar)));
           return DataItem << (Key ^ key) << (DataTerm << _(Term)->front());
         },
 
+      In(DataObject) *
+          (T(ObjectItem)
+           << ((T(Expr) << T(Term)[Key]) * (T(Expr) << T(Term)[Val]))) >>
+        [](Match& _) {
+          return DataObjectItem << (DataTerm << _(Key)->front())
+                                << (DataTerm << _(Val)->front());
+        },
+
       In(DataTerm) * T(Array)[Array] >>
         [](Match& _) { return DataArray << *_[Array]; },
 
diff --git a/src/passes/modules.cc b/src/passes/modules.cc
index 932b751b..956a71de 100644
--- a/src/passes/modules.cc
+++ b/src/passes/modules.cc
@@ -5,8 +5,8 @@ namespace
 {
   using namespace rego;
 
-  const inline auto KeyToken =
-    T(Var) / T(Square) / T(Dot) / ScalarToken / T(RawString) / T(JSONString);
+  const inline auto KeyToken = T(Var) / T(Square) / T(Dot) / ScalarToken /
+    T(RawString) / T(JSONString) / T(Brace);
   const inline auto RefToken =
     T(Var) / T(Dot) / T(Square) / T(RawString) / T(JSONString);
   const inline auto ImportToken = T(Var) / T(Dot) / T(Square) / T(As);
diff --git a/src/passes/replace_argvals.cc b/src/passes/replace_argvals.cc
index 8393465e..c6e43ce6 100644
--- a/src/passes/replace_argvals.cc
+++ b/src/passes/replace_argvals.cc
@@ -54,6 +54,10 @@ namespace rego
 
           return Seq << newargs << body;
         },
+
+      // errors
+      In(Literal) * T(SomeExpr)[SomeExpr] >>
+        [](Match& _) { return err(_(SomeExpr), "Invalid some expression"); },
     };
   }
 }
\ No newline at end of file
diff --git a/src/passes/rulebody.cc b/src/passes/rulebody.cc
index 003c42a1..9ac03109 100644
--- a/src/passes/rulebody.cc
+++ b/src/passes/rulebody.cc
@@ -17,8 +17,7 @@ namespace
   inline const auto wfi =
       (Expr <<= (Val >>= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BoolInfix | AssignInfix))
     | (AssignArg <<= (Val >>= RefTerm | NumTerm | UnaryExpr | ArithInfix | Term | BoolInfix))
-    | (ObjectItem <<= Key * Expr)
-    | (RefObjectItem <<= RefTerm * Expr)
+    | (ObjectItem <<= (Key >>= Expr) * (Val >>= Expr))
     | (ObjectCompr <<= Var * NestedBody)
     | (ArrayCompr <<= Var * NestedBody)
     | (SetCompr <<= Var * NestedBody)
@@ -269,11 +268,35 @@ namespace rego
           return seq;
         },
 
-      // <array> = <ref>
+      // <array> = <ref>|<expr-call>
       (In(UnifyBody) / In(LiteralInit)) *
           (T(AssignInfix)
            << ((T(AssignArg) << (T(Term) << T(Array)[Lhs])) *
-               (T(AssignArg) << T(RefTerm)[RefTerm]))) >>
+               (T(AssignArg) << (T(RefTerm) / T(ExprCall))[RefTerm]))) >>
+        [](Match& _) {
+          LOG("<array> = <ref>");
+          Node seq = NodeDef::create(Seq);
+          Location temp = _.fresh({"ref"});
+          seq->push_back(Local << (Var ^ temp) << Undefined);
+          seq->push_back(UnifyExpr << (Var ^ temp) << (Expr << _(RefTerm)));
+          for (std::size_t i = 0; i < _(Lhs)->size(); i++)
+          {
+            Node index = Scalar << (JSONInt ^ std::to_string(i));
+            Node ref = SimpleRef << (Var ^ temp) << (RefArgBrack << index);
+            seq->push_back(
+              AssignInfix << (AssignArg << wfi / _(Lhs)->at(i) / Val)
+                          << (AssignArg << (RefTerm << ref)));
+          }
+
+          return seq;
+        },
+
+      // <array> = <ref>|<expr-call>
+      In(UnifyBody) *
+          (T(LiteralInit)
+           << (T(AssignInfix)
+               << ((T(AssignArg) << (T(Term) << T(Array)[Lhs])) *
+                   (T(AssignArg) << (T(RefTerm) / T(ExprCall))[RefTerm])))) >>
         [](Match& _) {
           LOG("<array> = <ref>");
           Node seq = NodeDef::create(Seq);
@@ -304,82 +327,58 @@ namespace rego
           return AssignInfix << _(Rhs) << _(Lhs);
         },
 
-      // <object> = <var>
-      (In(UnifyBody) / In(LiteralInit)) *
+      // <object> = <ref>
+      In(UnifyBody) *
           (T(AssignInfix)
            << ((T(AssignArg) << (T(Term) << T(Object)[Lhs])) *
-               (T(AssignArg) << (T(RefTerm) << T(Var)[Rhs])))) >>
+               (T(AssignArg) << T(RefTerm)[RefTerm]))) >>
         [](Match& _) {
-          LOG("<object> = <var>");
+          LOG("<object> = <ref>");
           Node seq = NodeDef::create(Seq);
+          Location temp = _.fresh({"ref"});
+          seq << (Local << (Var ^ temp) << Undefined)
+              << (UnifyExpr << (Var ^ temp) << (Expr << _(RefTerm)));
           for (const auto& item : *_(Lhs))
           {
-            if (item->type() == ObjectItem)
-            {
-              Node index;
-              if (item->type() == RefObjectItem)
-              {
-                Location key = _.fresh({"key"});
-                seq->push_back(Local << (Var ^ key) << Undefined);
-                seq->push_back(
-                  AssignInfix << (AssignArg << (RefTerm << (Var ^ key)))
-                              << (AssignArg << wfi / item / RefTerm));
-                index = RefTerm << (Var ^ key);
-              }
-              else
-              {
-                std::string key =
-                  std::string((wfi / item / Key)->location().view());
-                index = Scalar << (JSONString ^ key);
-              }
-              Node ref = SimpleRef << _(Rhs)->clone() << (RefArgBrack << index);
-              seq->push_back(
-                AssignInfix << (AssignArg << wfi / item / Expr / Val)
-                            << (AssignArg << (RefTerm << ref)));
-            }
-            else
-            {
-              seq->push_back(err(item, "Expected non-ref object item"));
-            }
+            Node index;
+            Location key = _.fresh({"key"});
+            seq << (Local << (Var ^ key) << Undefined)
+                << (UnifyExpr << (Var ^ key) << (wfi / item / Key));
+            index = RefTerm << (Var ^ key);
+
+            Node ref = SimpleRef << (Var ^ temp) << (RefArgBrack << index);
+            seq
+              << (AssignInfix << (AssignArg << (wfi / item / Val)->front())
+                              << (AssignArg << (RefTerm << ref)));
           }
 
           return seq;
         },
 
       // <object> = <ref>
-      (In(UnifyBody) / In(LiteralInit)) *
-          (T(AssignInfix)
-           << ((T(AssignArg) << (T(Term) << T(Object)[Lhs])) *
-               (T(AssignArg) << T(RefTerm)[RefTerm]))) >>
+      In(UnifyBody) *
+          (T(LiteralInit)
+           << (T(AssignInfix)
+               << ((T(AssignArg) << (T(Term) << T(Object)[Lhs])) *
+                   (T(AssignArg) << T(RefTerm)[RefTerm])))) >>
         [](Match& _) {
           LOG("<object> = <ref>");
           Node seq = NodeDef::create(Seq);
           Location temp = _.fresh({"ref"});
-          seq->push_back(Local << (Var ^ temp) << Undefined);
-          seq->push_back(UnifyExpr << (Var ^ temp) << (Expr << _(RefTerm)));
+          seq << (Local << (Var ^ temp) << Undefined)
+              << (UnifyExpr << (Var ^ temp) << (Expr << _(RefTerm)));
           for (const auto& item : *_(Lhs))
           {
             Node index;
-            if (item->type() == RefObjectItem)
-            {
-              Location key = _.fresh({"key"});
-              seq->push_back(Local << (Var ^ key) << Undefined);
-              seq->push_back(
-                AssignInfix << (AssignArg << (RefTerm << (Var ^ key)))
-                            << (AssignArg << wfi / item / RefTerm));
-              index = RefTerm << (Var ^ key);
-            }
-            else
-            {
-              std::string key =
-                std::string((wfi / item / Key)->location().view());
-              index = Scalar << (JSONString ^ key);
-            }
+            Location key = _.fresh({"key"});
+            seq << (Local << (Var ^ key) << Undefined)
+                << (UnifyExpr << (Var ^ key) << (wfi / item / Key));
+            index = RefTerm << (Var ^ key);
 
             Node ref = SimpleRef << (Var ^ temp) << (RefArgBrack << index);
-            seq->push_back(
-              AssignInfix << (AssignArg << wfi / item / Expr / Val)
-                          << (AssignArg << (RefTerm << ref)));
+            seq
+              << (AssignInfix << (AssignArg << (wfi / item / Val)->front())
+                              << (AssignArg << (RefTerm << ref)));
           }
 
           return seq;
@@ -486,12 +485,6 @@ namespace rego
           return err(_(BoolInfix), "Invalid boolean expression");
         },
 
-      (In(UnifyBody) / In(Expr)) * T(AssignInfix)[AssignInfix] >>
-        [](Match& _) { return err(_(AssignInfix), "Invalid assignment"); },
-
-      In(UnifyBody) * (T(LiteralInit) << T(AssignInfix)[AssignInfix]) >>
-        [](Match& _) { return err(_(AssignInfix), "Invalid assignment"); },
-
       (In(Term) / In(BinArg)) * T(SetCompr)[SetCompr] >>
         [](Match& _) { return err(_(SetCompr), "Invalid set comprehension"); },
 
@@ -504,6 +497,18 @@ namespace rego
         [](Match& _) {
           return err(_(ObjectCompr), "Invalid object comprehension");
         },
+
+      In(UnifyBody) * T(LiteralInit)[LiteralInit] >>
+        [](Match& _) { return err(_(LiteralInit), "Invalid assignment"); },
+
+      In(ObjectItem) * (T(Expr)[Expr] << T(AssignInfix)) >>
+        [](Match& _) { return err(_(Expr), "Invalid object item"); },
+
+      In(Expr) * T(AssignInfix)[AssignInfix] >>
+        [](Match& _) { return err(_(AssignInfix), "Invalid assignment"); },
+
+      In(UnifyBody) * T(AssignInfix)[AssignInfix] >>
+        [](Match& _) { return err(_(AssignInfix), "Invalid assignment"); },
     };
   }
 }
\ No newline at end of file
diff --git a/src/passes/skip_refs.cc b/src/passes/skip_refs.cc
index 4105a5d9..2b37d6e1 100644
--- a/src/passes/skip_refs.cc
+++ b/src/passes/skip_refs.cc
@@ -16,19 +16,20 @@ namespace
     std::string path;
   };
 
-  SkipPrefix skip_prefix_varseq(const SkipSet& skips, Node varseq)
+  SkipPrefix skip_prefix_varseq(
+    const SkipSet& skips, Node varseq, const BuiltIns& builtins)
   {
     Node head = varseq->front();
     std::string path = std::string(head->location().view());
     std::size_t skip = 0;
     std::string longest_path = "";
+
     for (std::size_t i = 1; i < varseq->size(); i++)
     {
       Node var = varseq->at(i);
       std::string var_str = std::string(var->location().view());
       path = path + "." + var_str;
-      Node test = Var ^ path;
-      if (skips->contains(path))
+      if (skips->contains(path) || builtins.is_builtin(path))
       {
         skip = i + 1;
         longest_path = path;
@@ -73,12 +74,34 @@ namespace
 
     return {skip, longest_path};
   }
+
+  void find_used_builtins(
+    const Node& node,
+    const BuiltIns& builtins,
+    std::set<std::string>& used_builtins)
+  {
+    if (node->type() == Var)
+    {
+      std::string path = std::string(node->location().view());
+      if (builtins.is_builtin(path))
+      {
+        used_builtins.insert(path);
+      }
+    }
+    else
+    {
+      for (auto& child : *node)
+      {
+        find_used_builtins(child, builtins, used_builtins);
+      }
+    }
+  }
 }
 
 namespace rego
 {
   // Elides references to skips for efficiency.
-  PassDef skip_refs()
+  PassDef skip_refs(const BuiltIns& builtins)
   {
     SkipSet skips = std::make_shared<std::set<std::string>>();
 
@@ -99,11 +122,11 @@ namespace rego
           return Ref << (RefHead << (Var ^ skip.path)) << refargseq;
         },
 
-      In(ExprCall) * T(VarSeq)[VarSeq]([skips](auto& n) {
-        return skip_prefix_varseq(skips, *n.first).length > 0;
+      In(ExprCall) * T(VarSeq)[VarSeq]([skips, builtins](auto& n) {
+        return skip_prefix_varseq(skips, *n.first, builtins).length > 0;
       }) >>
-        [skips](Match& _) {
-          SkipPrefix skip = skip_prefix_varseq(skips, _(VarSeq));
+        [skips, builtins](Match& _) {
+          SkipPrefix skip = skip_prefix_varseq(skips, _(VarSeq), builtins);
           Node varseq = (_(VarSeq));
           varseq->erase(varseq->begin(), varseq->begin() + skip.length);
 
@@ -123,6 +146,18 @@ namespace rego
       return 0;
     });
 
+    skip_refs.post(Rego, [skips, builtins](Node node) {
+      std::set<std::string> used_builtins;
+      find_used_builtins(node, builtins, used_builtins);
+      Node skipseq = node / SkipSeq;
+      for (auto& builtin : used_builtins)
+      {
+        skipseq << (Skip << (Key ^ builtin) << (BuiltInHook ^ builtin));
+      }
+
+      return 0;
+    });
+
     return skip_refs;
   }
 }
\ No newline at end of file
diff --git a/src/passes/skips.cc b/src/passes/skips.cc
index 03a063ef..7c842c7a 100644
--- a/src/passes/skips.cc
+++ b/src/passes/skips.cc
@@ -116,7 +116,7 @@ namespace
 namespace rego
 {
   // Discovers skip paths in the AST to constant DataTerm nodes.
-  PassDef skips(const BuiltIns& builtins)
+  PassDef skips()
   {
     SkipMap skip_links = std::make_shared<std::map<std::string, Node>>();
 
@@ -124,12 +124,6 @@ namespace rego
       dir::topdown | dir::once,
     };
 
-    for (auto [location, _] : builtins)
-    {
-      std::string name = std::string(location.view());
-      skip_links->insert({name, BuiltInHook ^ location});
-    }
-
     skips.pre(Rego, [skip_links](Node node) {
       find_skips(skip_links, "data", VarSeq << (Var ^ "data"), node / Data);
       find_skips(skip_links, "input", VarSeq << (Var ^ "input"), node / Input);
diff --git a/src/passes/structure.cc b/src/passes/structure.cc
index 9315ca62..d28cc3c6 100644
--- a/src/passes/structure.cc
+++ b/src/passes/structure.cc
@@ -49,28 +49,21 @@ namespace rego
       In(With) * (T(WithExpr) << T(Group)[Group]) >>
         [](Match& _) { return Expr << *_[Group]; },
 
+      In(Membership) * T(Group)[Group] >>
+        [](Match& _) { return Expr << *_[Group]; },
+
       In(ExprEvery) * (T(EverySeq) << T(Group)[Group]) >>
         [](Match& _) { return _(Group); },
 
       (In(ObjectItemSeq) / In(Object)) *
-          (T(ObjectItem) << T(Group)[ObjectItemHead] * T(Group)[Expr]) >>
+          (T(ObjectItem) << T(Group)[Key] * T(Group)[Val]) >>
         [](Match& _) {
-          return ObjectItem << (ObjectItemHead << _(ObjectItemHead))
-                            << (Expr << *_[Expr]);
+          return ObjectItem << (Expr << *_[Key]) << (Expr << *_[Val]);
         },
 
       (In(ArrayCompr) / In(ObjectCompr) / In(SetCompr)) * T(Group)[Group] >>
         [](Match& _) { return Expr << *_[Group]; },
 
-      In(ObjectItemHead) * (T(Group) << StringToken[String]) >>
-        [](Match& _) { return Scalar << (String << _(String)); },
-
-      In(ObjectItemHead) * (T(Group) << ScalarToken[Scalar]) >>
-        [](Match& _) { return Scalar << _(Scalar); },
-
-      In(ObjectItemHead) * (T(Group) << (T(Var) / T(Ref))[Ref]) >>
-        [](Match& _) { return _(Ref); },
-
       In(Package) * (T(Group) << (T(Var)[Var] * End)) >>
         [](Match& _) { return Ref << (RefHead << _(Var)) << RefArgSeq; },
 
@@ -187,8 +180,20 @@ namespace rego
       (In(Array) / In(Set)) * T(Group)[Group] >>
         [](Match& _) { return Expr << *_[Group]; },
 
-      In(UnifyBody) * (T(Group) << T(SomeDecl)[SomeDecl]) >>
-        [](Match& _) { return Literal << _(SomeDecl); },
+      In(UnifyBody) *
+          (T(Group)
+           << (T(SomeDecl) << (T(VarSeq)[VarSeq] * T(Group)[Group]))) >>
+        [](Match& _) {
+          Node maybe_with = _(Group)->back();
+          if (maybe_with->type() == With)
+          {
+            _(Group)->pop_back();
+            return LiteralWith
+              << (UnifyBody << (Literal << (SomeDecl << _(VarSeq) << _(Group))))
+              << (WithSeq << maybe_with);
+          }
+          return Literal << (SomeDecl << _(VarSeq) << _(Group));
+        },
 
       In(UnifyBody) * T(Group)[Group] >>
         [](Match& _) {
@@ -242,24 +247,39 @@ namespace rego
           (T(Group) << (T(Subtract) * (T(JSONInt) / T(JSONFloat))[Val])) >>
         [](Match& _) { return Term << (Scalar << Resolver::negate(_(Val))); },
 
-      In(SomeDecl) * (T(Group) << T(Undefined)) >>
-        [](Match&) { return InSome << Undefined; },
+      In(ExprEvery) * (T(Group) << (T(IsIn) * ExprToken++[Expr] * End)) >>
+        [](Match& _) { return IsIn << (Expr << _[Expr]); },
 
-      (In(SomeDecl) / In(ExprEvery)) *
-          (T(Group) << (T(InSome) * ExprToken++[Expr] * End)) >>
-        [](Match& _) { return InSome << (Expr << _[Expr]); },
+      In(Expr) * (T(UnifyBody) << (T(Group)[Group] * End)) >>
+        [](Match& _) { return Set << _(Group); },
 
-      In(VarSeq) * (T(Group) << (T(Var)[Var] * End)) >>
-        [](Match& _) { return _(Var); },
+      In(VarSeq) * (T(Group) << T(Var)[Var]) >> [](Match& _) { return _(Var); },
 
-      In(Expr) * T(InSome)[InSome] >>
-        [](Match& _) { return MemberOf ^ _(InSome)->location(); },
+      In(SomeDecl) * (T(VarSeq)[VarSeq] * (T(Group) << T(Undefined))) >>
+        [](Match& _) { return _(VarSeq); },
 
-      In(Expr) * (T(UnifyBody) << (T(Group)[Group] * End)) >>
-        [](Match& _) { return Set << _(Group); },
+      In(Literal) *
+          (T(SomeDecl)
+           << (T(VarSeq)[VarSeq] *
+               (T(Group) << (T(IsIn) * ExprToken++[Expr] * End)))) >>
+        [](Match& _) {
+          if (_(VarSeq)->size() == 1)
+          {
+            return SomeExpr << Undefined << _(VarSeq)->front()
+                            << (IsIn << (Expr << _[Expr]));
+          }
+
+          return SomeExpr << *_[VarSeq] << (IsIn << (Expr << _[Expr]));
+        },
+
+      In(SomeExpr) * T(Group)[Group] >>
+        [](Match& _) { return Expr << *_[Group]; },
 
       // errors
 
+      In(VarSeq) * T(Group)[Group] >>
+        [](Match& _) { return err(_(Group), "Expected a variable"); },
+
       In(Import) * T(ImportRef)[ImportRef] >>
         [](Match& _) { return err(_(ImportRef), "Invalid import reference"); },
 
@@ -269,19 +289,10 @@ namespace rego
       In(Package) * T(Group)[Group] >>
         [](Match& _) { return err(_(Group), "Invalid package name"); },
 
-      In(VarSeq) * T(Group)[Group] >>
-        [](Match& _) { return err(_(Group), "Invalid variable name"); },
-
       (In(ExprCall) / In(ExprEvery) / In(SomeDecl)) *
           (T(VarSeq)[VarSeq] << End) >>
         [](Match& _) { return err(_(VarSeq), "Missing variables"); },
 
-      In(SomeDecl) * T(Group)[Group] >>
-        [](Match& _) { return err(_(Group), "Invalid some declaration"); },
-
-      In(ObjectItemHead) * T(Group)[Group] >>
-        [](Match& _) { return err(_(Group), "Invalid object item key"); },
-
       In(DefaultRule) * T(Group)[Group] >>
         [](Match& _) { return err(_(Group), "Invalid default rule"); },
 
@@ -326,6 +337,12 @@ namespace rego
 
       In(Rego) * (T(Query)[Query] << End) >>
         [](Match& _) { return err(_(Query), "Must provide a query"); },
+
+      In(Expr) * T(IsIn)[IsIn] >>
+        [](Match& _) { return err(_(IsIn), "Invalid expression"); },
+
+      In(SomeDecl) * T(Group)[Group] >>
+        [](Match& _) { return err(_(Group), "Invalid some declaration"); },
     };
   }
 }
\ No newline at end of file
diff --git a/src/passes/symbols.cc b/src/passes/symbols.cc
index 5b9fa14d..24510bb2 100644
--- a/src/passes/symbols.cc
+++ b/src/passes/symbols.cc
@@ -145,26 +145,6 @@ namespace rego
           (T(Term) << (T(Scalar) / T(Array) / T(Object) / T(Set))[Val]) >>
         [](Match& _) { return ArgVal << _(Val); },
 
-      In(ObjectItem) * ((T(ObjectItemHead) << T(Scalar)[Scalar])) >>
-        [](Match& _) {
-          std::string key = strip_quotes(to_json(_(Scalar)));
-          return Key ^ key;
-        },
-
-      In(Object) *
-          (T(ObjectItem)
-           << ((T(ObjectItemHead) << T(Var)[Var]) * T(Expr)[Expr])) >>
-        [](Match& _) {
-          return RefObjectItem << (RefTerm << _(Var)) << _(Expr);
-        },
-
-      In(Object) *
-          (T(ObjectItem)
-           << ((T(ObjectItemHead) << T(Ref)[Ref]) * T(Expr)[Expr])) >>
-        [](Match& _) {
-          return RefObjectItem << (RefTerm << _(Ref)) << _(Expr);
-        },
-
       In(Expr) * (T(Expr) << (T(Term)[Term] * End)) >>
         [](Match& _) { return _(Term); },
 
@@ -180,10 +160,7 @@ namespace rego
       In(RefArgBrack) * T(Var)[Var] >>
         [](Match& _) { return RefTerm << _(Var); },
 
-      In(UnifyBody) *
-          (T(Literal)
-           << (T(SomeDecl)
-               << (T(VarSeq)[VarSeq] * (T(InSome) << T(Undefined))))) >>
+      In(UnifyBody) * (T(Literal) << (T(SomeDecl) << T(VarSeq)[VarSeq])) >>
         [](Match& _) {
           Node seq = NodeDef::create(Seq);
           for (auto& var : *_(VarSeq))
@@ -195,16 +172,16 @@ namespace rego
 
       In(UnifyBody) *
           (T(Literal)
-           << (T(SomeDecl)
-               << ((T(VarSeq) << (T(Var)[Val] * End)) *
-                   (T(InSome) << T(Expr)[Expr])))) >>
+           << (T(SomeExpr)
+               << (T(Undefined) * T(Expr)[Item] *
+                   (T(IsIn) << T(Expr)[ItemSeq])))) >>
         [](Match& _) {
           Location item = _.fresh({"item"});
           return Seq << (Local << (Var ^ item) << Undefined)
-                     << (LiteralEnum << (Var ^ item) << _(Expr))
+                     << (LiteralEnum << (Var ^ item) << _(ItemSeq))
                      << (Literal
                          << (Expr
-                             << (RefTerm << _(Val)->clone()) << Unify
+                             << _(Item) << Unify
                              << (RefTerm
                                  << (Ref << (RefHead << (Var ^ item))
                                          << (RefArgSeq
@@ -216,7 +193,7 @@ namespace rego
       In(Expr) *
           (T(ExprEvery)([](auto& n) { return is_in(*n.first, {UnifyBody}); })
            << ((T(VarSeq) << (T(Var)[Val] * End)) * T(UnifyBody)[UnifyBody] *
-               (T(InSome) << T(Expr)[Expr]))) >>
+               (T(IsIn) << T(Expr)[Expr]))) >>
         [](Match& _) {
           Location item = _.fresh({"item"});
           Location every = _.fresh({"every"});
@@ -238,18 +215,16 @@ namespace rego
 
       In(UnifyBody) *
           (T(Literal)
-           << (T(SomeDecl)
-               << ((T(VarSeq) << (T(Var)[Idx] * T(Var)[Val] * End)) *
-                   (T(InSome) << T(Expr)[Expr])))) >>
+           << (T(SomeExpr)
+               << (T(Expr)[Idx] * T(Expr)[Item] *
+                   (T(IsIn) << T(Expr)[ItemSeq])))) >>
         [](Match& _) {
           Location item = _.fresh({"item"});
           return Seq << (Local << (Var ^ item) << Undefined)
-                     << (LiteralEnum << (Var ^ item) << _(Expr))
-                     << (Local << _(Idx) << Undefined)
-                     << (Local << _(Val) << Undefined)
+                     << (LiteralEnum << (Var ^ item) << _(ItemSeq))
                      << (Literal
                          << (Expr
-                             << (RefTerm << _(Idx)->clone()) << Unify
+                             << _(Idx) << Unify
                              << (RefTerm
                                  << (Ref << (RefHead << (Var ^ item))
                                          << (RefArgSeq
@@ -258,7 +233,7 @@ namespace rego
                                                      << (JSONInt ^ "0"))))))))
                      << (Literal
                          << (Expr
-                             << (RefTerm << _(Val)->clone()) << Unify
+                             << _(Item) << Unify
                              << (RefTerm
                                  << (Ref << (RefHead << (Var ^ item))
                                          << (RefArgSeq
@@ -270,7 +245,7 @@ namespace rego
       In(Expr) *
           (T(ExprEvery)([](auto& n) { return is_in(*n.first, {UnifyBody}); })
            << ((T(VarSeq) << (T(Var)[Idx] * T(Var)[Val] * End)) *
-               T(UnifyBody)[UnifyBody] * (T(InSome) << T(Expr)[Expr]))) >>
+               T(UnifyBody)[UnifyBody] * (T(IsIn) << T(Expr)[Expr]))) >>
         [](Match& _) {
           Location item = _.fresh({"item"});
           Location every = _.fresh({"every"});
@@ -365,6 +340,13 @@ namespace rego
                 << (Expr << (RefTerm << (Var ^ value)) << Unify << *_[Expr]));
         },
 
+      (In(ObjectCompr) / In(SetCompr) / In(ArrayCompr)) *
+          T(UnifyBody)[UnifyBody] >>
+        [](Match& _) {
+          Location compr = _.fresh({"compr"});
+          return NestedBody << (Key ^ compr) << _(UnifyBody);
+        },
+
       // errors
 
       In(UnifyBody) * (T(RuleHead)[RuleHead] << (T(Var) * T(RuleHeadFunc))) >>
@@ -372,9 +354,6 @@ namespace rego
           return err(_(RuleHead), "No rule functions allowed in rule bodies");
         },
 
-      In(ObjectItem) * T(ObjectItemHead)[ObjectItemHead] >>
-        [](Match& _) { return err(_(ObjectItemHead), "Invalid object key"); },
-
       In(Term) * T(Ref)[Ref] >>
         [](Match& _) { return err(_(Ref), "Invalid ref term"); },
 
@@ -396,10 +375,10 @@ namespace rego
       In(Expr) * T(Dot)[Dot] >>
         [](Match& _) { return err(_(Dot), "Invalid dot expression"); },
 
-      In(ExprEvery) * (T(InSome)[InSome] << T(Undefined)) >>
+      In(ExprEvery) * (T(IsIn)[IsIn] << T(Undefined)) >>
         [](Match& _) {
           return err(
-            _(InSome),
+            _(IsIn),
             "Every statement requires an explicit sequence to evaluate.");
         },
 
diff --git a/src/passes/unify.cc b/src/passes/unify.cc
index 17b1f2df..4f31ae57 100644
--- a/src/passes/unify.cc
+++ b/src/passes/unify.cc
@@ -45,10 +45,6 @@ namespace rego
           [](Match& _) { return Object << *_[DataObject]; },
         In(Object) * T(DataItem)[DataItem] >>
           [](Match& _) { return ObjectItem << *_[DataItem]; },
-
-        // errors
-        T(Query)[Query] << End >>
-          [](Match& _) { return err(_(Query), "no values in query"); },
       }};
 
     unify.pre(Rego, [builtins](Node node) {
diff --git a/src/resolver.cc b/src/resolver.cc
index 39b08b81..f9326960 100644
--- a/src/resolver.cc
+++ b/src/resolver.cc
@@ -1,5 +1,6 @@
 #include "resolver.h"
 
+#include "errors.h"
 #include "lang.h"
 #include "unifier.h"
 
@@ -21,7 +22,7 @@ namespace
     | (Term <<= Scalar | Array | Object | Set)
     | (Scalar <<= JSONString | JSONInt | JSONFloat | JSONTrue | JSONFalse | JSONNull)
     | (ArgVal <<= Scalar | Array | Object | Set)
-    | (ObjectItem <<= Key * Term)
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
     | (ArgVar <<= Var * (Val >>= Term | Undefined))
     | (RuleFunc <<= Var * RuleArgs * (Body >>= UnifyBody) * (Val >>= UnifyBody))
     | (Function <<= JSONString * ArgSeq)
@@ -63,7 +64,7 @@ namespace
       In case we need integer division in the future.
       if (rhs.is_zero())
       {
-        return err(op, "divide by zero", "eval_builtin_error");
+        return err(op, "divide by zero", EvalBuiltInError);
       }
 
       value = lhs / rhs;
@@ -73,7 +74,7 @@ namespace
     {
       if (rhs.is_zero())
       {
-        return err(op, "modulo by zero", "eval_builtin_error");
+        return err(op, "modulo by zero", EvalBuiltInError);
       }
 
       value = lhs % rhs;
@@ -105,14 +106,14 @@ namespace
     {
       if (rhs == 0.0)
       {
-        return err(op, "divide by zero", "eval_builtin_error");
+        return err(op, "divide by zero", EvalBuiltInError);
       }
 
       value = lhs / rhs;
     }
     else if (op->type() == Modulo)
     {
-      return err(op, "modulo on floating-point number", "eval_builtin_error");
+      return err(op, "modulo on floating-point number", EvalBuiltInError);
     }
     else
     {
@@ -281,8 +282,12 @@ namespace rego
 
   std::string Resolver::get_string(const Node& node)
   {
-    assert(node->type() == JSONString);
-    return strip_quotes(std::string(node->location().view()));
+    if (node->type() == JSONString)
+    {
+      return strip_quotes(std::string(node->location().view()));
+    }
+
+    return std::string(node->location().view());
   }
 
   Node Resolver::scalar(const std::string& value)
@@ -331,7 +336,7 @@ namespace rego
   {
     if (lhs->type() == Undefined || rhs->type() == Undefined)
     {
-      return JSONFalse;
+      return JSONFalse ^ "false";
     }
 
     if (lhs->type() == Error)
@@ -373,8 +378,7 @@ namespace rego
 
       if (maybe_lhs_number.has_value() && maybe_rhs_set.has_value())
       {
-        return err(
-          rhs, "operand 2 must be number but got set", "eval_type_error");
+        return err(rhs, "operand 2 must be number but got set", EvalTypeError);
       }
 
       return err(
@@ -417,7 +421,7 @@ namespace rego
   {
     if (lhs->type() == Undefined || rhs->type() == Undefined)
     {
-      return JSONFalse;
+      return JSONFalse ^ "false";
     }
 
     if (lhs->type() == Error)
@@ -547,6 +551,26 @@ namespace rego
     return std::nullopt;
   }
 
+  std::optional<Node> Resolver::maybe_unwrap_array(const Node& node)
+  {
+    Node value;
+    if (node->type() == Term || node->type() == DataTerm)
+    {
+      value = node->front();
+    }
+    else
+    {
+      value = node;
+    }
+
+    if (value->type() == Array)
+    {
+      return value;
+    }
+
+    return std::nullopt;
+  }
+
   std::optional<Nodes> Resolver::apply_access(
     const Node& container, const Node& arg)
   {
@@ -618,7 +642,7 @@ namespace rego
         }
         else if (def->type() == ObjectItem)
         {
-          nodes.push_back(wfi / def / Term);
+          nodes.push_back(wfi / def / Val);
         }
         else if (def->type() == Submodule)
         {
@@ -655,11 +679,18 @@ namespace rego
   Node Resolver::object(const Node& object_items)
   {
     Node object = NodeDef::create(Object);
+    std::map<std::string, Node> items;
     for (std::size_t i = 0; i < object_items->size(); i += 2)
     {
-      std::string key_str = strip_quotes(to_json(object_items->at(i)));
-      object->push_back(
-        ObjectItem << (Key ^ key_str) << object_items->at(i + 1));
+      std::string key = to_json(object_items->at(i));
+      Node item = ObjectItem << object_items->at(i) << object_items->at(i + 1);
+      items[key] = item;
+    }
+
+    // objects need to be created with sorted keys
+    for (auto [_, item] : items)
+    {
+      object->push_back(item);
     }
 
     return object;
@@ -1068,6 +1099,11 @@ namespace rego
 
   bool Resolver::is_falsy(const Node& node)
   {
+    if (node->type() == Undefined)
+    {
+      return true;
+    }
+
     if (node->type() != Term)
     {
       return false;
@@ -1098,7 +1134,7 @@ namespace rego
         defs.begin(),
         defs.end(),
         std::back_inserter(terms),
-        [](const Node& def) { return wfi / def / Term; });
+        [](const Node& def) { return wfi / def / Val; });
       return terms;
     }
 
@@ -1115,7 +1151,7 @@ namespace rego
 
       if (key_str == query_str)
       {
-        terms.push_back(wfi / object_item / Term);
+        terms.push_back(wfi / object_item / Val);
       }
     }
 
@@ -1230,11 +1266,220 @@ namespace rego
       }
     }
 
-    if (result->size() == 0)
+    return result;
+  }
+
+  Node Resolver::membership(
+    const Node& index, const Node& item, const Node& itemseq)
+  {
+    Node items = itemseq;
+    if (items->type() == Term)
     {
-      result->push_back(Undefined);
+      items = items->front();
     }
 
-    return result;
+    std::vector<std::string> indices;
+    if (items->type() == Array || items->type() == Set)
+    {
+      indices = array_find(items, to_json(item));
+    }
+    else if (items->type() == Object)
+    {
+      indices = object_find(items, to_json(item));
+    }
+    else
+    {
+      return JSONFalse ^ "false";
+    }
+
+    std::string index_str = to_json(index);
+    for (auto& i : indices)
+    {
+      if (i == index_str)
+      {
+        return JSONTrue ^ "true";
+      }
+    }
+
+    return JSONFalse ^ "false";
+  }
+
+  Node Resolver::membership(const Node& item, const Node& itemseq)
+  {
+    Node items = itemseq;
+    if (items->type() == Term)
+    {
+      items = items->front();
+    }
+
+    std::vector<std::string> indices;
+    if (items->type() == Array || items->type() == Set)
+    {
+      indices = array_find(items, to_json(item));
+    }
+    else if (items->type() == Object)
+    {
+      indices = object_find(items, to_json(item));
+    }
+    else
+    {
+      return JSONFalse ^ "false";
+    }
+
+    if (indices.size() > 0)
+    {
+      return JSONTrue ^ "True";
+    }
+
+    return JSONFalse ^ "false";
+  }
+
+  std::vector<std::string> Resolver::array_find(
+    const Node& array, const std::string& search)
+  {
+    std::vector<std::string> indices;
+    for (std::size_t i = 0; i < array->size(); ++i)
+    {
+      Node item = array->at(i);
+      if (to_json(item) == search)
+      {
+        indices.push_back(std::to_string(i));
+      }
+    }
+
+    return indices;
+  }
+
+  std::vector<std::string> Resolver::object_find(
+    const Node& object, const std::string& search)
+  {
+    std::vector<std::string> indices;
+    for (auto& objectitem : *object)
+    {
+      if (to_json(objectitem / Val) == search)
+      {
+        indices.push_back(to_json(objectitem / Key));
+      }
+    }
+
+    return indices;
+  }
+
+  std::string Resolver::NodePrinter::str() const
+  {
+    std::ostringstream buf;
+    buf << *this;
+    return buf.str();
+  }
+
+  Node Resolver::unwrap(
+    const Node& node,
+    const Token& type,
+    const std::string& error_prefix,
+    const std::string& error_code,
+    bool specify_number)
+  {
+    Node value;
+    if (node->type() == Term || node->type() == DataTerm)
+    {
+      value = node->front();
+    }
+    else
+    {
+      value = node;
+    }
+
+    if (value->type() == type)
+    {
+      return value;
+    }
+
+    if (value->type() == Scalar)
+    {
+      value = value->front();
+      if (value->type() == type)
+      {
+        return value;
+      }
+    }
+
+    std::ostringstream error;
+    error << error_prefix << "must be " << type_name(type, specify_number)
+          << " but got " << type_name(value->type(), specify_number);
+    return err(node, error.str(), error_code);
+  }
+
+  std::optional<Node> Resolver::maybe_unwrap(
+    const Node& node, const std::set<Token>& types)
+  {
+    Node value;
+    if (node->type() == Term || node->type() == DataTerm)
+    {
+      value = node->front();
+    }
+    else
+    {
+      value = node;
+    }
+
+    if (types.contains(value->type()))
+    {
+      return value;
+    }
+
+    if (value->type() == Scalar)
+    {
+      value = value->front();
+      if (types.contains(value->type()))
+      {
+        return value;
+      }
+    }
+
+    return std::nullopt;
+  }
+
+  std::string Resolver::type_name(const Token& type, bool specify_number)
+  {
+    if (type == JSONInt)
+    {
+      if (specify_number)
+      {
+        return "integer number";
+      }
+      return "number";
+    }
+
+    if (type == JSONFloat)
+    {
+      if (specify_number)
+      {
+        return "floating-point number";
+      }
+      return "number";
+    }
+
+    if (type == JSONString)
+    {
+      return "string";
+    }
+
+    return std::string(type.str());
+  }
+
+  std::string Resolver::type_name(const Node& node, bool specify_number)
+  {
+    Node value = node;
+    if (value->type() == Term)
+    {
+      value = value->front();
+    }
+
+    if (value->type() == Scalar)
+    {
+      value = value->front();
+    }
+
+    return type_name(value->type(), specify_number);
   }
 }
\ No newline at end of file
diff --git a/src/unifier.cc b/src/unifier.cc
index 0efbadcb..ee32765e 100644
--- a/src/unifier.cc
+++ b/src/unifier.cc
@@ -27,7 +27,7 @@ namespace
     | (RuleSet <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term))
     | (RuleObj <<= Var * (Body >>= UnifyBody | Empty) * (Val >>= UnifyBody | Term))
     | (Function <<= JSONString * ArgSeq)
-    | (ObjectItem <<= Key * Term)
+    | (ObjectItem <<= (Key >>= Term) * (Val >>= Term))
     | (Term <<= Scalar | Array | Object | Set)
     | (ArgVar <<= Var * Term)
     | (Binding <<= Var * Term)
@@ -40,34 +40,6 @@ namespace rego
 {
   using namespace wf::ops;
 
-  std::ostream& operator<<(std::ostream& os, const Unifier& unifier)
-  {
-    for (const auto& [first, second] : unifier->m_variables)
-    {
-      os << first.view() << "(" << second.dependency_score() << ") -> {";
-      std::string sep = "";
-      for (const auto& dep : second.dependencies())
-      {
-        os << sep << dep.view();
-        sep = ", ";
-      }
-      os << "}" << std::endl;
-    }
-    os << std::endl;
-    for (const auto& unifyexpr : unifier->m_statements)
-    {
-      if (unifyexpr->type() == UnifyExprWith)
-      {
-        os << Resolver::with_str(unifyexpr) << std::endl;
-      }
-      else
-      {
-        os << Resolver::expr_str(unifyexpr) << std::endl;
-      }
-    }
-    return os;
-  }
-
   UnifierDef::UnifierDef(
     const Location& rule,
     const Node& rulebody,
@@ -82,44 +54,135 @@ namespace rego
     m_parent_type(rulebody->parent()->type()),
     m_cache(cache)
   {
-    init_from_body(rulebody, m_statements);
+    LOG_HEADER("ASSEMBLING UNIFICATION", "---");
+    m_dependency_graph.push_back({"start", {}, 0});
+    init_from_body(rulebody, m_statements, 0);
     compute_dependency_scores();
-    m_retries = Variable::detect_cycles(m_variables);
+    m_retries = detect_cycles();
     if (m_retries > 0)
     {
       LOG("Detected ", m_retries, " cycles in dependency graph");
     }
+
+    LOG("Dependency graph:");
+    LOG_INDENT();
+    for (auto& dep : m_dependency_graph)
+    {
+      std::ostringstream os;
+      os << "[" << dep.name << "](" << dep.score << ") -> {";
+      std::string sep = "";
+      for (auto id : dep.dependencies)
+      {
+        os << sep << m_dependency_graph[id].name;
+        sep = ", ";
+      }
+      os << "}";
+      LOG(os.str());
+    }
+    LOG_UNINDENT();
+    LOG_HEADER("ASSEMBLY COMPLETE", "---");
+  }
+
+  std::size_t UnifierDef::detect_cycles() const
+  {
+    std::size_t cycles = 0;
+    for (std::size_t id = 0; id < m_dependency_graph.size(); ++id)
+    {
+      if (has_cycle(id))
+      {
+        ++cycles;
+      }
+    }
+
+    return cycles;
+  }
+
+  bool UnifierDef::has_cycle(std::size_t id) const
+  {
+    std::set<std::size_t> visited;
+
+    auto deps = m_dependency_graph[id].dependencies;
+    std::vector<std::size_t> stack(deps.begin(), deps.end());
+    while (!stack.empty())
+    {
+      auto current = stack.back();
+      stack.pop_back();
+
+      if (current == id)
+      {
+        return true;
+      }
+
+      if (visited.contains(current))
+      {
+        continue;
+      }
+
+      visited.insert(current);
+
+      for (const auto& dep : m_dependency_graph[current].dependencies)
+      {
+        stack.push_back(dep);
+      }
+    }
+
+    return false;
   }
 
   void UnifierDef::init_from_body(
-    const Node& rulebody, std::vector<Node>& statements)
+    const Node& rulebody, std::vector<Node>& statements, std::size_t root)
   {
     std::for_each(rulebody->begin(), rulebody->end(), [&](const auto& stmt) {
       if (stmt->type() == Local)
       {
-        add_variable(stmt);
+        std::size_t id = add_variable(stmt);
+        m_dependency_graph[id].dependencies.insert(root);
       }
       else if (stmt->type() == UnifyExpr)
       {
         statements.push_back(stmt);
-        add_unifyexpr(stmt);
+        std::size_t id = add_unifyexpr(stmt);
+        m_dependency_graph[id].dependencies.insert(root);
       }
       else if (stmt->type() == UnifyExprWith)
       {
         statements.push_back(stmt);
+        std::size_t id = m_dependency_graph.size();
+        std::string name = "with" + std::to_string(id);
+        m_dependency_graph.push_back({name, {root}, 0});
+        m_expr_ids[stmt] = id;
         m_nested_statements[stmt] = {};
-        init_from_body(stmt / UnifyBody, m_nested_statements[stmt]);
+        init_from_body(stmt / UnifyBody, m_nested_statements[stmt], id);
+
+        Node withseq = stmt / WithSeq;
+        std::vector<std::size_t> dep_ids;
+        for (auto& with : *withseq)
+        {
+          std::vector<Location> deps;
+          scan_vars(wfi / with / Var, deps);
+          std::transform(
+            deps.begin(),
+            deps.end(),
+            std::back_inserter(dep_ids),
+            [this](auto& dep) { return m_variables.at(dep).id(); });
+        }
+
+        m_dependency_graph[id].dependencies.insert(
+          dep_ids.begin(), dep_ids.end());
       }
     });
   }
 
-  void UnifierDef::add_variable(const Node& local)
+  std::size_t UnifierDef::add_variable(const Node& local)
   {
     auto name = (wfi / local / Var)->location();
-    m_variables.insert({name, Variable(local)});
+    std::size_t id = m_dependency_graph.size();
+    m_variables.insert({name, Variable(local, id)});
+    m_dependency_graph.push_back({std::string(name.view()), {}, 0});
+    return id;
   }
 
-  void UnifierDef::add_unifyexpr(const Node& unifyexpr)
+  std::size_t UnifierDef::add_unifyexpr(const Node& unifyexpr)
   {
     Node lhs = wfi / unifyexpr / Var;
     Node rhs = wfi / unifyexpr / Val;
@@ -132,22 +195,51 @@ namespace rego
 
     Variable& var = get_variable(lhs->location());
     std::vector<Location> deps;
-    std::size_t num_vars = scan_vars(rhs, deps);
-    var.increase_dependency_score(num_vars - deps.size());
-    var.insert_dependencies(deps.begin(), deps.end());
+    scan_vars(rhs, deps);
+    std::vector<std::size_t> dep_ids;
+    std::transform(
+      deps.begin(), deps.end(), std::back_inserter(dep_ids), [this](auto& dep) {
+        return m_variables.at(dep).id();
+      });
+    std::size_t expr_id = m_dependency_graph.size();
+    m_expr_ids[unifyexpr] = expr_id;
+    std::string name = Resolver::expr_str(unifyexpr).str();
+    m_dependency_graph.push_back(
+      {name, std::set(dep_ids.begin(), dep_ids.end()), 0});
+    m_dependency_graph[var.id()].dependencies.insert(expr_id);
+    return expr_id;
+  }
+
+  std::size_t UnifierDef::compute_dependency_score(
+    std::size_t id, std::set<std::size_t>& visited)
+  {
+    if (visited.contains(id))
+    {
+      return m_dependency_graph[id].score;
+    }
+
+    visited.insert(id);
+    std::size_t score = 1;
+    for (auto dep_id : m_dependency_graph[id].dependencies)
+    {
+      score += compute_dependency_score(dep_id, visited);
+    }
+
+    m_dependency_graph[id].score = score;
+    return score;
   }
 
   void UnifierDef::compute_dependency_scores()
   {
-    std::set<Location> visited;
-    for (auto& [_, variable] : m_variables)
+    std::set<std::size_t> visited;
+    for (std::size_t id = 0; id < m_dependency_graph.size(); ++id)
     {
-      variable.dependency_score(compute_dependency_score(variable, visited));
+      compute_dependency_score(id, visited);
     }
 
     std::sort(
       m_statements.begin(), m_statements.end(), [this](auto& lhs, auto& rhs) {
-        return compute_dependency_score(lhs) < compute_dependency_score(rhs);
+        return dependency_score(lhs) < dependency_score(rhs);
       });
 
     for (auto& [_, nested_statements] : m_nested_statements)
@@ -156,31 +248,19 @@ namespace rego
         nested_statements.begin(),
         nested_statements.end(),
         [this](auto& lhs, auto& rhs) {
-          return compute_dependency_score(lhs) < compute_dependency_score(rhs);
+          return dependency_score(lhs) < dependency_score(rhs);
         });
     }
   }
 
-  std::size_t UnifierDef::compute_dependency_score(
-    Variable& variable, std::set<Location>& visited)
+  std::size_t UnifierDef::dependency_score(const Node& expr) const
   {
-    if (visited.contains(variable.name()))
-    {
-      return get_variable(variable.name()).dependency_score();
-    }
-
-    visited.insert(variable.name());
-    std::size_t score = std::transform_reduce(
-      variable.dependencies().cbegin(),
-      variable.dependencies().cend(),
-      variable.dependency_score(),
-      std::plus{},
-      [&](auto& dep) {
-        return compute_dependency_score(get_variable(dep), visited);
-      });
-    variable.dependency_score(score);
+    return m_dependency_graph[m_expr_ids.at(expr)].score;
+  }
 
-    return score;
+  std::size_t UnifierDef::dependency_score(const Variable& var) const
+  {
+    return m_dependency_graph[var.id()].score;
   }
 
   bool UnifierDef::is_local(const Node& var)
@@ -263,20 +343,6 @@ namespace rego
     execute_statements(m_statements.begin(), m_statements.end());
   }
 
-  void UnifierDef::mark_invalid_values()
-  {
-    for (auto& [_, var] : m_variables)
-    {
-      if (!var.is_unify())
-      {
-        // only unification statements can mark values as invalid
-        continue;
-      }
-
-      var.mark_invalid_values();
-    }
-  }
-
   void UnifierDef::remove_invalid_values()
   {
     std::for_each(m_variables.begin(), m_variables.end(), [](auto& pair) {
@@ -300,9 +366,9 @@ namespace rego
 
         Node node = var.bind();
 
-        if (node->type() == Error || node->type() == Undefined)
+        if (node->type() == Error)
         {
-          LOG("> ", var.name().view(), ": Undefined or Error");
+          LOG("> ", var.name().view(), ": Error");
           return node;
         }
 
@@ -327,7 +393,12 @@ namespace rego
             return JSONTrue ^ "true";
           }
         }
-        else if (result->type() == Undefined)
+        else if (var.is_unify() && Resolver::is_falsy(node))
+        {
+          LOG("> ", var.name().view(), ": false => false");
+          return JSONFalse ^ "false";
+        }
+        else if (node->type() == Term && result->type() == Undefined)
         {
           LOG("> ", var.name().view(), ": Term => true");
           return JSONTrue ^ "true";
@@ -357,7 +428,7 @@ namespace rego
     {
       LOG_HEADER("Pass " + std::to_string(pass_index), "=====");
       pass();
-      mark_invalid_values();
+      // mark_invalid_values();
       remove_invalid_values();
     }
 
@@ -399,8 +470,26 @@ namespace rego
   Values UnifierDef::evaluate_function(
     const Location& var, const std::string& func_name, const Args& args)
   {
-    LOG("> calling ", func_name, " with ", args);
     Values values;
+    if (args.size() == 0)
+    {
+      LOG("> calling ", func_name, " with no args");
+      if (func_name == "array")
+      {
+        values.push_back(ValueDef::create(NodeDef::create(Array)));
+      }
+      else if (func_name == "object")
+      {
+        values.push_back(ValueDef::create(NodeDef::create(Object)));
+      }
+      else if (func_name == "set")
+      {
+        values.push_back(ValueDef::create(NodeDef::create(Set)));
+      }
+      return values;
+    }
+
+    LOG("> calling ", func_name, " with ", args);
     std::set<Value> valid_args;
     for (std::size_t i = 0; i < args.size(); ++i)
     {
@@ -705,6 +794,17 @@ namespace rego
         values.push_back(ValueDef::create(var, JSONTrue, sources));
       }
     }
+    else if (func_name == "membership-tuple")
+    {
+      Node result =
+        Resolver::membership(args[0]->node(), args[1]->node(), args[2]->node());
+      values.push_back(ValueDef::create(var, result, sources));
+    }
+    else if (func_name == "membership-single")
+    {
+      Node result = Resolver::membership(args[0]->node(), args[1]->node());
+      values.push_back(ValueDef::create(var, result, sources));
+    }
     else if (func_name == "apply_access")
     {
       Node container = args[0]->node();
@@ -880,11 +980,8 @@ namespace rego
       {
         for (const Node& object_item : *container)
         {
-          std::string key =
-            std::string((wfi / object_item / Key)->location().view());
-          Node key_str = (Scalar << (JSONString ^ key));
-          Node tuple = Term << (Array << key_str << wfi / object_item / Term);
-
+          Node tuple = Term
+            << (Array << wfi / object_item / Key << wfi / object_item / Val);
           items.push_back(ValueDef::create(var, tuple));
         }
       }
@@ -893,8 +990,6 @@ namespace rego
       {
         for (const Node& value : *container)
         {
-          std::string key = to_json(value);
-          Node key_str = (Scalar << (JSONString ^ key));
           Node tuple = Term << (Array << value << value);
           items.push_back(ValueDef::create(var, tuple));
         }
@@ -916,70 +1011,6 @@ namespace rego
     return buf.str();
   }
 
-  std::string UnifierDef::dependency_str() const
-  {
-    std::ostringstream buf;
-    for (auto& [name, var] : m_variables)
-    {
-      buf << name.view() << "(" << var.dependency_score() << ") -> {";
-      std::string sep = "";
-      for (const auto& dep : var.dependencies())
-      {
-        buf << sep << dep.view();
-        sep = ", ";
-      }
-      buf << "}" << std::endl;
-    }
-
-    return buf.str();
-  }
-
-  std::size_t UnifierDef::compute_dependency_score(const Node& unifyexpr)
-  {
-    if (unifyexpr->type() == Local)
-    {
-      return 0;
-    }
-
-    std::size_t score = 0;
-
-    if (unifyexpr->type() == UnifyExprWith)
-    {
-      Node body = unifyexpr / UnifyBody;
-      for (auto& child : *body)
-      {
-        if (child->type() != Local)
-        {
-          score += compute_dependency_score(child);
-        }
-      }
-      Node withseq = unifyexpr / WithSeq;
-      for (auto& with : *withseq)
-      {
-        std::vector<Location> deps;
-        std::size_t num_vars = scan_vars(wfi / with / Var, deps);
-        score += num_vars - deps.size();
-        for (auto& dep : deps)
-        {
-          score += get_variable(dep).dependency_score();
-        }
-      }
-    }
-    else
-    {
-      std::vector<Location> deps;
-      std::size_t num_vars = scan_vars(wfi / unifyexpr / Val, deps);
-      score += num_vars - deps.size();
-
-      for (auto& dep : deps)
-      {
-        score += get_variable(dep).dependency_score();
-      }
-    }
-
-    return score;
-  }
-
   bool UnifierDef::push_rule(const Location& rule)
   {
     LOG("Pushing rule: ", rule.view());
@@ -1016,35 +1047,81 @@ namespace rego
   Values UnifierDef::check_with(const Node& var)
   {
     std::string key_str = std::string(var->location().view());
+
+    Values result;
+    std::map<std::string, Values> partials;
     for (auto it = m_with_stack->rbegin(); it != m_with_stack->rend(); ++it)
     {
-      if (it->contains(key_str))
+      for (auto& [key, val] : *it)
       {
-        LOG("Found key: ", key_str, " in with stack");
-        Values values = it->at(key_str);
-        if (m_parent_type == RuleFunc && m_builtins.is_builtin(key_str))
+        if (key == key_str)
         {
-          if (
-            std::find_if(values.begin(), values.end(), [&](const Value& value) {
-              Node node = value->node();
-              if (node->type() == RuleFunc)
-              {
-                return (node / Var)->location() == m_rule;
-              }
-
-              return false;
-            }) != values.end())
+          LOG("Found key: ", key_str, " in with stack");
+          result = val;
+          if (m_parent_type == RuleFunc && m_builtins.is_builtin(key_str))
           {
-            LOG("Recursion detected in rule-func: ", key_str);
-            return {};
+            if (
+              std::find_if(
+                result.begin(), result.end(), [&](const Value& value) {
+                  Node node = value->node();
+                  if (node->type() == RuleFunc)
+                  {
+                    return (node / Var)->location() == m_rule;
+                  }
+
+                  return false;
+                }) != result.end())
+            {
+              LOG("Recursion detected in rule-func: ", key_str);
+              return {};
+            }
           }
+
+          break;
         }
+        else if (key.starts_with(key_str) && !partials.contains(key))
+        {
+          LOG("Found prefix match: ", key_str, " in with stack");
+          partials[key.substr(key_str.size() + 1)] = val;
+        }
+      }
+    }
 
-        return values;
+    if (partials.size() == 0)
+    {
+      return result;
+    }
+
+    std::map<std::string, Node> object_map;
+    if (result.size() > 0)
+    {
+      Node base = result.front()->node();
+      for (auto& item : *base)
+      {
+        std::string key =
+          strip_quotes(std::string((item / Key)->location().view()));
+        object_map[key] = item / Val;
+      }
+    }
+
+    for (auto& [key, val] : partials)
+    {
+      if (val.size() == 0)
+      {
+        continue;
       }
+
+      object_map[key] = val.front()->node();
+    }
+
+    Node object = NodeDef::create(Object);
+    for (auto& [loc, val] : object_map)
+    {
+      Node key = Term << (Scalar << (JSONString ^ loc));
+      object->push_back(ObjectItem << key << val);
     }
 
-    return {};
+    return {ValueDef::create(object)};
   }
 
   Values UnifierDef::resolve_skip(const Node& skip)
@@ -1174,6 +1251,10 @@ namespace rego
     }
 
     assert(rulecomp->type() == RuleComp);
+    if (rulecomp->type() != RuleComp)
+    {
+      return std::nullopt;
+    }
 
     Location rulename = (wfi / rulecomp / Var)->location();
     Node rulebody = wfi / rulecomp / Body;
@@ -1245,7 +1326,10 @@ namespace rego
   std::optional<RankedNode> UnifierDef::resolve_rulefunc(
     const Node& rulefunc, const Nodes& args)
   {
-    assert(rulefunc->type() == RuleFunc);
+    if (rulefunc->type() != RuleFunc)
+    {
+      return std::nullopt;
+    }
 
     rank_t index = ValueDef::get_rank(wfi / rulefunc / Idx);
     Node rule = Resolver::inject_args(rulefunc, args);
@@ -1329,6 +1413,11 @@ namespace rego
     for (const auto& rule : ruleset)
     {
       assert(rule->type() == RuleSet);
+      if (rule->type() != RuleSet)
+      {
+        return std::nullopt;
+      }
+
       Location rulename = (wfi / rule / Var)->location();
       Node rulebody = wfi / rule / Body;
       Node value = wfi / rule / Val;
@@ -1414,6 +1503,11 @@ namespace rego
         return rule;
       }
       assert(rule->type() == RuleObj);
+      if (rule->type() != RuleObj)
+      {
+        return std::nullopt;
+      }
+
       Location rulename = (wfi / rule / Var)->location();
       Node rulebody = wfi / rule / Body;
       Node value = wfi / rule / Val;
@@ -1593,7 +1687,7 @@ namespace rego
       Node item_node = item_value->node();
       if (item_node->type() == Undefined)
       {
-        return JSONFalse;
+        return JSONFalse ^ "false";
       }
 
       if (item_node->type() == Term)
@@ -1613,7 +1707,7 @@ namespace rego
       Node result = rule_unifier(name, body)->unify();
       if (result->type() == JSONFalse)
       {
-        return JSONFalse;
+        return JSONFalse ^ "false";
       }
 
       if (result->type() == Error)
@@ -1622,7 +1716,7 @@ namespace rego
       }
     }
 
-    return JSONTrue;
+    return JSONTrue ^ "true";
   }
 
   bool UnifierDef::is_variable(const Location& name) const
diff --git a/src/variable.cc b/src/variable.cc
index 36a55609..2a11b28b 100644
--- a/src/variable.cc
+++ b/src/variable.cc
@@ -20,8 +20,8 @@ namespace
 
 namespace rego
 {
-  Variable::Variable(const Node& local) :
-    m_local(local), m_initialized(false), m_dependency_score(1)
+  Variable::Variable(const Node& local, std::size_t id) :
+    m_local(local), m_initialized(false), m_id(id)
   {
     Location name = (wfi / local / Var)->location();
     m_unify = is_unify(name.view());
@@ -94,28 +94,8 @@ namespace rego
 
   std::ostream& operator<<(std::ostream& os, const Variable& variable)
   {
-    return os << (wfi / variable.m_local / Var)->location().view() << "("
-              << variable.m_dependency_score << ") = " << variable.m_values;
-  }
-
-  std::ostream& operator<<(
-    std::ostream& os, const std::map<Location, Variable>& variables)
-  {
-    os << "{";
-    std::string sep = "";
-    for (const auto& [loc, var] : variables)
-    {
-      os << sep << loc.view() << "(" << var.m_dependency_score << ") -> "
-         << var.m_dependencies;
-      sep = ", ";
-    }
-    os << "}";
-    return os;
-  }
-
-  void Variable::mark_invalid_values()
-  {
-    m_values.mark_invalid_values();
+    return os << (wfi / variable.m_local / Var)->location().view() << " = "
+              << variable.m_values;
   }
 
   void Variable::mark_valid_values()
@@ -139,6 +119,7 @@ namespace rego
       return nodes[0];
     }
 
+    std::set<std::string> values;
     Node term_set = NodeDef::create(TermSet);
     for (const auto& node : nodes)
     {
@@ -152,7 +133,12 @@ namespace rego
         continue;
       }
 
-      term_set->push_back(node);
+      std::string json = to_json(node);
+      if (!values.contains(json))
+      {
+        values.insert(json);
+        term_set->push_back(node);
+      }
     }
 
     if (term_set->size() == 1)
@@ -173,28 +159,6 @@ namespace rego
     return m_user_var;
   }
 
-  std::size_t Variable::dependency_score() const
-  {
-    return m_dependency_score;
-  }
-
-  Variable& Variable::dependency_score(std::size_t score)
-  {
-    m_dependency_score = score;
-    return *this;
-  }
-
-  const std::set<Location>& Variable::dependencies() const
-  {
-    return m_dependencies;
-  }
-
-  std::size_t Variable::increase_dependency_score(std::size_t amount)
-  {
-    m_dependency_score += amount;
-    return m_dependency_score;
-  }
-
   Values Variable::valid_values() const
   {
     return m_values.valid_values();
@@ -225,7 +189,7 @@ namespace rego
   {
     if (!m_unify && !m_user_var)
     {
-      return JSONTrue;
+      return JSONTrue ^ "true";
     }
 
     Node term = to_term();
@@ -235,15 +199,16 @@ namespace rego
       return term;
     }
 
-    if (Resolver::is_truthy(term) || m_user_var)
+    if (term->type() == TermSet && term->size() == 0)
     {
-      m_local->back() = term;
-    }
-    else
-    {
-      return Undefined;
+      if (term->size() == 0)
+      {
+        m_local->back() = Undefined;
+        return Undefined;
+      }
     }
 
+    m_local->back() = term;
     return term;
   }
 
@@ -252,56 +217,8 @@ namespace rego
     return (wfi / m_local / Var)->location();
   }
 
-  bool Variable::has_cycle(const std::map<Location, Variable>& variables) const
+  std::size_t Variable::id() const
   {
-    std::set<Location> visited;
-
-    std::vector<Location> stack(m_dependencies.begin(), m_dependencies.end());
-    while (!stack.empty())
-    {
-      Location loc = stack.back();
-      stack.pop_back();
-
-      if (loc == name())
-      {
-        return true;
-      }
-
-      if (visited.contains(loc))
-      {
-        continue;
-      }
-
-      visited.insert(loc);
-
-      if (!variables.contains(loc))
-      {
-        // This is a variable from an outer scope and thus
-        // cannot cause a cycle.
-        continue;
-      }
-
-      const Variable& variable = variables.at(loc);
-      for (const auto& dep : variable.dependencies())
-      {
-        stack.push_back(dep);
-      }
-    }
-
-    return false;
-  }
-
-  std::size_t Variable::detect_cycles(std::map<Location, Variable>& variables)
-  {
-    std::size_t cycles = 0;
-    for (auto& [_, variable] : variables)
-    {
-      if (variable.has_cycle(variables))
-      {
-        ++cycles;
-      }
-    }
-
-    return cycles;
+    return m_id;
   }
 }
\ No newline at end of file


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index ae7a47da..9eaa7f41 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -24,16 +24,33 @@ add_custom_command(TARGET rego_test POST_BUILD
 
 if(REGOCPP_OPA_TESTS)
   set( OPA_TEST_DIRS
+  aggregates
+  array
   arithmetic
+  comparisonexpr
+  compositebasedereference
+  helloworld
+  indexing
+  intersection
   numbersrange
   rand
+  replacen
   sets
+  sprintf
+  strings
+  trim
+  trimleft
+  trimprefix
+  trimright
+  trimspace
+  trimsuffix
+  union
   )
 
   set( OPA_TEST_ROOT ${CMAKE_CURRENT_BINARY_DIR}/../opa/test/cases/testdata )
   foreach( TEST_DIR ${OPA_TEST_DIRS})
     add_test(NAME rego_test_${TEST_DIR}
-             COMMAND rego_test ${OPA_TEST_ROOT}/${TEST_DIR}
+             COMMAND rego_test ${OPA_TEST_ROOT}/${TEST_DIR}/
              WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
   endforeach()
 endif()
diff --git a/tests/main.cc b/tests/main.cc
index c95550db..e7a5340f 100644
--- a/tests/main.cc
+++ b/tests/main.cc
@@ -47,8 +47,11 @@ int main(int argc, char** argv)
   app.add_flag(
     "-f,--fail-first", fail_first, "Stop after first test case failure");
 
-  std::string note;
-  app.add_option("-n,--note", note, "Note of specific test to run");
+  std::string note_match;
+  app.add_option(
+    "-n,--note",
+    note_match,
+    "Note (or note substring) of specific test to run");
 
   try
   {
@@ -82,17 +85,23 @@ int main(int argc, char** argv)
     }
   }
 
+  int total = 0;
   int failures = 0;
   for (auto& [category, cat_cases] : all_testcases)
   {
     std::cout << White << category << std::endl;
     for (auto& testcase : cat_cases)
     {
-      if (!note.empty() && testcase.note().find(note) == std::string::npos)
+      if (
+        !note_match.empty() &&
+        testcase.note().find(note_match) == std::string::npos)
       {
         continue;
       }
 
+      total++;
+      std::string note = testcase.note().substr(category.size() + 1);
+
       try
       {
         auto start = std::chrono::steady_clock::now();
@@ -102,18 +111,18 @@ int main(int argc, char** argv)
 
         if (result.passed)
         {
-          std::cout << Green << "  PASS: " << Reset << testcase.note()
-                    << std::fixed << std::setw(62 - testcase.note().length())
-                    << std::internal << std::setprecision(2) << elapsed.count()
-                    << " sec" << std::endl;
+          std::cout << Green << "  PASS: " << Reset << note << std::fixed
+                    << std::setw(62 - note.length()) << std::internal
+                    << std::setprecision(2) << elapsed.count() << " sec"
+                    << std::endl;
         }
         else
         {
           failures++;
-          std::cout << Red << "  FAIL: " << Reset << testcase.note()
-                    << std::fixed << std::setw(62 - testcase.note().length())
-                    << std::internal << std::setprecision(2) << elapsed.count()
-                    << " sec" << std::endl;
+          std::cout << Red << "  FAIL: " << Reset << note << std::fixed
+                    << std::setw(62 - note.length()) << std::internal
+                    << std::setprecision(2) << elapsed.count() << " sec"
+                    << std::endl;
           std::cout << "  " << result.error << std::endl;
           std::cout << "(from " << testcase.filename() << ")" << std::endl;
           if (fail_first)
@@ -125,7 +134,7 @@ int main(int argc, char** argv)
       catch (const std::exception& e)
       {
         failures++;
-        std::cout << Red << "  FAIL: " << Reset << testcase.note() << std::endl;
+        std::cout << Red << "  EXCEPTION: " << Reset << note << std::endl;
         std::cout << "  " << e.what() << std::endl;
         std::cout << "(from " << testcase.filename() << ")" << std::endl;
         if (fail_first)
@@ -139,6 +148,9 @@ int main(int argc, char** argv)
       break;
     }
 
+    std::cout << std::endl;
+    std::cout << (total - failures) << " / " << total << " passed" << std::endl;
+
     std::cout << std::endl;
   }
 
diff --git a/tests/parse.cc b/tests/parse.cc
index 5f81aa75..db6185cf 100644
--- a/tests/parse.cc
+++ b/tests/parse.cc
@@ -88,17 +88,31 @@ namespace rego_test
         // Double quote string
         "\"" >>
           [quote](auto& m) {
-            m.push(DoubleQuoteString, 1);
-            m.mode("multiline-string");
-            *quote = Quote::Double;
+            if (m.previous(Colon) || m.in(Block) || m.in(Square) || m.in(Brace))
+            {
+              m.push(DoubleQuoteString, 1);
+              m.mode("multiline-string");
+              *quote = Quote::Double;
+            }
+            else
+            {
+              m.extend(String);
+            }
           },
 
         // Single quote string
         "'" >>
           [quote](auto& m) {
-            m.push(SingleQuoteString, 1);
-            m.mode("multiline-string");
-            *quote = Quote::Single;
+            if (m.previous(Colon) || m.in(Square) || m.in(Brace) || m.in(Block))
+            {
+              m.push(SingleQuoteString, 1);
+              m.mode("multiline-string");
+              *quote = Quote::Single;
+            }
+            else
+            {
+              m.extend(String);
+            }
           },
 
         // KeyValue.
@@ -111,7 +125,13 @@ namespace rego_test
         ">" >> [](auto& m) { m.push(FoldedString); },
 
         // Entry.
-        "-[ ]*" >> [](auto& m) { m.add(Hyphen); },
+        "-[ \n][\r\n]*" >>
+          [indent, indents](auto& m) {
+            *indent += 2;
+            m.add(Hyphen);
+            m.push(Block);
+            indents->push_back(*indent);
+          },
 
         // True.
         "true\\b" >> [](auto& m) { m.add(True); },
@@ -123,22 +143,30 @@ namespace rego_test
         "null\\b" >> [](auto& m) { m.add(Null); },
 
         // String
-        R"([a-zA-Z_][a-zA-Z0-9_!\/ \.=\+\-\*\[\]\(\);><\|]*)" >>
-          [](auto& m) { m.extend(String); },
+        R"([^[:digit:]\-[:blank:]\r\n])" >>
+          [](auto& m) {
+            m.extend(String);
+            m.mode("string");
+          },
 
         // Float.
-        R"([[:digit:]]+\.[[:digit:]]+(?:e[+-]?[[:digit:]]+)?\b)" >>
+        R"(\-?[[:digit:]]+\.[[:digit:]]+(?:e[+-]?[[:digit:]]+)?\b)" >>
           [](auto& m) { m.add(Float); },
 
         // Int.
-        R"([[:digit:]]+\b)" >> [](auto& m) { m.add(Integer); },
+        R"(\-?[[:digit:]]+\b)" >> [](auto& m) { m.add(Integer); },
 
         // Space.
         " " >> [](auto&) {},
       });
 
     p("indent",
-      {// end of file terminates
+      {// Line comment.
+       "#[^\n]*\n" >> [indent](auto&) { *indent = 0; },
+
+       R"(\\ )" >> [](auto&) {},
+
+       // end of file terminates
        "\r*\n$" >>
          [indents](auto& m) {
            while (!indents->empty())
@@ -173,7 +201,7 @@ namespace rego_test
          },
 
        // Hyphen
-       "-[ ]*" >>
+       "-[ \n][\r\n]*" >>
          [indent, string_indent, indents](auto& m) {
            m.mode("start");
            if (m.in(LiteralString) || m.in(FoldedString))
@@ -204,7 +232,7 @@ namespace rego_test
        // Character
        "." >>
          [indent, string_indent, indents](auto& m) {
-           std::string mode = "start";
+           std::string mode = "string";
            if (m.in(LiteralString) || m.in(FoldedString))
            {
              mode = "multiline-string";
@@ -214,7 +242,7 @@ namespace rego_test
              }
              else if (*indent < *string_indent)
              {
-               mode = "start";
+               mode = "string";
                *string_indent = 0;
                if (m.in(LiteralString))
                {
@@ -255,9 +283,36 @@ namespace rego_test
            m.add(String);
          }});
 
+    p("string",
+      {
+        "\r*\n$" >>
+          [indents](auto& m) {
+            while (!indents->empty())
+            {
+              m.term({Block});
+              indents->pop_back();
+            }
+          },
+
+        "\r*\n" >>
+          [indent](auto& m) {
+            *indent = 0;
+            m.term();
+            m.mode("indent");
+          },
+
+        ":" >>
+          [](auto& m) {
+            m.add(Colon);
+            m.mode("start");
+          },
+
+        // Character
+        "." >> [](auto& m) { m.extend(String); },
+      });
+
     p("multiline-string",
-      {// end of file terminates
-       "\r*\n$" >>
+      {R"(\\?\r*\n$)" >>
          [indents](auto& m) {
            while (!indents->empty())
            {
@@ -266,14 +321,29 @@ namespace rego_test
            }
          },
 
-       "\r*\n" >>
+       R"(\\?\r*\n)" >>
          [indent](auto& m) {
            *indent = 0;
            m.term();
            m.mode("indent");
          },
 
-       R"(\\")" >> [](auto& m) { m.extend(String); },
+       R"(\\("))" >> [](auto& m) { m.add(String, 1); },
+
+       R"(\\ )" >> [](auto&) {},
+
+       R"(\\n)" >>
+         [quote](auto& m) {
+           if (*quote == Quote::Double)
+           {
+             m.term();
+             m.add(NewLine);
+           }
+           else if (*quote == Quote::None)
+           {
+             m.extend(String);
+           }
+         },
 
        "\"" >>
          [string_indent, quote](auto& m) {
diff --git a/tests/rego_test.cc b/tests/rego_test.cc
index b860d4f9..d362839c 100644
--- a/tests/rego_test.cc
+++ b/tests/rego_test.cc
@@ -15,6 +15,16 @@ namespace rego_test
       In(Block) * (T(Group) << (T(String) * T(Colon) * (T(Brace) << End))) >>
         [](Match&) { return Node(); },
 
+      In(Group) * (T(Colon) * T(Integer)[Integer] * T(String)[String]) >>
+        [](Match& _) {
+          Location int_loc = _(Integer)->location();
+          Location str_loc = _(String)->location();
+          std::size_t end = str_loc.pos + str_loc.len;
+          Location loc = int_loc;
+          loc.len = end - loc.pos;
+          return Seq << Colon << (String ^ loc);
+        },
+
       In(Block) *
           ((T(Group) << (T(String) * T(Colon) * End)) *
            (T(Group)[Group] << (T(String) * T(Colon)))) >>
@@ -25,6 +35,11 @@ namespace rego_test
 
       In(Group) * T(Square)[Square] >>
         [](Match& _) {
+          if (_(Square)->size() == 0)
+          {
+            return EmptySequence ^ _(Square);
+          }
+
           Node seq = NodeDef::create(Seq);
           for (auto group : *_(Square))
           {
@@ -36,20 +51,17 @@ namespace rego_test
       In(Group) * (T(Hyphen) * (T(Block) / T(Group))[Entry]) >>
         [](Match& _) { return Entry << _(Entry); },
 
-      In(Group) * (T(Hyphen)[Hyphen] * T(Integer)[Integer]) >>
-        [](Match& _) {
-          Location loc0 = _(Hyphen)->location();
-          Location loc1 = _(Integer)->location();
-          Location loc(loc0.source, loc0.pos, loc1.pos + loc1.len - loc0.pos);
-          return Integer ^ loc;
-        },
-
-      In(Group) * (T(Hyphen)[Hyphen] * T(Float)[Float]) >>
+      In(DoubleQuoteString) *
+          (T(Group) << (T(NewLine) * ~T(String)++[String])) >>
         [](Match& _) {
-          Location loc0 = _(Hyphen)->location();
-          Location loc1 = _(Float)->location();
-          Location loc(loc0.source, loc0.pos, loc1.pos + loc1.len - loc0.pos);
-          return Float ^ loc;
+          std::ostringstream buf;
+          buf << "\n";
+          for (auto it = _[String].first; it != _[String].second; ++it)
+          {
+            Node str = *it;
+            buf << str->location().view();
+          }
+          return String ^ buf.str();
         },
 
       (In(LiteralString) / In(FoldedString) / In(SingleQuoteString) /
@@ -95,12 +107,17 @@ namespace rego_test
           return err(_(Group), "Invalid double-quoted string element");
         },
 
+      In(Group) * T(NewLine)[NewLine] >>
+        [](Match& _) { return err(_(NewLine), "Invalid newline"); },
     };
   }
 
   PassDef sequence()
   {
     return {
+      In(Group) * T(EmptySequence)[EmptySequence] >>
+        [](Match& _) { return Sequence ^ _(EmptySequence); },
+
       In(Group) * (T(Entry)[Head] * T(Entry)++[Tail] * End) >>
         [](Match& _) { return Sequence << _(Head) << _[Tail]; },
 
@@ -199,6 +216,20 @@ namespace rego_test
           return KeyValue << (Key ^ key_loc) << (Group << _[Val]);
         },
 
+      (In(Entry) / In(Block)) *
+          ((T(Group) << ((T(Scalar) << T(String))[Key] * T(Colon))) * End) >>
+        [](Match& _) {
+          Location key_loc = _(Key)->location();
+          std::string key = std::string(key_loc.view());
+          std::size_t length = key.find(' ');
+          if (length != std::string::npos)
+          {
+            key_loc.len = length;
+          }
+
+          return KeyValue << (Key ^ key_loc) << (Group << Sequence);
+        },
+
       In(Entry) * T(KeyValue)[KeyValue] >>
         [](Match& _) { return Group << (Block << _(KeyValue)); },
 
@@ -274,6 +305,14 @@ namespace rego_test
                                       << (rego::List << *_[Mapping]))));
         },
 
+      In(Mapping) *
+          (T(KeyValue)
+           << ((T(Key)[Key]([](auto& n) {
+                 return name_equals(*n.first, {"data", "input"});
+               })) *
+               (T(Scalar) << T(Null)))) >>
+        [](Match&) { return Node{}; },
+
       In(Mapping) *
           (T(KeyValue)
            << ((T(Key)[Key]([](auto& n) {
diff --git a/tests/rego_test.h b/tests/rego_test.h
index 6c3673d3..d7369338 100644
--- a/tests/rego_test.h
+++ b/tests/rego_test.h
@@ -41,10 +41,12 @@ namespace rego_test
   inline const auto Blank = TokenDef("yaml-blank");
   inline const auto Brace = TokenDef("yaml-brace");
   inline const auto Square = TokenDef("yaml-square");
+  inline const auto NewLine = TokenDef("yaml-newline");
+  inline const auto EmptySequence = TokenDef("yaml-empty-sequence");
 
   inline const auto wf_parse_tokens = Block | String | Integer | Float | True |
     False | Null | Colon | LiteralString | FoldedString | SingleQuoteString |
-    DoubleQuoteString | Hyphen | Blank | Brace | Square;
+    DoubleQuoteString | Hyphen | Blank | Brace | Square | NewLine;
 
   // clang-format off
   inline const auto wf_parser =
@@ -53,8 +55,8 @@ namespace rego_test
     | (Block <<= Group++[1])
     | (LiteralString <<= Group++[1])
     | (FoldedString <<= Group++[1])
-    | (SingleQuoteString <<= Group++[1])
-    | (DoubleQuoteString <<= Group++[1])
+    | (SingleQuoteString <<= Group++)
+    | (DoubleQuoteString <<= Group++)
     | (Brace <<= Group++)
     | (Square <<= Group++)
     | (Group <<= wf_parse_tokens++[1])
@@ -63,7 +65,7 @@ namespace rego_test
 
   inline const auto wf_entry_tokens = Block | String | Integer | Float | True |
     False | Null | Colon | Entry | LiteralString | FoldedString |
-    SingleQuoteString | DoubleQuoteString;
+    SingleQuoteString | DoubleQuoteString | EmptySequence;
 
   // clang-format off
   inline const auto wf_pass_entry =
@@ -71,8 +73,8 @@ namespace rego_test
     | (Top <<= Block)
     | (LiteralString <<= String++[1])
     | (FoldedString <<= String++[1])
-    | (SingleQuoteString <<= String++[1])
-    | (DoubleQuoteString <<= String++[1])
+    | (SingleQuoteString <<= String++)
+    | (DoubleQuoteString <<= String++)
     | (Entry <<= Block | Group)
     | (Group <<= wf_entry_tokens++[1])
     ;
@@ -84,7 +86,7 @@ namespace rego_test
   // clang-format off
   inline const auto wf_pass_sequence =
     wf_pass_entry
-    | (Sequence <<= Entry++[1])
+    | (Sequence <<= Entry++)
     | (Group <<= wf_sequence_tokens++[1])
     ;
   // clang-format on   
@@ -109,7 +111,7 @@ namespace rego_test
   // clang-format off
   inline const auto wf_pass_mapping =
     wf_pass_keyvalue
-    | (Mapping <<= KeyValue++[1])
+    | (Mapping <<= KeyValue++)
     | (Entry <<= Group)
     | (Group <<= Mapping | Scalar | Sequence)
     ;
@@ -119,8 +121,8 @@ namespace rego_test
     (Top <<= Document)
     | (Document <<= KeyValue)
     | (KeyValue <<= Key * (Val >>= Scalar | Sequence | Mapping))[Key]
-    | (Sequence <<= Entry++[1])
-    | (Mapping <<= KeyValue++[1])
+    | (Sequence <<= Entry++)
+    | (Mapping <<= KeyValue++)
     | (Entry <<= Scalar | Sequence | Mapping)
     | (Scalar <<= String | Integer | Float | True | False | Null)
     ;
@@ -140,7 +142,7 @@ namespace rego_test
     | (rego::Square <<= rego::List)
     | (rego::List <<= Group++)
     | (Group <<= rego::wf_parse_tokens++[1])
-    | (WantResult <<= rego::Binding++[1])
+    | (WantResult <<= rego::Binding++)
     | (rego::Binding <<= rego::Var * rego::Term)
     | (rego::Term <<= wf_rego_term_tokens)
     | (rego::Object <<= rego::ObjectItem++[1])
diff --git a/tests/regocpp.yaml b/tests/regocpp.yaml
index faa16b6a..52044ac3 100644
--- a/tests/regocpp.yaml
+++ b/tests/regocpp.yaml
@@ -258,38 +258,18 @@ cases:
   modules:
   - |
     package sets
-    
-    a := 1
-    b := 1
-    c := 2
-    d := 3
-    
-    e := "one"
-    f := "one"
-    g := "two"
-    h := "three" {
-      d > c
-      c > b
-      b >= a
-    }
-    
+       
     i := {"foo": 1}
     j := {"foo": 1}
     k := {"foo": 2, "bar": 1, "baz": 1}
     l := {"foo": 3, "bar": 2, "baz": 1}
     
-    int_set := {4, a, c, d, b}
-    string_set := {"four", g, f, e}
     obj_set := {i, j, k, l}
-    array_set := {[a, b], [b, a], [a, c], [a, d], [b, c], [b, d], [c, d]}
     
     empty_set := set()
     
     m {
-      data.sets.int_set[1]
-      string_set["one"]
       data.sets.obj_set[{"foo": 1}]
-      array_set[[1, 1]]
     }
   query: data.sets.m = x
   want_result:
diff --git a/tests/test_case.cc b/tests/test_case.cc
index 90808450..f7b5b0d7 100644
--- a/tests/test_case.cc
+++ b/tests/test_case.cc
@@ -19,12 +19,13 @@ namespace rego_test
     std::filesystem::path output;
     if (index < 10)
     {
-      output =
-        debug_path / ("0" + std::to_string(index) + "_" + pass + ".trieste");
+      output = debug_path /
+        ("0" + std::to_string(index) + "_yaml_" + pass + ".trieste");
     }
     else
     {
-      output = debug_path / (std::to_string(index) + "_" + pass + ".trieste");
+      output =
+        debug_path / (std::to_string(index) + "_yaml_" + pass + ".trieste");
     }
     std::ofstream f(output, std::ios::binary | std::ios::out);
 
@@ -189,11 +190,17 @@ namespace rego_test
   }
 
   void TestCase::diff(
-    const std::string& actual, const std::string& wanted, std::ostream& os)
+    const std::string& actual, const std::string& wanted_raw, std::ostream& os)
   {
     std::set<std::size_t> errors;
     std::size_t index = 0;
-    std::size_t length = std::min(actual.size(), wanted.size());
+    std::size_t length = std::min(actual.size(), wanted_raw.size());
+    std::string_view wanted = wanted_raw;
+    if (wanted.size() > length)
+    {
+      wanted = wanted.substr(wanted.size() - length);
+    }
+
     for (; index < length; ++index)
     {
       if (actual[index] != wanted[index])
@@ -202,7 +209,7 @@ namespace rego_test
       }
     }
 
-    length = std::max(actual.size(), wanted.size());
+    length = actual.size();
     for (; index < length; ++index)
     {
       errors.insert(index);
@@ -222,6 +229,7 @@ namespace rego_test
         os << " ";
       }
     }
+    os << std::endl;
   }
 
   bool TestCase::compare(
@@ -252,7 +260,8 @@ namespace rego_test
     const std::string& wanted,
     std::ostream& os) const
   {
-    if (actual == wanted)
+    // some rego tests cases add a non-standard prefix to the expected error
+    if (wanted.ends_with(actual))
     {
       return true;
     }
@@ -390,6 +399,7 @@ namespace rego_test
   {
     rego::Interpreter interpreter;
     interpreter.executable(executable_path);
+    interpreter.builtins().strict_errors(m_strict_error);
     if (!debug_path.empty())
     {
       interpreter.debug_enabled(true);
@@ -455,7 +465,11 @@ namespace rego_test
         std::string actual_code =
           std::string((actual / ErrorCode)->location().view());
         bool pass_error = compare(actual_error, m_want_error, error);
-        bool pass_code = compare(actual_code, m_want_error_code, error);
+        bool pass_code = true;
+        if (m_want_error_code.length() > 0)
+        {
+          pass_code = compare(actual_code, m_want_error_code, error);
+        }
         pass = pass_error && pass_code;
       }
     }
@@ -467,7 +481,7 @@ namespace rego_test
     }
     else if (m_want_result)
     {
-      if (actual->front()->type() != Undefined)
+      if (actual->size() > 0)
       {
         if (actual->type() == Error)
         {
@@ -485,17 +499,41 @@ namespace rego_test
       }
       else
       {
-        pass = false;
-        error << "wanted a result, but was undefined";
+        if (m_want_result->size() == 0)
+        {
+          pass = true;
+        }
+        else
+        {
+          pass = false;
+          error << "wanted a result, but was undefined";
+        }
       }
     }
     else if (m_want_defined)
     {
-      pass = actual->front()->type() != Undefined;
+      if (actual->size() > 0)
+      {
+        pass = true;
+      }
+      else
+      {
+        pass = false;
+        error << "wanted a defined result, but was undefined";
+      }
     }
     else
     {
-      pass = actual->front()->type() == Undefined;
+      if (actual->size() == 0)
+      {
+        pass = true;
+      }
+      else
+      {
+        std::cout << actual << std::endl;
+        pass = false;
+        error << "wanted an undefined result, but was defined";
+      }
     }
 
     return {pass, error.str()};