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

Issue:
`numbers` built-ins
- `abs`
- `ceil`
- `floor`
- `rand.intn`
- `round`



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/pr_gate.yml b/.github/workflows/pr_gate.yml
index f14a4935..e046ced7 100644
--- a/.github/workflows/pr_gate.yml
+++ b/.github/workflows/pr_gate.yml
@@ -40,7 +40,7 @@ jobs:
           sudo apt-get install ninja-build
 
       - name: CMake config
-        run: cmake -B ${{github.workspace}}/build -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -DREGOCPP_BUILD_TOOLS=1 -DREGOCPP_BUILD_TESTS=1
+        run: cmake -B ${{github.workspace}}/build -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++ -DREGOCPP_BUILD_TOOLS=1 -DREGOCPP_BUILD_TESTS=1 -DREGOCPP_OPA_TESTS=1
 
       - name: CMake build
         working-directory: ${{github.workspace}}/build
@@ -59,7 +59,7 @@ jobs:
       
       - name: CMake config
         run: |
-          cmake -B ${{github.workspace}}/build -DREGOCPP_BUILD_TOOLS=1 -DREGOCPP_BUILD_TESTS=1
+          cmake -B ${{github.workspace}}/build -DREGOCPP_BUILD_TOOLS=1 -DREGOCPP_BUILD_TESTS=1 -DREGOCPP_OPA_TESTS=1
       
       - name: CMake build
         working-directory: ${{github.workspace}}/build
diff --git a/CHANGELOG b/CHANGELOG
index d91f3199..aada045b 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,5 +1,22 @@
 # Changelog
 
+## [2023-08-12 - Version 0.2.5](https://github.com/microsoft/scenepic/releases/tag/v0.2.5)
+Adding the `numbers` built-ins.
+
+**New Features**
+- `numbers` built-ins have been added
+- The library now supports arbitrary precision integers
+
+**Test Coverage**
+- `arithmetic`
+- `numbersrange`
+- `rand`
+- `sets`
+
+**Bug Fixes**
+- Multiple implementation details exposed by testing
+
+
 ## [2023-08-12 - Version 0.2.4](https://github.com/microsoft/scenepic/releases/tag/v0.2.4)
 Adding a test driver that can execute OPA Rego-style YAML test files against the
 `regocpp` interpreter.
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b5b81064..bee1c3f9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -24,6 +24,12 @@ project( regocpp VERSION ${REGOCPP_VERSION} LANGUAGES CXX)
 set(CMAKE_CXX_STANDARD 20)
 set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
 
+# -------------------- Options --------------------------
+
+option(REGOCPP_BUILD_TOOLS "Specifies whether to build the command line tools" OFF)
+option(REGOCPP_BUILD_TESTS "Specifies whether to build the tests" OFF)
+option(REGOCPP_OPA_TESTS "Specifies whether to load and run the OPA built-ins tests" OFF)
+
 # -------------------- Find packages --------------------------
 
 FetchContent_Declare(
@@ -60,12 +66,16 @@ else()
                     ${ALL_SOURCE_FILES})
 endif()
 
-set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)
-
-# -------------------- Options --------------------------
+if(REGOCPP_OPA_TESTS)
+  find_package(Git REQUIRED)
+  if(NOT IS_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/opa)
+    execute_process(COMMAND ${GIT_EXECUTABLE} clone --depth=1 --branch v0.55.0 https://github.com/open-policy-agent/opa/ 
+                    WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+                    OUTPUT_QUIET)
+  endif()
+endif()
 
-option(REGOCPP_BUILD_TOOLS "Specifies whether to build the command line tools" OFF)
-option(REGOCPP_BUILD_TESTS "Specifies whether to build the tests" OFF)
+set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)
 
 # -------------------- Walk the subdirectories --------------------
 
@@ -123,7 +133,7 @@ endif()
 
 if (REGOCPP_BUILD_TESTS)
   install(TARGETS rego_test yaml_trieste RUNTIME)
-  install(FILES tests/regocpp.yaml DESTINATION tests)
+  install(FILES tests/regocpp.yaml tests/bigint.yaml DESTINATION tests)
 endif()
 
 export(EXPORT ${PROJECT_NAME}_Targets
diff --git a/README.md b/README.md
index 93d6e132..9b896fca 100644
--- a/README.md
+++ b/README.md
@@ -154,19 +154,26 @@ 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:
 
-- `print`
-- `startswith`
-- `endswith`
-- `count`
-- `to_number`
-- `intersection`
-- `union`
+- `numbers`
+- `sets`
+- miscellaneous
+    - `print`
+    - `startswith`
+    - `endswith`
+    - `count`
+    - `to_number`
 
 ### Compatibility with the OPA Rego Go implementation
 
 Our goal is to achieve and maintain full compatibility with the reference Go
-implementation. We are working on a test driver which will run the same tests
-and validate that we produce the same outputs.
+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:
+
+- `arithmetic`
+- `numbersrange`
+- `rand`
+- `sets`
     
 ## Contributing
 
diff --git a/VERSION b/VERSION
index 72f9fa82..28af839c 100644
--- a/VERSION
+++ b/VERSION
@@ -1,1 +1,1 @@
-0.2.4
\ No newline at end of file
+0.2.5
\ No newline at end of file
diff --git a/include/rego/bigint.h b/include/rego/bigint.h
new file mode 100644
index 00000000..3da47722
--- /dev/null
+++ b/include/rego/bigint.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include "lang.h"
+
+namespace rego
+{
+  class BigInt
+  {
+  public:
+    BigInt();
+    BigInt(const Location& value);
+    BigInt(std::int64_t value);
+    BigInt(std::size_t value);
+
+    const Location& loc() const;
+    std::int64_t to_int() const;
+    std::size_t to_size() const;
+    bool is_negative() const;
+    bool is_zero() const;
+    BigInt increment() const;
+    BigInt decrement() const;
+
+    static bool is_int(const Location& loc);
+
+    friend BigInt operator+(const BigInt& lhs, const BigInt& rhs);
+    friend BigInt operator-(const BigInt& lhs, const BigInt& rhs);
+    friend BigInt operator*(const BigInt& lhs, const BigInt& rhs);
+    friend BigInt operator/(const BigInt& lhs, const BigInt& rhs);
+    friend BigInt operator%(const BigInt& lhs, const BigInt& rhs);
+
+    friend bool operator>(const BigInt& lhs, const BigInt& rhs);
+    friend bool operator<(const BigInt& lhs, const BigInt& rhs);
+    friend bool operator<=(const BigInt& lhs, const BigInt& rhs);
+    friend bool operator>=(const BigInt& lhs, const BigInt& rhs);
+    friend bool operator==(const BigInt& lhs, const BigInt& rhs);
+    friend bool operator!=(const BigInt& lhs, const BigInt& rhs);
+
+    friend std::ostream& operator<<(std::ostream& os, const BigInt& value);
+
+    BigInt negate() const;
+
+  private:
+    struct DivideResult
+    {
+      std::string quotient;
+      std::string remainder;
+    };
+
+    static bool less_than(
+      const std::string_view& lhs, const std::string_view& rhs);
+    static bool greater_than(
+      const std::string_view& lhs, const std::string_view& rhs);
+    static bool equal(const std::string_view& lhs, const std::string_view& rhs);
+    static std::string add(
+      const std::string_view& lhs, const std::string_view& rhs, bool negative);
+    static std::string subtract(
+      const std::string_view& lhs, const std::string_view& rhs, bool negative);
+    static DivideResult divide(
+      const std::string_view& lhs, const std::string_view& rhs);
+    static std::string multiply(
+      const std::string_view& lhs, const std::string_view& rhs);
+    std::string_view digits() const;
+    Location m_loc;
+    static Location Zero;
+    static Location One;
+  };
+}
\ No newline at end of file
diff --git a/include/rego/builtins.h b/include/rego/builtins.h
index feddbc3c..72ce5f82 100644
--- a/include/rego/builtins.h
+++ b/include/rego/builtins.h
@@ -1,12 +1,12 @@
 #pragma once
 
-#include "value.h"
-
 #include <functional>
 #include <string>
+#include <trieste/driver.h>
 
 namespace rego
 {
+  using namespace trieste;
   struct BuiltInDef;
   using BuiltIn = std::shared_ptr<BuiltInDef>;
   using BuiltInBehavior = Node (*)(const Nodes&);
@@ -27,11 +27,24 @@ namespace rego
     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;
+
+    template <typename T>
+    BuiltIns& register_builtins(const T& built_ins)
+    {
+      for (auto& built_in : built_ins)
+      {
+        register_builtin(built_in);
+      }
+
+      return *this;
+    }
+
     BuiltIns& register_standard_builtins();
     std::map<Location, BuiltIn>::const_iterator begin() const;
     std::map<Location, BuiltIn>::const_iterator end() const;
 
   private:
-    std::map<Location, BuiltIn> s_builtins;
+    std::map<Location, BuiltIn> m_builtins;
   };
 }
\ No newline at end of file
diff --git a/include/rego/interpreter.h b/include/rego/interpreter.h
index 23315fc6..c2270714 100644
--- a/include/rego/interpreter.h
+++ b/include/rego/interpreter.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "builtins.h"
 #include "lang.h"
 
 #include <string>
@@ -30,19 +31,21 @@ namespace rego
     bool well_formed_checks_enabled() const;
     Interpreter& executable(const std::filesystem::path& path);
     const std::filesystem::path& executable() const;
+    BuiltIns& builtins();
+    const BuiltIns& builtins() const;
 
   private:
     void write_ast(
       std::size_t index, const std::string& pass, const Node& ast) const;
-    bool has_error(const Node& ast) const;
+    Node get_errors(const Node& ast) const;
     Parse m_parser;
     wf::Wellformed m_wf_parser;
-    std::vector<PassCheck> m_passes;
     Node m_module_seq;
     Node m_data_seq;
     Node m_input;
     std::filesystem::path m_debug_path;
     bool m_debug_enabled;
     bool m_well_formed_checks_enabled;
+    BuiltIns m_builtins;
   };
 }
\ No newline at end of file
diff --git a/include/rego/lang.h b/include/rego/lang.h
index e86425d7..db2dcbe7 100644
--- a/include/rego/lang.h
+++ b/include/rego/lang.h
@@ -1,5 +1,6 @@
 #pragma once
 
+#include "builtins.h"
 #include "trieste/driver.h"
 #include "trieste/token.h"
 
@@ -25,7 +26,6 @@ namespace rego
   inline const auto ExprCall = TokenDef("expr-call");
   inline const auto ExprEvery = TokenDef("expr-every");
   inline const auto UnaryExpr = TokenDef("unary-expr");
-  inline const auto NotExpr = TokenDef("not-expr");
   inline const auto Term = TokenDef("term");
   inline const auto InfixOperator = TokenDef("infix-operator");
   inline const auto BoolOperator = TokenDef("bool-operator");
@@ -131,6 +131,7 @@ namespace rego
   inline const auto LiteralWith = TokenDef("literal-with");
   inline const auto LiteralEnum = TokenDef("literal-enum");
   inline const auto LiteralInit = TokenDef("literal-init");
+  inline const auto LiteralNot = TokenDef("literal-not");
 
   // utility
   inline const auto Undefined = TokenDef("undefined");
@@ -159,6 +160,7 @@ namespace rego
   inline const auto UnifyExprWith = TokenDef("unify-expr-with");
   inline const auto UnifyExprCompr = TokenDef("unify-expr-compr");
   inline const auto UnifyExprEnum = TokenDef("unify-expr-enum");
+  inline const auto UnifyExprNot = TokenDef("unify-expr-not");
   inline const auto TermSet = TokenDef("term-set");
   inline const auto Empty = TokenDef("empty");
   inline const auto SimpleRef = TokenDef("simple-ref");
@@ -183,6 +185,8 @@ namespace rego
   inline const auto WithRef = TokenDef("with-ref");
   inline const auto WithExpr = TokenDef("with-expr");
   inline const auto EverySeq = TokenDef("every-seq");
+  inline const auto ErrorCode = TokenDef("error-code", flag::print);
+  inline const auto ErrorSeq = TokenDef("error-seq");
 
   // lists
   inline const auto List = TokenDef("list");
@@ -202,26 +206,21 @@ namespace rego
   inline const std::set<Token> RuleTypes(
     {RuleComp, RuleFunc, RuleSet, RuleObj, DefaultRule});
 
-  inline auto err(NodeRange& r, const std::string& msg)
-  {
-    return Error << (ErrorMsg ^ msg) << (ErrorAst << r);
-  }
+  Node err(
+    NodeRange& r,
+    const std::string& msg,
+    const std::string& code = "runtime_error");
 
-  inline auto err(Node node, const std::string& msg)
-  {
-    return Error << (ErrorMsg ^ msg) << (ErrorAst << node->clone());
-  }
-
-  inline auto err(const std::string& msg)
-  {
-    return Error << (ErrorMsg ^ msg);
-  }
+  Node err(
+    Node node,
+    const std::string& msg,
+    const std::string& code = "runtime_error");
 
   Parse parser();
-  Driver& driver();
+  Driver& driver(const BuiltIns& builtins);
   using PassCheck = std::tuple<std::string, Pass, const wf::Wellformed*>;
-  std::vector<PassCheck> passes();
-  std::string to_json(const Node& node);
+  std::vector<PassCheck> passes(const BuiltIns& builtins);
+  std::string to_json(const Node& node, bool sort = false);
   bool contains_local(const Node& node);
   bool contains_ref(const Node& node);
   bool is_in(const Node& node, const std::set<Token>& token);
diff --git a/include/rego/passes.h b/include/rego/passes.h
index 990e3ef3..cbaffec5 100644
--- a/include/rego/passes.h
+++ b/include/rego/passes.h
@@ -45,11 +45,12 @@ namespace rego
   PassDef compr();
   PassDef absolute_refs();
   PassDef merge_modules();
-  PassDef skips();
+  PassDef skips(const BuiltIns& builtins);
+  PassDef unary();
   PassDef multiply_divide();
   PassDef add_subtract();
   PassDef comparison();
-  PassDef assign();
+  PassDef assign(const BuiltIns& builtins);
   PassDef skip_refs();
   PassDef simple_refs();
   PassDef implicit_enums();
@@ -57,6 +58,6 @@ namespace rego
   PassDef rulebody();
   PassDef lift_to_rule();
   PassDef functions();
-  PassDef unify();
+  PassDef unify(const BuiltIns& builtins);
   PassDef query();
 }
\ No newline at end of file
diff --git a/include/rego/resolver.h b/include/rego/resolver.h
index 83f48260..41a907b2 100644
--- a/include/rego/resolver.h
+++ b/include/rego/resolver.h
@@ -1,8 +1,11 @@
 #pragma once
 
+#include "bigint.h"
+#include "builtins.h"
+#include "lang.h"
+
 #include <optional>
 #include <string>
-#include <trieste/driver.h>
 
 namespace rego
 {
@@ -18,9 +21,9 @@ namespace rego
       PrintNode printer;
     };
 
-    static std::int64_t get_int(const Node& node);
+    static BigInt get_int(const Node& node);
 
-    static Node scalar(int64_t value);
+    static Node scalar(BigInt value);
     static Node scalar(double value);
     static Node scalar(bool value);
     static Node scalar(const std::string& value);
@@ -28,7 +31,7 @@ namespace rego
     static double get_double(const Node& node);
     static std::string get_string(const Node& node);
     static bool get_bool(const Node& node);
-    static Node resolve_query(const Node& query);
+    static Node resolve_query(const Node& query, const BuiltIns& builtins);
     static NodePrinter stmt_str(const Node& stmt);
     static NodePrinter func_str(const Node& func);
     static NodePrinter arg_str(const Node& arg);
@@ -57,6 +60,7 @@ namespace rego
     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);
   };
diff --git a/include/rego/unifier.h b/include/rego/unifier.h
index e5740cd4..a047072c 100644
--- a/include/rego/unifier.h
+++ b/include/rego/unifier.h
@@ -8,7 +8,6 @@
 #include <map>
 #include <set>
 #include <string>
-#include <trieste/driver.h>
 #include <vector>
 
 namespace rego
diff --git a/include/rego/value.h b/include/rego/value.h
index 6b570d1c..01a7c4af 100644
--- a/include/rego/value.h
+++ b/include/rego/value.h
@@ -2,15 +2,13 @@
 
 #include "lang.h"
 
-#include <cstdint>
-#include <trieste/driver.h>
-
 namespace rego
 {
   class ValueDef;
+  using rank_t = std::size_t;
   using Value = std::shared_ptr<ValueDef>;
   using Values = std::vector<Value>;
-  using RankedNode = std::pair<std::int64_t, Node>;
+  using RankedNode = std::pair<rank_t, Node>;
 
   class ValueDef
   {
@@ -26,7 +24,7 @@ namespace rego
     const Node& node() const;
     const Values& sources() const;
     Node to_term() const;
-    std::int64_t rank() const;
+    rank_t rank() const;
     friend std::ostream& operator<<(std::ostream& os, const Value& value);
     friend std::ostream& operator<<(std::ostream& os, const ValueDef& value);
     friend bool operator==(const Value& lhs, const Value& rhs);
@@ -42,6 +40,7 @@ namespace rego
       const Location& var, const Node& value, const Values& sources);
     static Value copy_to(const Value& value, const Location& var);
     static Values filter_by_rank(const Values& values);
+    static rank_t get_rank(const Node& node);
 
   private:
     void to_string(std::ostream& buf, const Location& root, bool first) const;
@@ -49,7 +48,7 @@ namespace rego
     Node m_node;
     Values m_sources;
     bool m_invalid;
-    std::int64_t m_rank;
+    rank_t m_rank;
     ValueDef(const RankedNode& value);
     ValueDef(const Location& var, const RankedNode& value);
     ValueDef(
diff --git a/include/rego/variable.h b/include/rego/variable.h
index 58a159c4..bef95795 100644
--- a/include/rego/variable.h
+++ b/include/rego/variable.h
@@ -3,8 +3,6 @@
 #include "value.h"
 #include "value_map.h"
 
-#include <trieste/driver.h>
-
 namespace rego
 {
   class Variable
@@ -30,6 +28,8 @@ namespace rego
     Location name() 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)
diff --git a/include/rego/wf.h b/include/rego/wf.h
index 89df122d..1fa3a527 100644
--- a/include/rego/wf.h
+++ b/include/rego/wf.h
@@ -40,6 +40,7 @@ namespace rego
     | (Group <<= wf_parse_tokens++[1])
     | (Some <<= (List | Group)++)
     | (With <<= Group * Group)
+    | (Error <<= ErrorMsg * ErrorAst * ErrorCode)
     ;
   // clang-format on
 
@@ -202,6 +203,7 @@ namespace rego
     | (ObjectItemSeq <<= ObjectItem++)
     | (ModuleSeq <<= Module++)
     | (Query <<= Literal++[1])
+    | (Error <<= ErrorMsg * ErrorAst * ErrorCode)
     // Below this point is the grammar of the version of Rego we support
     | (Module <<= Package * ImportSeq * Policy)
     | (ImportSeq <<= (Import | Keyword)++)
@@ -299,7 +301,7 @@ namespace rego
     | (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)++[1])
+    | (Expr <<= (RefTerm | NumTerm | Term | wf_arith_op | wf_bin_op | wf_bool_op | Unify | Expr | ExprCall | ExprEvery | Set | SetCompr)++[1])
     | (Import <<= Var * Ref)[Var]
     | (ExprEvery <<= VarSeq * NestedBody)
     | (NestedBody <<= Key * (Val >>= UnifyBody))
@@ -372,6 +374,15 @@ namespace rego
     ;
   // clang-format on
 
+  // clang-format off
+  inline const auto wf_pass_unary =
+    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])
+    ;
+  // clang-format on
+
   inline const auto wf_math_tokens =
     RefTerm | NumTerm | UnaryExpr | ArithInfix | ExprCall;
 
@@ -380,13 +391,13 @@ namespace rego
 
   // clang-format off
   inline const auto wf_pass_multiply_divide =
-    wf_pass_skips
+    wf_pass_unary
     | (ArithInfix <<= ArithArg * (Op >>= Multiply | Divide | Modulo) * ArithArg)
-    | (ArithArg <<= (Add | Subtract | Expr | wf_math_tokens)++[1])
+    | (ArithArg <<= Expr | wf_math_tokens)
     | (BinInfix <<= BinArg * (Op >>= And) * BinArg)
-    | (BinArg <<= (Or | Expr | wf_bin_tokens)++[1])
+    | (BinArg <<= (Expr | wf_bin_tokens)++[1])
     | (UnaryExpr <<= ArithArg)
-    | (Expr <<= (NumTerm | RefTerm | Term | Add | Subtract | Or | wf_bool_op | Unify | Expr | ArithInfix | BinInfix | ExprCall | ExprEvery)++[1])
+    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery | Set | SetCompr | Add | Subtract | Or | wf_bool_op)++[1])
     ;
   // clang-format on
 
@@ -397,7 +408,7 @@ namespace rego
     | (ArithArg <<= Expr | wf_math_tokens)
     | (BinInfix <<= BinArg * (Op >>= wf_bin_op) * BinArg)
     | (BinArg <<= Expr | wf_bin_tokens)
-    | (Expr <<= (NumTerm | RefTerm | Term | wf_bool_op | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery)++[1])
+    | (Expr <<= (NumTerm | RefTerm | Term | Unify | Expr | UnaryExpr | ArithInfix | BinInfix | ExprCall | ExprEvery | Set | SetCompr | wf_bool_op)++[1])
     ;
   // clang-format on
 
@@ -408,9 +419,9 @@ namespace rego
     | (BoolArg <<= Term | BinInfix | wf_math_tokens)
     | (ArithArg <<= wf_math_tokens)
     | (BinArg <<= wf_bin_tokens)
-    | (Literal <<= Expr | NotExpr)
-    | (NotExpr <<= Expr)
-    | (Expr <<= (NumTerm | RefTerm | Term | UnaryExpr | Unify | Expr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate)++[1])
+    | (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])
     | (Enumerate <<= Expr)
     ;
   // clang-format on
@@ -443,7 +454,7 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_init =
     wf_pass_implicit_enums
-    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralInit)++[1])
+    | (UnifyBody <<= (Local | Literal | LiteralWith | LiteralEnum | LiteralNot | LiteralInit)++[1])
     | (LiteralInit <<= AssignInfix)
     ;
   // clang-format on
@@ -452,12 +463,13 @@ namespace rego
   inline const auto wf_pass_rulebody =
     wf_pass_init
     | (Module <<= (Import | RuleComp | DefaultRule | RuleFunc | RuleSet | RuleObj | Submodule)++)
-    | (UnifyExpr <<= Var * (Val >>= NotExpr | Expr))
+    | (UnifyExpr <<= Var * (Val >>= Expr))
     | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate)
-    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprCompr | UnifyExprEnum)++[1])
+    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprCompr | UnifyExprEnum | UnifyExprNot)++[1])
     | (UnifyExprWith <<= UnifyBody * WithSeq)
     | (UnifyExprCompr <<= Var * (Val >>= ArrayCompr | SetCompr | ObjectCompr) * NestedBody)
     | (UnifyExprEnum <<= Var * (Item >>= Var) * (ItemSeq >>= Var) * UnifyBody)
+    | (UnifyExprNot <<= Var * (Val >>= Expr))
     | (ArrayCompr <<= Var)
     | (SetCompr <<= Var)
     | (ObjectCompr <<= Var)
@@ -468,7 +480,7 @@ namespace rego
   // clang-format off
   inline const auto wf_pass_lift_to_rule =
     wf_pass_rulebody
-    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith)++[1])
+    | (UnifyBody <<= (Local | UnifyExpr | UnifyExprWith | UnifyExprNot)++[1])
     | (Expr <<= NumTerm | RefTerm | Term | UnaryExpr | ArithInfix | BinInfix | BoolInfix | ExprCall | ExprEvery | Enumerate | ArrayCompr | SetCompr | ObjectCompr | Merge | ToValues)
     | (Merge <<= Var)
     | (ToValues <<= Expr)
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 354699ca..318f5460 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,6 +1,7 @@
 configure_file("${CMAKE_CURRENT_SOURCE_DIR}/version.h.in" "${CMAKE_CURRENT_BINARY_DIR}/version.h" @ONLY)
 
 set( SOURCES
+bigint.cc
 interpreter.cc
 lang.cc
 parse.cc
@@ -34,8 +35,7 @@ passes/compr.cc
 passes/absolute_refs.cc
 passes/merge_modules.cc
 passes/skips.cc
-passes/multiply_divide.cc
-passes/add_subtract.cc
+passes/arithmetic.cc
 passes/comparison.cc
 passes/assign.cc
 passes/skip_refs.cc
@@ -46,6 +46,8 @@ passes/lift_to_rule.cc
 passes/functions.cc
 passes/unify.cc
 passes/query.cc
+builtins/numbers.cc
+builtins/sets.cc
 )
 
 add_library(rego STATIC ${SOURCES})
diff --git a/src/README.md b/src/README.md
index 55825a04..e0b34e46 100644
--- a/src/README.md
+++ b/src/README.md
@@ -27,3 +27,4 @@ output {
     }
 }
 ```
+7. Big number support is unclear.
diff --git a/src/bigint.cc b/src/bigint.cc
new file mode 100644
index 00000000..f6de920a
--- /dev/null
+++ b/src/bigint.cc
@@ -0,0 +1,574 @@
+#include "bigint.h"
+
+#include <string_view>
+
+namespace rego
+{
+  Location BigInt::Zero = Location("0");
+  Location BigInt::One = Location("1");
+
+  BigInt::BigInt() : m_loc(BigInt::Zero) {}
+
+  BigInt::BigInt(const Location& loc) : m_loc(loc)
+  {
+    assert(is_int(loc));
+  }
+
+  BigInt::BigInt(const std::int64_t value) : BigInt(std::to_string(value)) {}
+
+  BigInt::BigInt(const std::size_t value) : BigInt(std::to_string(value)) {}
+
+  const Location& BigInt::loc() const
+  {
+    return m_loc;
+  }
+
+  bool BigInt::is_negative() const
+  {
+    return m_loc.view()[0] == '-';
+  }
+
+  BigInt BigInt::negate() const
+  {
+    if (is_negative())
+    {
+      return BigInt(std::string(m_loc.view().substr(1)));
+    }
+
+    return BigInt("-" + std::string(m_loc.view()));
+  }
+
+  BigInt operator+(const BigInt& lhs, const BigInt& rhs)
+  {
+    bool negate = false;
+    if (lhs.is_negative())
+    {
+      if (rhs.is_negative())
+      {
+        negate = true;
+      }
+      else
+      {
+        // adding a negative number
+        return rhs - lhs.negate();
+      }
+    }
+    else
+    {
+      if (rhs.is_negative())
+      {
+        // adding a negative number
+        return lhs - rhs.negate();
+      }
+    }
+
+    std::string sum = BigInt::add(lhs.digits(), rhs.digits(), negate);
+    return BigInt(sum);
+  }
+
+  std::string BigInt::add(
+    const std::string_view& lhs, const std::string_view& rhs, bool negative)
+  {
+    if (less_than(lhs, rhs))
+    {
+      return BigInt::add(rhs, lhs, negative);
+    }
+
+    if (rhs == "0")
+    {
+      return std::string(lhs);
+    }
+
+    std::string result;
+    auto lhs_it = lhs.rbegin();
+    auto rhs_it = rhs.rbegin();
+    int carry = 0;
+    for (; lhs_it != lhs.rend() && rhs_it != rhs.rend(); ++lhs_it, ++rhs_it)
+    {
+      auto lhs_digit = *lhs_it - '0';
+      auto rhs_digit = *rhs_it - '0';
+      auto sum = lhs_digit + rhs_digit + carry;
+      if (sum > 9)
+      {
+        carry = 1;
+        sum -= 10;
+      }
+      else
+      {
+        carry = 0;
+      }
+      result.push_back(static_cast<char>(sum) + '0');
+    }
+
+    // lhs will have the same or more digits
+    for (; lhs_it != lhs.rend(); ++lhs_it)
+    {
+      auto lhs_digit = *lhs_it - '0';
+      auto sum = lhs_digit + carry;
+      if (sum > 9)
+      {
+        carry = 1;
+        sum -= 10;
+      }
+      else
+      {
+        carry = 0;
+      }
+      result.push_back(static_cast<char>(sum) + '0');
+    }
+
+    if (carry > 0)
+    {
+      result.push_back('1');
+    }
+
+    if (negative)
+    {
+      result.push_back('-');
+    }
+
+    std::reverse(result.begin(), result.end());
+
+    return result;
+  }
+
+  BigInt operator-(const BigInt& lhs, const BigInt& rhs)
+  {
+    bool negate = false;
+    if (lhs.is_negative())
+    {
+      if (rhs.is_negative())
+      {
+        // subtracting a negative number
+        if (BigInt::greater_than(lhs.digits(), rhs.digits()))
+        {
+          // we can do the subtraction as if both numbers were positive
+          // and then negate it.
+          negate = true;
+        }
+        else
+        {
+          // reverse the order of the subtraction
+          return rhs.negate() - lhs.negate();
+        }
+      }
+      else
+      {
+        // do this as addition
+        return lhs + rhs.negate();
+      }
+    }
+    else
+    {
+      if (rhs.is_negative())
+      {
+        // subtracting a negative number
+        return lhs + rhs.negate();
+      }
+      else
+      {
+        if (BigInt::less_than(lhs.digits(), rhs.digits()))
+        {
+          // reverse the order of the subtraction
+          return (rhs - lhs).negate();
+        }
+      }
+    }
+
+    if (rhs.is_zero())
+    {
+      return lhs;
+    }
+
+    std::string difference =
+      BigInt::subtract(lhs.digits(), rhs.digits(), negate);
+    return BigInt(difference);
+  }
+
+  std::string BigInt::subtract(
+    const std::string_view& lhs, const std::string_view& rhs, bool negative)
+  {
+    assert(!less_than(lhs, rhs));
+    std::string result;
+    auto lhs_it = lhs.rbegin();
+    auto rhs_it = rhs.rbegin();
+    bool borrow = false;
+    for (; lhs_it != lhs.rend() && rhs_it != rhs.rend(); ++lhs_it, ++rhs_it)
+    {
+      auto lhs_digit = *lhs_it - '0';
+      auto rhs_digit = *rhs_it - '0';
+      if (borrow)
+      {
+        lhs_digit -= 1;
+        borrow = false;
+      }
+
+      if (lhs_digit < rhs_digit)
+      {
+        lhs_digit += 10;
+        borrow = true;
+      }
+
+      auto diff = lhs_digit - rhs_digit;
+      result.push_back(static_cast<char>(diff) + '0');
+    }
+
+    // lhs will have the same or more digits
+    for (; lhs_it != lhs.rend(); ++lhs_it)
+    {
+      auto lhs_digit = *lhs_it - '0';
+      if (borrow)
+      {
+        lhs_digit -= 1;
+        borrow = false;
+      }
+
+      if (lhs_digit < 0)
+      {
+        lhs_digit += 10;
+        borrow = true;
+      }
+
+      result.push_back(static_cast<char>(lhs_digit) + '0');
+    }
+
+    assert(!borrow);
+
+    while (result.size() > 1 && result.back() == '0')
+    {
+      result.pop_back();
+    }
+
+    if (negative && result != "0")
+    {
+      result.push_back('-');
+    }
+
+    std::reverse(result.begin(), result.end());
+
+    return result;
+  }
+
+  BigInt operator*(const BigInt& lhs, const BigInt& rhs)
+  {
+    std::string product = BigInt::multiply(lhs.digits(), rhs.digits());
+
+    if (product == "0")
+    {
+      return BigInt();
+    }
+
+    bool negate = false;
+    if (lhs.is_negative())
+    {
+      negate = true;
+    }
+    if (rhs.is_negative())
+    {
+      negate = !negate;
+    }
+
+    if (negate)
+    {
+      product.insert(product.begin(), '-');
+    }
+
+    return BigInt(product);
+  }
+
+  std::string BigInt::multiply(
+    const std::string_view& lhs, const std::string_view& rhs)
+  {
+    if (lhs == "0" || rhs == "0")
+    {
+      return "0";
+    }
+
+    if (greater_than(lhs, rhs))
+    {
+      return BigInt::multiply(rhs, lhs);
+    }
+
+    std::string result = "0";
+    std::string start = "";
+    for (auto lhs_it = lhs.rbegin(); lhs_it != lhs.rend(); lhs_it++)
+    {
+      auto lhs_digit = *lhs_it - '0';
+      std::string intermediate = start;
+      int carry = 0;
+      for (auto rhs_it = rhs.rbegin(); rhs_it != rhs.rend(); rhs_it++)
+      {
+        auto rhs_digit = *rhs_it - '0';
+        auto product = lhs_digit * rhs_digit + carry;
+        if (product > 9)
+        {
+          carry = product / 10;
+          product = product % 10;
+        }
+        else
+        {
+          carry = 0;
+        }
+        intermediate.push_back(static_cast<char>(product) + '0');
+      }
+
+      if (carry > 0)
+      {
+        intermediate.push_back(static_cast<char>(carry) + '0');
+      }
+
+      std::reverse(intermediate.begin(), intermediate.end());
+      start += "0";
+      result = BigInt::add(result, intermediate, false);
+    }
+
+    return result;
+  }
+
+  BigInt operator/(const BigInt& lhs, const BigInt& rhs)
+  {
+    if (BigInt::less_than(lhs.digits(), rhs.digits()))
+    {
+      return BigInt();
+    }
+
+    if (rhs.is_zero())
+    {
+      throw std::invalid_argument("division by zero");
+    }
+
+    bool negate = false;
+    if (lhs.is_negative())
+    {
+      negate = true;
+    }
+    if (rhs.is_negative())
+    {
+      negate = !negate;
+    }
+
+    auto quotient = BigInt::divide(lhs.digits(), rhs.digits()).quotient;
+    if (negate)
+    {
+      quotient.insert(quotient.begin(), '-');
+    }
+
+    return BigInt(quotient);
+  }
+
+  BigInt operator%(const BigInt& lhs, const BigInt& rhs)
+  {
+    if (BigInt::less_than(lhs.digits(), rhs.digits()))
+    {
+      return BigInt();
+    }
+
+    if (rhs.is_zero())
+    {
+      throw std::invalid_argument("modulo by zero");
+    }
+
+    auto remainder = BigInt::divide(lhs.digits(), rhs.digits()).remainder;
+    if (lhs.is_negative())
+    {
+      remainder.insert(remainder.begin(), '-');
+    }
+
+    return BigInt(remainder);
+  }
+
+  BigInt::DivideResult BigInt::divide(
+    const std::string_view& lhs, const std::string_view& rhs)
+  {
+    std::string remainder;
+
+    bool leading = true;
+    std::string quotient;
+    for (auto it = lhs.begin(); it != lhs.end(); ++it)
+    {
+      int digit = 0;
+      remainder.push_back(*it);
+      while (!less_than(remainder, rhs))
+      {
+        remainder = BigInt::subtract(remainder, rhs, false);
+        digit++;
+      }
+
+      if (digit == 0 && leading)
+      {
+        continue;
+      }
+
+      leading = false;
+      quotient.push_back(static_cast<char>(digit) + '0');
+    }
+
+    return DivideResult{quotient, remainder};
+  }
+
+  bool BigInt::less_than(
+    const std::string_view& lhs, const std::string_view& rhs)
+  {
+    if (lhs.size() < rhs.size())
+    {
+      return true;
+    }
+
+    if (lhs.size() > rhs.size())
+    {
+      return false;
+    }
+
+    return lhs < rhs;
+  }
+
+  bool BigInt::greater_than(
+    const std::string_view& lhs, const std::string_view& rhs)
+  {
+    if (lhs.size() > rhs.size())
+    {
+      return true;
+    }
+
+    if (lhs.size() < rhs.size())
+    {
+      return false;
+    }
+
+    return lhs > rhs;
+  }
+
+  bool operator<(const BigInt& lhs, const BigInt& rhs)
+  {
+    if (lhs.is_negative())
+    {
+      if (rhs.is_negative())
+      {
+        return BigInt::greater_than(lhs.digits(), rhs.digits());
+      }
+      else
+      {
+        return true;
+      }
+    }
+    else
+    {
+      if (rhs.is_negative())
+      {
+        return false;
+      }
+      else
+      {
+        return BigInt::less_than(lhs.digits(), rhs.digits());
+      }
+    }
+  }
+
+  bool operator>(const BigInt& lhs, const BigInt& rhs)
+  {
+    if (lhs.is_negative())
+    {
+      if (rhs.is_negative())
+      {
+        return BigInt::less_than(lhs.digits(), rhs.digits());
+      }
+      else
+      {
+        return false;
+      }
+    }
+    else
+    {
+      if (rhs.is_negative())
+      {
+        return true;
+      }
+      else
+      {
+        return BigInt::greater_than(lhs.digits(), rhs.digits());
+      }
+    }
+  }
+
+  bool operator<=(const BigInt& lhs, const BigInt& rhs)
+  {
+    return !(lhs > rhs);
+  }
+
+  bool operator>=(const BigInt& lhs, const BigInt& rhs)
+  {
+    return !(lhs < rhs);
+  }
+
+  bool operator==(const BigInt& lhs, const BigInt& rhs)
+  {
+    return lhs.m_loc == rhs.m_loc;
+  }
+
+  bool operator!=(const BigInt& lhs, const BigInt& rhs)
+  {
+    return lhs.m_loc != rhs.m_loc;
+  }
+
+  std::string_view BigInt::digits() const
+  {
+    if (is_negative())
+    {
+      return m_loc.view().substr(1);
+    }
+
+    return m_loc.view();
+  }
+
+  bool BigInt::is_zero() const
+  {
+    return m_loc.view() == "0";
+  }
+
+  std::int64_t BigInt::to_int() const
+  {
+    return std::stoll(std::string(m_loc.view()));
+  }
+
+  std::size_t BigInt::to_size() const
+  {
+    return std::stoul(std::string(m_loc.view()));
+  }
+
+  std::ostream& operator<<(std::ostream& os, const BigInt& bigint)
+  {
+    os << bigint.m_loc.view();
+    return os;
+  }
+
+  BigInt BigInt::increment() const
+  {
+    return *this + One;
+  }
+
+  BigInt BigInt::decrement() const
+  {
+    return *this - One;
+  }
+
+  bool BigInt::is_int(const Location& loc)
+  {
+    std::set<char> digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'};
+    auto it = loc.view().begin();
+    auto end = loc.view().end();
+    if (*it == '-')
+    {
+      ++it;
+    }
+
+    for (; it != end; ++it)
+    {
+      if (!digits.contains(*it))
+      {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}
\ No newline at end of file
diff --git a/src/builtins.cc b/src/builtins.cc
index 32b45984..7722d013 100644
--- a/src/builtins.cc
+++ b/src/builtins.cc
@@ -1,9 +1,10 @@
 #include "builtins.h"
 
+#include "builtins/register.h"
 #include "lang.h"
 #include "resolver.h"
 
-namespace builtins
+namespace
 {
   using namespace rego;
 
@@ -13,7 +14,7 @@ namespace builtins
     auto maybe_base = Resolver::maybe_unwrap_string(args[1]);
     if (!maybe_search.has_value() || !maybe_base.has_value())
     {
-      return err("startswith: expected string arguments");
+      return err(args[0], "startswith: expected string arguments");
     }
 
     std::string search = Resolver::get_string(*maybe_search);
@@ -27,7 +28,7 @@ namespace builtins
     auto maybe_base = Resolver::maybe_unwrap_string(args[1]);
     if (!maybe_search.has_value() || !maybe_base.has_value())
     {
-      return err("endswith: expected string arguments");
+      return err(args[0], "endswith: expected string arguments");
     }
 
     std::string search = Resolver::get_string(*maybe_search);
@@ -38,22 +39,31 @@ namespace builtins
   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(static_cast<std::int64_t>(collection->size()));
+      return Resolver::scalar(BigInt(collection->size()));
     }
-    else if (collection->type() == JSONString)
+
+    if (collection->type() == Scalar)
     {
-      std::string collection_str =
-        strip_quotes(std::string(collection->location().view()));
-      return Resolver::scalar(static_cast<std::int64_t>(collection_str.size()));
+      collection = collection->front();
     }
-    else
+
+    if (collection->type() == JSONString)
     {
-      return err("count: expected collection");
+      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)
@@ -61,27 +71,23 @@ namespace builtins
     auto maybe_number = Resolver::maybe_unwrap_string(args[0]);
     if (!maybe_number.has_value())
     {
-      return err("to_number: expected string argument");
+      return err(args[0], "to_number: expected string argument");
     }
 
-    std::string number_str = Resolver::get_string(*maybe_number);
+    if (BigInt::is_int(args[0]->location()))
+    {
+      return JSONInt ^ args[0]->location();
+    }
 
+    std::string number_str = Resolver::get_string(*maybe_number);
     try
     {
-      std::int64_t int_value = std::stoll(number_str);
-      return Resolver::scalar(int_value);
+      double float_value = std::stod(number_str);
+      return Resolver::scalar(float_value);
     }
     catch (const std::invalid_argument)
     {
-      try
-      {
-        double float_value = std::stod(number_str);
-        return Resolver::scalar(float_value);
-      }
-      catch (const std::invalid_argument)
-      {
-        return err("to_number: invalid number");
-      }
+      return err(args[0], "to_number: invalid number");
     }
   }
 
@@ -103,15 +109,6 @@ namespace builtins
     return Resolver::scalar(true);
   }
 
-  Node intersection(const Nodes& args)
-  {
-    return Resolver::set_intersection(args[0], args[1]);
-  }
-
-  Node union_(const Nodes& args)
-  {
-    return Resolver::set_union(args[0], args[1]);
-  }
 }
 
 namespace rego
@@ -124,45 +121,47 @@ namespace rego
 
   bool BuiltIns::is_builtin(const Location& name) const
   {
-    return s_builtins.contains(name);
+    return m_builtins.contains(name);
   }
 
   Node BuiltIns::call(const Location& name, const Nodes& args) const
   {
-    return s_builtins.at(name)->behavior(args);
+    return m_builtins.at(name)->behavior(args);
   }
 
   BuiltIns& BuiltIns::register_builtin(const BuiltIn& built_in)
   {
-    s_builtins[built_in->name] = built_in;
+    m_builtins[built_in->name] = built_in;
     return *this;
   }
 
   BuiltIns& BuiltIns::register_standard_builtins()
   {
     register_builtin(
-      BuiltInDef::create(Location("startswith"), 2, builtins::startswith));
-    register_builtin(
-      BuiltInDef::create(Location("endswith"), 2, builtins::endswith));
-    register_builtin(BuiltInDef::create(Location("count"), 1, builtins::count));
-    register_builtin(
-      BuiltInDef::create(Location("to_number"), 1, builtins::to_number));
-    register_builtin(
-      BuiltInDef::create(Location("print"), AnyArity, builtins::print));
-    register_builtin(
-      BuiltInDef::create(Location("union"), 2, builtins::union_));
-    register_builtin(
-      BuiltInDef::create(Location("intersection"), 2, builtins::intersection));
+      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::numbers());
+
     return *this;
   }
 
   std::map<Location, BuiltIn>::const_iterator BuiltIns::begin() const
   {
-    return s_builtins.begin();
+    return m_builtins.begin();
   }
 
   std::map<Location, BuiltIn>::const_iterator BuiltIns::end() const
   {
-    return s_builtins.end();
+    return m_builtins.end();
+  }
+
+  const BuiltIn& BuiltIns::at(const Location& name) const
+  {
+    return m_builtins.at(name);
   }
 }
\ No newline at end of file
diff --git a/src/builtins/numbers.cc b/src/builtins/numbers.cc
new file mode 100644
index 00000000..0d8319a5
--- /dev/null
+++ b/src/builtins/numbers.cc
@@ -0,0 +1,200 @@
+#include "register.h"
+#include "resolver.h"
+
+namespace
+{
+  using namespace rego;
+
+  Node abs(const Nodes& args)
+  {
+    auto maybe_number = Resolver::maybe_unwrap_number(args[0]);
+    if (!maybe_number.has_value())
+    {
+      return err(args[0], "Not a number");
+    }
+
+    Node number = maybe_number.value();
+    if (number->type() == JSONInt)
+    {
+      BigInt value = Resolver::get_int(number);
+      if (value.is_negative())
+      {
+        value = value.negate();
+      }
+      return JSONInt ^ value.loc();
+    }
+    else
+    {
+      double value = Resolver::get_double(number);
+      if (value < 0)
+      {
+        value *= -1;
+      }
+      return JSONFloat ^ std::to_string(value);
+    }
+  }
+
+  Node ceil(const Nodes& args)
+  {
+    auto maybe_number = Resolver::maybe_unwrap_number(args[0]);
+    if (!maybe_number.has_value())
+    {
+      return err(args[0], "Not a number");
+    }
+
+    Node number = maybe_number.value();
+    if (number->type() == JSONInt)
+    {
+      return number;
+    }
+    else
+    {
+      BigInt value(
+        static_cast<std::int64_t>(std::ceil(Resolver::get_double(number))));
+      return JSONInt ^ value.loc();
+    }
+  }
+
+  Node floor(const Nodes& args)
+  {
+    auto maybe_number = Resolver::maybe_unwrap_number(args[0]);
+    if (!maybe_number.has_value())
+    {
+      return err(args[0], "Not a number");
+    }
+
+    Node number = maybe_number.value();
+    if (number->type() == JSONInt)
+    {
+      return number;
+    }
+    else
+    {
+      BigInt value(
+        static_cast<std::int64_t>(std::floor(Resolver::get_double(number))));
+      return JSONInt ^ value.loc();
+    }
+  }
+
+  Node round(const Nodes& args)
+  {
+    auto maybe_number = Resolver::maybe_unwrap_number(args[0]);
+    if (!maybe_number.has_value())
+    {
+      return err(args[0], "Not a number");
+    }
+
+    Node number = maybe_number.value();
+    if (number->type() == JSONInt)
+    {
+      return number;
+    }
+    else
+    {
+      BigInt value(
+        static_cast<std::int64_t>(std::round(Resolver::get_double(number))));
+      return JSONInt ^ value.loc();
+    }
+  }
+
+  Node numbers_range(const Nodes& args)
+  {
+    auto maybe_lhs_number = Resolver::maybe_unwrap_number(args[0]);
+    if (!maybe_lhs_number.has_value())
+    {
+      return err(args[0], "Not a number");
+    }
+    auto maybe_rhs_number = Resolver::maybe_unwrap_number(args[1]);
+    if (!maybe_rhs_number.has_value())
+    {
+      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];
+    if (lhs < rhs)
+    {
+      BigInt curr = lhs;
+      while (curr < rhs)
+      {
+        array->push_back(Term << (Scalar << (JSONInt ^ curr.loc())));
+        curr = curr.increment();
+      }
+      array->push_back(Term << (Scalar << (JSONInt ^ curr.loc())));
+    }
+    else
+    {
+      BigInt curr = lhs;
+      while (curr > rhs)
+      {
+        array->push_back(Term << (Scalar << (JSONInt ^ curr.loc())));
+        curr = curr.decrement();
+      }
+      array->push_back(Term << (Scalar << (JSONInt ^ curr.loc())));
+    }
+
+    return array;
+  }
+
+  Node rand_intn(const Nodes& args)
+  {
+    auto maybe_seed_string = Resolver::maybe_unwrap_string(args[0]);
+    if (!maybe_seed_string.has_value())
+    {
+      return err(args[0], "Not a string");
+    }
+
+    auto maybe_n_number = Resolver::maybe_unwrap_number(args[1]);
+    if (!maybe_n_number.has_value())
+    {
+      return err(args[1], "Not an number");
+    }
+
+    std::string seed_string =
+      strip_quotes(std::string(maybe_seed_string.value()->location().view()));
+    std::size_t n = BigInt(args[1]->location()).to_size();
+    std::hash<std::string> hash;
+    auto seed = static_cast<std::mt19937::result_type>(hash(seed_string));
+    std::mt19937 rng(seed);
+    std::size_t value = rng() % n;
+    return JSONInt ^ std::to_string(value);
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> numbers()
+    {
+      return {
+        BuiltInDef::create(Location("abs"), 1, abs),
+        BuiltInDef::create(Location("ceil"), 1, ceil),
+        BuiltInDef::create(Location("floor"), 1, floor),
+        BuiltInDef::create(Location("round"), 1, round),
+        BuiltInDef::create(Location("numbers.range"), 2, numbers_range),
+        BuiltInDef::create(Location("rand.intn"), 2, rand_intn)};
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/register.h b/src/builtins/register.h
new file mode 100644
index 00000000..b3b33f45
--- /dev/null
+++ b/src/builtins/register.h
@@ -0,0 +1,12 @@
+#pragma once
+
+#include "builtins.h"
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> numbers();
+    std::vector<BuiltIn> sets();
+  }
+}
\ No newline at end of file
diff --git a/src/builtins/sets.cc b/src/builtins/sets.cc
new file mode 100644
index 00000000..a59db013
--- /dev/null
+++ b/src/builtins/sets.cc
@@ -0,0 +1,36 @@
+#include "register.h"
+#include "resolver.h"
+
+namespace
+{
+  using namespace rego;
+
+  Node intersection(const Nodes& args)
+  {
+    return Resolver::set_intersection(args[0], args[1]);
+  }
+
+  Node union_(const Nodes& args)
+  {
+    return Resolver::set_union(args[0], args[1]);
+  }
+
+  Node difference(const Nodes& args)
+  {
+    return Resolver::set_difference(args[0], args[1]);
+  }
+}
+
+namespace rego
+{
+  namespace builtins
+  {
+    std::vector<BuiltIn> sets()
+    {
+      return {
+        BuiltInDef::create(Location("intersection"), 2, intersection),
+        BuiltInDef::create(Location("union"), 2, union_),
+        BuiltInDef::create(Location("set_diff"), 2, difference)};
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/interpreter.cc b/src/interpreter.cc
index 1b2924e7..01413aa7 100644
--- a/src/interpreter.cc
+++ b/src/interpreter.cc
@@ -1,6 +1,7 @@
 #include "interpreter.h"
 
 #include "lang.h"
+#include "trieste/ast.h"
 #include "wf.h"
 
 #include <iostream>
@@ -11,7 +12,6 @@ namespace rego
   Interpreter::Interpreter(bool disable_well_formed_checks) :
     m_parser(parser()),
     m_wf_parser(wf_parser),
-    m_passes(passes()),
     m_module_seq(NodeDef::create(ModuleSeq)),
     m_data_seq(NodeDef::create(DataSeq)),
     m_input(NodeDef::create(Input)),
@@ -20,6 +20,7 @@ namespace rego
     m_well_formed_checks_enabled(!disable_well_formed_checks)
   {
     wf::push_back(&wf_parser);
+    m_builtins.register_standard_builtins();
   }
 
   Interpreter::~Interpreter()
@@ -107,22 +108,28 @@ namespace rego
     m_input->push_back(node);
   }
 
-  bool Interpreter::has_error(const Node& node) const
+  Node Interpreter::get_errors(const Node& node) const
   {
     if (node->type() == Error)
     {
-      return true;
+      return node->clone();
     }
 
+    Node errorseq = NodeDef::create(ErrorSeq);
     for (auto& child : *node)
     {
-      if (has_error(child))
+      Node error = get_errors(child);
+      if (error->type() == Error)
       {
-        return true;
+        errorseq->push_back(error);
+      }
+      else if (error->size() > 0)
+      {
+        errorseq->insert(errorseq->end(), error->begin(), error->end());
       }
     }
 
-    return false;
+    return errorseq;
   }
 
   Node Interpreter::raw_query(const std::string& query_expr) const
@@ -162,14 +169,13 @@ namespace rego
     write_ast(0, "parse", ast);
     if (!ok)
     {
-      std::ostringstream buf;
-      ast->errors(buf);
-      throw std::runtime_error(buf.str());
+      return get_errors(ast);
     }
 
-    for (std::size_t i = 0; i < m_passes.size(); ++i)
+    auto passes = rego::passes(m_builtins);
+    for (std::size_t i = 0; i < passes.size(); ++i)
     {
-      auto& [pass_name, pass, wf] = m_passes[i];
+      auto& [pass_name, pass, wf] = passes[i];
       wf::push_back(wf);
       auto [new_ast, count, changes] = pass->run(ast);
       wf::pop_front();
@@ -183,17 +189,23 @@ namespace rego
         ok = wf->check(ast, std::cout) && ok;
       }
 
-      if (has_error(ast))
+      Node errors = get_errors(ast);
+      if (errors->size() > 0)
       {
         ok = false;
       }
 
       if (!ok)
       {
-        std::ostringstream buf;
-        buf << "Failed at pass " << pass_name << std::endl;
-        ast->errors(buf);
-        throw std::runtime_error(buf.str());
+        if (errors->size() == 0)
+        {
+          std::ostringstream error;
+          error << "Failed at pass " << pass_name << std::endl;
+          ast->errors(error);
+          errors->push_back(err(ast, error.str(), "well_formed_error"));
+        }
+
+        return errors;
       }
     }
 
@@ -204,9 +216,26 @@ namespace rego
   {
     Node ast = raw_query(query_expr);
     std::ostringstream result_buf;
-    for (auto result : *ast)
+    if (ast->type() == ErrorSeq)
     {
-      result_buf << rego::to_json(result) << std::endl;
+      result_buf << "errors:" << std::endl;
+
+      for (auto& error : *ast)
+      {
+        result_buf << "---" << std::endl;
+        result_buf << "error: " << (error / ErrorMsg)->location().view()
+                   << std::endl;
+        result_buf << "code: " << (error / ErrorCode)->location().view()
+                   << std::endl;
+        result_buf << error / ErrorAst;
+      }
+    }
+    else
+    {
+      for (auto result : *ast)
+      {
+        result_buf << rego::to_json(result) << std::endl;
+      }
     }
 
     return result_buf.str();
@@ -215,6 +244,15 @@ namespace rego
   Interpreter& Interpreter::debug_path(const std::filesystem::path& path)
   {
     m_debug_path = path;
+    if (!m_debug_path.empty())
+    {
+      if (std::filesystem::is_directory(m_debug_path))
+      {
+        std::filesystem::remove_all(m_debug_path);
+      }
+
+      std::filesystem::create_directory(m_debug_path);
+    }
     return *this;
   }
 
@@ -253,11 +291,6 @@ namespace rego
       return;
     }
 
-    if (!std::filesystem::is_directory(m_debug_path))
-    {
-      std::filesystem::create_directory(m_debug_path);
-    }
-
     std::filesystem::path output;
     if (index < 10)
     {
@@ -290,4 +323,14 @@ namespace rego
   {
     return m_parser.executable();
   }
+
+  BuiltIns& Interpreter::builtins()
+  {
+    return m_builtins;
+  }
+
+  const BuiltIns& Interpreter::builtins() const
+  {
+    return m_builtins;
+  }
 }
diff --git a/src/lang.cc b/src/lang.cc
index 2b5119e0..f274cf56 100644
--- a/src/lang.cc
+++ b/src/lang.cc
@@ -27,7 +27,7 @@ namespace rego
   bool Logger::enabled = false;
   std::string Logger::indent = "";
 
-  std::vector<PassCheck> passes()
+  std::vector<PassCheck> passes(const BuiltIns& builtins)
   {
     return {
       {"input_data", input_data(), &wf_pass_input_data},
@@ -53,11 +53,12 @@ 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(), &wf_pass_skips},
+      {"skips", skips(builtins), &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(), &wf_pass_assign},
+      {"assign", assign(builtins), &wf_pass_assign},
       {"skip_refs", skip_refs(), &wf_pass_skip_refs},
       {"simple_refs", simple_refs(), &wf_pass_simple_refs},
       {"implicit_enums", implicit_enums(), &wf_pass_implicit_enums},
@@ -65,12 +66,12 @@ namespace rego
       {"rulebody", rulebody(), &wf_pass_rulebody},
       {"lift_to_rule", lift_to_rule(), &wf_pass_lift_to_rule},
       {"functions", functions(), &wf_pass_functions},
-      {"unify", unify(), &wf_pass_unify},
+      {"unify", unify(builtins), &wf_pass_unify},
       {"query", query(), &wf_pass_query},
     };
   }
 
-  Driver& driver()
+  Driver& driver(const BuiltIns& builtins)
   {
     static Driver d(
       "rego",
@@ -101,11 +102,12 @@ 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(), wf_pass_skips},
+        {"skips", skips(builtins), 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(), wf_pass_assign},
+        {"assign", assign(builtins), wf_pass_assign},
         {"skip_refs", skip_refs(), wf_pass_skip_refs},
         {"simple_refs", simple_refs(), wf_pass_simple_refs},
         {"implicit_enums", implicit_enums(), wf_pass_implicit_enums},
@@ -113,13 +115,13 @@ namespace rego
         {"rulebody", rulebody(), wf_pass_rulebody},
         {"lift_to_rule", lift_to_rule(), wf_pass_lift_to_rule},
         {"functions", functions(), wf_pass_functions},
-        {"unify", unify(), wf_pass_unify},
+        {"unify", unify(builtins), wf_pass_unify},
         {"query", query(), wf_pass_query},
       });
     return d;
   }
 
-  std::string to_json(const Node& node)
+  std::string to_json(const Node& node, bool sort)
   {
     std::ostringstream buf;
     if (node->type() == JSONInt)
@@ -164,11 +166,21 @@ namespace rego
     }
     else if (node->type() == Array || node->type() == DataArray)
     {
+      std::vector<std::string> items;
+      for (const auto& child : *node)
+      {
+        items.push_back(to_json(child, sort));
+      }
+
+      if (sort)
+      {
+        std::sort(items.begin(), items.end());
+      }
       buf << "[";
       std::string sep = "";
-      for (const auto& child : *node)
+      for (const auto& item : items)
       {
-        buf << sep << to_json(child);
+        buf << sep << item;
         sep = ", ";
       }
       buf << "]";
@@ -178,7 +190,7 @@ namespace rego
       std::set<std::string> items;
       for (const auto& child : *node)
       {
-        items.insert(to_json(child));
+        items.insert(to_json(child, sort));
       }
 
       buf << "[";
@@ -198,7 +210,7 @@ namespace rego
       {
         auto key = wfi / child / Key;
         auto value = wfi / child / Val;
-        items[to_json(key)] = to_json(value);
+        items[to_json(key, sort)] = to_json(value, sort);
       }
 
       buf << "{";
@@ -215,12 +227,12 @@ namespace rego
       node->type() == Scalar || node->type() == Term ||
       node->type() == DataTerm)
     {
-      return to_json(node->front());
+      return to_json(node->front(), sort);
     }
     else if (node->type() == Binding)
     {
       buf << (wfi / node / Var)->location().view() << " = "
-          << to_json(wfi / node / Term);
+          << to_json(wfi / node / Term, sort);
     }
     else if (node->type() == TermSet)
     {
@@ -228,7 +240,7 @@ namespace rego
       std::string sep = "";
       for (const auto& child : *node)
       {
-        buf << sep << to_json(child);
+        buf << sep << to_json(child, sort);
         sep = ", ";
       }
       buf << "}";
@@ -426,4 +438,15 @@ namespace rego
 
     return in_query(node->parent()->shared_from_this());
   }
+
+  Node err(NodeRange& r, const std::string& msg, const std::string& code)
+  {
+    return Error << (ErrorMsg ^ msg) << (ErrorAst << r) << (ErrorCode ^ code);
+  }
+
+  Node err(Node node, const std::string& msg, const std::string& code)
+  {
+    return Error << (ErrorMsg ^ msg) << (ErrorAst << node->clone())
+                 << (ErrorCode ^ code);
+  }
 }
\ No newline at end of file
diff --git a/src/parse.cc b/src/parse.cc
index b4800351..fc2bb613 100644
--- a/src/parse.cc
+++ b/src/parse.cc
@@ -85,6 +85,9 @@ namespace rego
         // And
         "&" >> [](auto& m) { m.add(And); },
 
+        // OPA local variable.
+        R"(__[[:alnum:]]+__\b)" >> [](auto& m) { m.add(Var); },
+
         // Placeholder
         "_" >> [](auto& m) { m.add(Placeholder); },
 
diff --git a/src/passes/add_subtract.cc b/src/passes/add_subtract.cc
deleted file mode 100644
index bda03617..00000000
--- a/src/passes/add_subtract.cc
+++ /dev/null
@@ -1,72 +0,0 @@
-#include "passes.h"
-
-namespace rego
-{
-  // Transforms addition, subtraction, and unary negation into ArithInfix Nodes.
-  PassDef add_subtract()
-  {
-    return {
-      In(Expr) *
-          (ArithInfixArg[Lhs] * (T(Add) / T(Subtract))[Op] *
-           ArithInfixArg[Rhs]) >>
-        [](Match& _) {
-          return ArithInfix << (ArithArg << _(Lhs)) << _(Op)
-                            << (ArithArg << _(Rhs));
-        },
-
-      In(Expr) *
-          (ArithInfixArg[Lhs] * (T(Add) / T(Subtract))[Op] * T(Subtract) *
-           ArithInfixArg[Rhs]) >>
-        [](Match& _) {
-          return ArithInfix
-            << (ArithArg << _(Lhs)) << _(Op)
-            << (ArithArg << (UnaryExpr << (ArithArg << _(Rhs))));
-        },
-
-      In(Expr) * (T(Subtract) * ArithInfixArg[Val] * End) >>
-        [](Match& _) { return UnaryExpr << (ArithArg << _(Val)); },
-
-      In(Expr) * (T(UnaryExpr) << (T(UnaryExpr) << Any[Val])) >>
-        [](Match& _) { return _(Val); },
-
-      In(ArithArg) *
-          (T(Expr)
-           << ((T(RefTerm) / T(NumTerm) / T(ArithInfix) / T(UnaryExpr) /
-                T(ExprCall))[Val] *
-               End)) >>
-        [](Match& _) { return _(Val); },
-
-      In(Expr) *
-          (BinInfixArg[Lhs] * (T(Subtract) / T(Or))[Op] * BinInfixArg[Rhs]) >>
-        [](Match& _) {
-          return BinInfix << (BinArg << _(Lhs)) << _(Op) << (BinArg << _(Rhs));
-        },
-
-      In(BinArg) *
-          (T(Expr)
-           << ((T(Ref) / T(RefTerm) / T(ExprCall) / T(Set) / T(SetCompr) /
-                T(BinInfix))[Val] *
-               End)) >>
-        [](Match& _) { return _(Val); },
-
-      // errors
-
-      (In(ArithArg) / In(Expr)) * (T(Add) / T(Subtract))[Op] >>
-        [](Match& _) { return err(_(Op), "Invalid operator"); },
-
-      (In(BinArg) / In(Expr)) * (T(Or) / T(And) / T(Subtract))[Op] >>
-        [](Match& _) { return err(_(Op), "Invalid operator"); },
-
-      T(ArithArg)[ArithArg] << (Any * Any) >>
-        [](Match& _) {
-          return err(_(ArithArg), "Argument can only have one element");
-        },
-
-      T(BinArg)[BinArg] << (Any * Any) >>
-        [](Match& _) {
-          return err(_(BinArg), "Argument can only have one element");
-        },
-    };
-  }
-
-}
\ No newline at end of file
diff --git a/src/passes/arithmetic.cc b/src/passes/arithmetic.cc
new file mode 100644
index 00000000..15fa3f72
--- /dev/null
+++ b/src/passes/arithmetic.cc
@@ -0,0 +1,121 @@
+#include "passes.h"
+
+namespace rego
+{
+  const inline auto AllOps =
+    T(Add) / T(Subtract) / T(Multiply) / T(Divide) / T(Modulo) / T(Unify);
+
+  // Finds unary expressions
+  PassDef unary()
+  {
+    return {
+      In(Expr) * (Start * T(Subtract) * ArithInfixArg[Val]) >>
+        [](Match& _) { return UnaryExpr << (ArithArg << _(Val)); },
+
+      In(Expr) * (AllOps[Op] * T(Subtract) * ArithInfixArg[Val]) >>
+        [](Match& _) {
+          return Seq << _(Op) << (UnaryExpr << (ArithArg << _(Val)));
+        },
+    };
+  }
+
+  const inline auto Ops = T(Multiply) / T(Divide) / T(Modulo);
+
+  // Processes multiply, divide, and modulo operations into ArithInfix nodes.
+  PassDef multiply_divide()
+  {
+    return {
+      In(Expr) * (ArithInfixArg[Lhs] * Ops[Op] * ArithInfixArg[Rhs]) >>
+        [](Match& _) {
+          return ArithInfix << (ArithArg << _(Lhs)) << _(Op)
+                            << (ArithArg << _(Rhs));
+        },
+
+      T(Expr) << (T(Expr)[Expr] * End) >> [](Match& _) { return _(Expr); },
+
+      In(Expr) * (BinInfixArg[Lhs] * T(And) * BinInfixArg[Rhs]) >>
+        [](Match& _) {
+          return BinInfix << (BinArg << _(Lhs)) << And << (BinArg << _(Rhs));
+        },
+
+      // errors
+
+      In(Expr) * Ops[Op] >>
+        [](Match& _) { return err(_(Op), "Invalid multiply/divide"); },
+
+      In(Expr) * T(And)[And] >>
+        [](Match& _) { return err(_(And), "Invalid and"); },
+    };
+  }
+
+  // Transforms addition, subtraction, and unary negation into ArithInfix Nodes.
+  PassDef add_subtract()
+  {
+    return {
+      In(Expr) *
+          (ArithInfixArg[Lhs] * (T(Add) / T(Subtract))[Op] *
+           ArithInfixArg[Rhs]) >>
+        [](Match& _) {
+          return ArithInfix << (ArithArg << _(Lhs)) << _(Op)
+                            << (ArithArg << _(Rhs));
+        },
+
+      In(Expr) * (ArithInfixArg * T(Subtract) * BinInfixArg[Rhs]) >>
+        [](Match& _) {
+          return err(
+            _(Rhs), "operand 2 must be number but got set", "eval_type_error");
+        },
+
+      In(Expr) * (BinInfixArg * T(Subtract) * ArithInfixArg[Rhs]) >>
+        [](Match& _) {
+          return err(
+            _(Rhs), "operand 2 must be set but got number", "eval_type_error");
+        },
+
+      In(Expr) * (T(Subtract) * ArithInfixArg[Val] * End) >>
+        [](Match& _) { return UnaryExpr << (ArithArg << _(Val)); },
+
+      In(Expr) * (T(UnaryExpr) << (T(UnaryExpr) << Any[Val])) >>
+        [](Match& _) { return _(Val); },
+
+      In(ArithArg) *
+          (T(Expr)
+           << ((T(RefTerm) / T(NumTerm) / T(ArithInfix) / T(UnaryExpr) /
+                T(ExprCall))[Val] *
+               End)) >>
+        [](Match& _) { return _(Val); },
+
+      In(Expr) *
+          (BinInfixArg[Lhs] * (T(Subtract) / T(Or))[Op] * BinInfixArg[Rhs]) >>
+        [](Match& _) {
+          return BinInfix << (BinArg << _(Lhs)) << _(Op) << (BinArg << _(Rhs));
+        },
+
+      In(BinArg) *
+          (T(Expr)
+           << ((T(Ref) / T(RefTerm) / T(ExprCall) / T(Set) / T(SetCompr) /
+                T(BinInfix))[Val] *
+               End)) >>
+        [](Match& _) { return _(Val); },
+
+      // errors
+
+      In(Expr) * (T(Add) / T(Subtract))[Op] >>
+        [](Match& _) { return err(_(Op), "Invalid add/subtract"); },
+
+      In(Expr) * T(Or)[Op] >>
+        [](Match& _) { return err(_(Op), "Invalid set union"); },
+
+      T(ArithArg)[ArithArg] << (Any * Any) >>
+        [](Match& _) {
+          return err(_(ArithArg), "Argument can only have one element");
+        },
+
+      T(BinArg)[BinArg] << (Any * Any) >>
+        [](Match& _) {
+          return err(_(BinArg), "Argument can only have one element");
+        },
+    };
+  }
+
+}
\ No newline at end of file
diff --git a/src/passes/assign.cc b/src/passes/assign.cc
index 89db1746..719cadbb 100644
--- a/src/passes/assign.cc
+++ b/src/passes/assign.cc
@@ -3,6 +3,64 @@
 
 #include <sstream>
 
+namespace
+{
+  using namespace rego;
+
+  Location concat(const Node& varseq)
+  {
+    std::ostringstream path;
+    std::string sep = "";
+    for (Node var : *varseq)
+    {
+      path << sep << var->location().view();
+      sep = ".";
+    }
+
+    return Location(path.str());
+  }
+
+  bool needs_rewrite(
+    const NodeRange& n,
+    const BuiltIns& builtins,
+    const std::shared_ptr<NodeMap<bool>>& cache)
+  {
+    Node varseq = n.first[0];
+    Node argseq = n.first[1];
+    if (cache->contains(varseq))
+    {
+      return cache->at(varseq);
+    }
+
+    if (!is_in(varseq, {UnifyBody}))
+    {
+      cache->insert({varseq, false});
+      return false;
+    }
+
+    Location path = concat(varseq);
+    if (!builtins.is_builtin(path))
+    {
+      cache->insert({varseq, false});
+      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;
+
+    cache->insert({varseq, result});
+    return result;
+  }
+}
+
 namespace rego
 {
   const inline auto AssignInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
@@ -10,8 +68,10 @@ namespace rego
     T(Enumerate);
 
   // Transforms unification expressions into AssignInfix nodes.
-  PassDef assign()
+  PassDef assign(const BuiltIns& builtins)
   {
+    auto cache = std::make_shared<NodeMap<bool>>();
+
     return {
       In(Expr) *
           (AssignInfixArg[Head](
@@ -34,19 +94,66 @@ namespace rego
           return AssignInfix << (AssignArg << _(Lhs)) << (AssignArg << _(Rhs));
         },
 
-      In(UnifyBody) *
-          (T(Literal) << (T(Expr) << (AssignInfixArg[Arg] * End))) >>
+      In(Expr) *
+          (AssignInfixArg[Lhs] * T(Unify) * (T(Set) / T(SetCompr))[Rhs]) >>
+        [](Match& _) {
+          return AssignInfix << (AssignArg << _(Lhs))
+                             << (AssignArg << (Term << _(Rhs)));
+        },
+
+      In(Expr) *
+          ((T(Set) / T(SetCompr))[Lhs] * T(Unify) * AssignInfixArg[Rhs]) >>
+        [](Match& _) {
+          return AssignInfix << (AssignArg << (Term << _(Lhs)))
+                             << (AssignArg << _(Rhs));
+        },
+
+      In(Literal) *
+          (T(Expr)
+           << (T(ExprCall) << (T(VarSeq)[VarSeq] * T(ArgSeq)[ArgSeq])(
+                 [builtins, cache](auto& n) {
+                   return needs_rewrite(n, builtins, cache);
+                 }))) >>
+        [builtins](Match& _) {
+          Node varseq = VarSeq << (Var ^ concat(_(VarSeq)));
+          Node argseq = _(ArgSeq);
+          Node var = argseq->pop_back();
+          return Expr
+            << (AssignInfix << (AssignArg << var->front())
+                            << (AssignArg << (ExprCall << varseq << argseq)));
+        },
+
+      In(LiteralNot) *
+          (T(Expr)
+           << (T(ExprCall) << (T(VarSeq)[VarSeq] * T(ArgSeq)[ArgSeq])(
+                 [builtins, cache](auto& n) {
+                   return needs_rewrite(n, builtins, cache);
+                 }))) >>
+        [builtins](Match& _) {
+          Node varseq = VarSeq << (Var ^ concat(_(VarSeq)));
+          Node argseq = _(ArgSeq);
+          Node var = argseq->pop_back();
+          return Seq << (Lift
+                         << UnifyBody
+                         << (Literal
+                             << (Expr
+                                 << (AssignInfix
+                                     << (AssignArg << var->front())
+                                     << (AssignArg
+                                         << (ExprCall << varseq << argseq))))))
+                     << var->clone();
+        },
+
+      In(Literal) * (T(Expr) << (AssignInfixArg[Arg] * End)) >>
         [](Match& _) {
-          Node seq = NodeDef::create(Seq);
           std::string prefix = in_query(_(Arg)) ? "value" : "unify";
           Location temp = _.fresh({prefix});
-          seq->push_back(Local << (Var ^ temp) << Undefined);
-          seq->push_back(
-            Literal
-            << (Expr
-                << (AssignInfix << (AssignArg << (RefTerm << (Var ^ temp)))
-                                << (AssignArg << _(Arg)))));
-          return seq;
+          return Seq << (Lift << UnifyBody
+                              << (Local << (Var ^ temp) << Undefined))
+                     << (Expr
+                         << (AssignInfix
+                             << (AssignArg << (RefTerm << (Var ^ temp)))
+                             << (AssignArg << _(Arg))));
         },
 
       // errors
@@ -65,6 +172,9 @@ namespace rego
 
       In(Expr) * T(Enumerate)[Enumerate] >>
         [](Match& _) { return err(_(Enumerate), "Invalid enumeration"); },
+
+      In(Expr) * (T(Set) / T(SetCompr))[Set] >>
+        [](Match& _) { return err(_(Set), "Invalid set in expression"); },
     };
   }
 }
\ No newline at end of file
diff --git a/src/passes/comparison.cc b/src/passes/comparison.cc
index 994860ea..0c86f3f3 100644
--- a/src/passes/comparison.cc
+++ b/src/passes/comparison.cc
@@ -4,7 +4,7 @@ namespace rego
 {
   const inline auto NotArg = ArithInfixArg / T(BoolInfix);
   const inline auto BoolInfixArg = T(RefTerm) / T(NumTerm) / T(UnaryExpr) /
-    T(ArithInfix) / T(Term) / T(ExprCall) / T(BinInfix);
+    T(ArithInfix) / T(Term) / T(ExprCall) / T(BinInfix) / T(Set) / T(SetCompr);
   const inline auto RefArgBrackArg =
     T(Scalar) / T(Var) / T(Object) / T(Array) / T(Set);
 
@@ -12,8 +12,8 @@ namespace rego
   PassDef comparison()
   {
     return {
-      In(Expr) * (T(Expr) << (BoolInfixArg[Arg] * End)) >>
-        [](Match& _) { return _(Arg); },
+      In(UnifyBody) * (T(Literal) << (T(Expr) << (T(Not) * Any++[Expr]))) >>
+        [](Match& _) { return LiteralNot << (Expr << _[Expr]); },
 
       In(Literal) *
           (T(Expr)
@@ -32,6 +32,9 @@ namespace rego
                               << _(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)
@@ -39,13 +42,13 @@ namespace rego
         },
 
       In(Expr) *
-          (~T(Not)[Not] * BoolInfixArg[Lhs]([](auto& n) {
-            return is_in(*n.first, {UnifyBody});
-          }) *
+          (BoolInfixArg[Lhs](
+             [](auto& n) { return is_in(*n.first, {UnifyBody}); }) *
            T(MemberOf) * BoolInfixArg[Rhs]) >>
         [](Match& _) {
           Location item = _.fresh({"item"});
-          Location unify = _.fresh({_(Not) == nullptr ? "unify" : "not"});
+          Location unify =
+            _.fresh({is_in(_(Lhs), {LiteralNot}) ? "not" : "unify"});
           Node seq = Seq
             << (Lift << UnifyBody << (Local << (Var ^ item) << Undefined))
             << (Lift << UnifyBody << (Local << (Var ^ unify) << Undefined))
@@ -65,44 +68,38 @@ namespace rego
                                                      << (JSONInt ^ "1")))))))))
             << (RefTerm << (Var ^ unify)) << Unify << _(Lhs);
 
-          if (_(Not) != nullptr)
-          {
-            seq
-              << (Lift << UnifyBody
-                       << (Literal
-                           << (NotExpr
-                               << (Expr << (RefTerm << (Var ^ unify))))));
-          }
-
           return seq;
         },
 
+      In(Expr) * (T(Set) / T(SetCompr))[Set] >>
+        [](Match& _) { return Term << _(Set); },
+
       In(RefArgBrack) * (T(Term) << RefArgBrackArg[Arg]) >>
         [](Match& _) { return _(Arg); },
 
-      In(Literal) * (T(Expr) << (T(Not) * NotArg[Val])) >>
-        [](Match& _) { return NotExpr << (Expr << _(Val)); },
-
       (In(ArithArg) / In(BoolArg)) * (T(Expr) << ArithInfixArg[Val]) >>
         [](Match& _) { return _(Val); },
 
       // errors
 
+      T(Expr)[Expr] << End >>
+        [](Match& _) { return err(_(Expr), "Empty expression"); },
+
       In(Expr) * BoolToken[Op] >>
         [](Match& _) { return err(_(Op), "Invalid comparison"); },
 
       In(Expr) * T(Not)[Not] >>
         [](Match& _) { return err(_(Not), "Invalid not"); },
 
-      In(Literal) * (T(NotExpr)[NotExpr] << End) >>
-        [](Match& _) { return err(_(NotExpr), "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"); },
 
+      In(BoolArg) * (T(Set) / T(SetCompr))[Set] >>
+        [](Match& _) { return err(_(Set), "Invalid boolean argument"); },
+
       In(ArithArg) * T(Expr)[Expr] >>
         [](Match& _) { return err(_(Expr), "Invalid argument"); },
 
diff --git a/src/passes/enumerate.cc b/src/passes/enumerate.cc
index ff2c2ce9..c906332a 100644
--- a/src/passes/enumerate.cc
+++ b/src/passes/enumerate.cc
@@ -30,9 +30,8 @@ namespace
     }
   }
 
-  bool can_grab(Node local)
+  bool can_grab(Node local, Node unifybody)
   {
-    Node unifybody = local->parent()->shared_from_this();
     Nodes refs;
     all_refs(local->scope(), (local / Var)->location(), refs);
     for (auto& ref : refs)
@@ -42,7 +41,7 @@ namespace
         continue;
       }
 
-      Node common_parent = local->common_parent(ref);
+      Node common_parent = unifybody->common_parent(ref);
       if (common_parent != unifybody)
       {
         return false;
@@ -51,6 +50,22 @@ namespace
 
     return true;
   }
+
+  Node next_enum(Node local)
+  {
+    Node unifybody = local->parent()->shared_from_this();
+    auto it = unifybody->find(local) + 1;
+    while (it != unifybody->end())
+    {
+      Node node = *it;
+      if (node->type() == LiteralEnum)
+      {
+        return node;
+      }
+      ++it;
+    }
+    return {};
+  }
 }
 
 namespace rego
@@ -138,7 +153,29 @@ namespace rego
         },
 
       In(UnifyBody) * T(Local)[Local]([](auto& n) {
-        return is_in(*n.first, {LiteralEnum}) && !can_grab(*n.first);
+        Node local = *n.first;
+        if ((local / Var)->location().view().starts_with("out$"))
+        {
+          return false;
+        }
+
+        Node literalenum = next_enum(local);
+        if (literalenum == nullptr)
+        {
+          return false;
+        }
+
+        return can_grab(local, literalenum / UnifyBody);
+      }) >>
+        [](Match& _) {
+          Node unifybody = next_enum(_(Local)) / UnifyBody;
+          unifybody->push_front(_(Local));
+          return Node{};
+        },
+
+      In(UnifyBody) * T(Local)[Local]([](auto& n) {
+        Node unifybody = (*n.first)->parent()->shared_from_this();
+        return is_in(*n.first, {LiteralEnum}) && !can_grab(*n.first, unifybody);
       }) >>
         [](Match& _) { return Lift << LiteralEnum << _(Local); },
 
diff --git a/src/passes/functions.cc b/src/passes/functions.cc
index 8c92809f..d00cbef1 100644
--- a/src/passes/functions.cc
+++ b/src/passes/functions.cc
@@ -160,18 +160,14 @@ namespace rego
           return seq;
         },
 
-      In(UnifyExpr) * (T(NotExpr) << T(Expr)[Expr]) >>
+      In(UnifyBody) * (T(UnifyExprNot) << (T(Var)[Lhs] * T(Expr)[Rhs])) >>
         [](Match& _) {
-          Node seq = NodeDef::create(Seq);
           Location temp = _.fresh({"expr"});
-          seq->push_back(
-            Lift << UnifyBody << (Local << (Var ^ temp) << Undefined));
-          seq->push_back(
-            Lift << UnifyBody << (UnifyExpr << (Var ^ temp) << _(Expr)));
-          seq->push_back(
-            Function << (JSONString ^ "not") << (ArgSeq << (Var ^ temp)));
-
-          return seq;
+          return Seq << (Local << (Var ^ temp) << Undefined)
+                     << (UnifyExpr << (Var ^ temp) << _(Rhs))
+                     << (UnifyExpr << _(Lhs)
+                                   << (Function << (JSONString ^ "not")
+                                                << (ArgSeq << (Var ^ temp))));
         },
 
       In(UnifyExpr) *
diff --git a/src/passes/multiply_divide.cc b/src/passes/multiply_divide.cc
deleted file mode 100644
index 735a1c17..00000000
--- a/src/passes/multiply_divide.cc
+++ /dev/null
@@ -1,42 +0,0 @@
-#include "passes.h"
-
-namespace rego
-{
-  const inline auto Ops = T(Multiply) / T(Divide) / T(Modulo);
-
-  // Processes multiply, divide, and modulo operations into ArithInfix nodes.
-  PassDef multiply_divide()
-  {
-    return {
-      In(Expr) * (ArithInfixArg[Lhs] * Ops[Op] * ArithInfixArg[Rhs]) >>
-        [](Match& _) {
-          return ArithInfix << (ArithArg << _(Lhs)) << _(Op)
-                            << (ArithArg << _(Rhs));
-        },
-
-      T(Expr) << (T(Expr)[Expr] * End) >> [](Match& _) { return _(Expr); },
-
-      In(Expr) *
-          (ArithInfixArg[Lhs] * Ops[Op] * T(Subtract) * ArithInfixArg[Rhs]) >>
-        [](Match& _) {
-          return ArithInfix
-            << (ArithArg << _(Lhs)) << _(Op)
-            << (ArithArg << (UnaryExpr << (ArithArg << _(Rhs))));
-        },
-
-      In(Expr) * (BinInfixArg[Lhs] * T(And) * BinInfixArg[Rhs]) >>
-        [](Match& _) {
-          return BinInfix << (BinArg << _(Lhs)) << And << (BinArg << _(Rhs));
-        },
-
-      // errors
-
-      In(Expr) * Ops[Op] >>
-        [](Match& _) { return err(_(Op), "Invalid multiply/divide"); },
-
-      In(Expr) * T(And)[And] >>
-        [](Match& _) { return err(_(And), "Invalid and"); },
-    };
-  }
-
-}
\ No newline at end of file
diff --git a/src/passes/rulebody.cc b/src/passes/rulebody.cc
index 0a86ad0f..003c42a1 100644
--- a/src/passes/rulebody.cc
+++ b/src/passes/rulebody.cc
@@ -1,6 +1,7 @@
 #include "lang.h"
 #include "log.h"
 #include "passes.h"
+#include "variable.h"
 
 namespace
 {
@@ -77,15 +78,41 @@ namespace rego
                      << (Var ^ temp);
         },
 
-      // <expr>|<notexpr>
-      In(UnifyBody) * (T(Literal) << (T(Expr) / T(NotExpr))[Expr]) >>
+      // <expr>
+      In(UnifyBody) * (T(Literal) << T(Expr)[Expr]) >>
         [](Match& _) {
-          Node seq = NodeDef::create(Seq);
           std::string prefix = in_query(_(Expr)) ? "value" : "unify";
           Location temp = _.fresh({prefix});
-          seq->push_back(Local << (Var ^ temp) << Undefined);
-          seq->push_back(UnifyExpr << (Var ^ temp) << _(Expr));
-          return seq;
+          return Seq << (Local << (Var ^ temp) << Undefined)
+                     << (UnifyExpr << (Var ^ temp) << _(Expr));
+        },
+
+      // not any = any
+      In(UnifyBody) *
+          (T(LiteralNot)
+           << (T(Expr)
+               << (T(AssignInfix)
+                   << (T(AssignArg)[Lhs] * T(AssignArg)[Rhs])))) >>
+        [](Match& _) {
+          LOG("not any = any");
+          std::string prefix = in_query(_(Lhs)) ? "value" : "unify";
+          Location unify = _.fresh({prefix});
+          Location temp = _.fresh({"not"});
+          return Seq << (Local << (Var ^ temp) << Undefined)
+                     << (Local << (Var ^ unify) << Undefined)
+                     << (UnifyExpr << (Var ^ temp) << (Expr << _(Lhs)->front()))
+                     << (UnifyExpr << (Var ^ temp) << (Expr << _(Rhs)->front()))
+                     << (UnifyExprNot << (Var ^ unify)
+                                      << (Expr << (RefTerm << (Var ^ temp))));
+        },
+
+      // <notexpr>
+      In(UnifyBody) * (T(LiteralNot) << T(Expr)[Expr]) >>
+        [](Match& _) {
+          std::string prefix = in_query(_(Expr)) ? "value" : "unify";
+          Location temp = _.fresh({prefix});
+          return Seq << (Local << (Var ^ temp) << Undefined)
+                     << (UnifyExprNot << (Var ^ temp) << _(Expr));
         },
 
       // <term> = <term>
@@ -117,7 +144,7 @@ namespace rego
           return AssignInfix << _(Rhs) << _(Lhs);
         },
 
-      // a = <term>
+      // a = any
       In(UnifyBody) *
           (T(AssignInfix)
            << ((T(AssignArg) << (T(RefTerm) << T(Var)[Lhs]([](auto& n) {
@@ -125,11 +152,22 @@ namespace rego
                                  }))) *
                T(AssignArg)[Rhs])) >>
         [](Match& _) {
-          LOG("a = <term>");
-          return UnifyExpr << _(Lhs) << (Expr << wfi / _(Rhs) / Val);
+          LOG("a = any");
+          auto name = _(Lhs)->location().view();
+          if (Variable::is_unify(name) || Variable::is_user_var(name))
+          {
+            return UnifyExpr << _(Lhs) << (Expr << _(Rhs)->front());
+          }
+
+          Location unify = _.fresh({"unify"});
+          return Seq << (Local << (Var ^ unify) << Undefined)
+                     << (UnifyExpr << _(Lhs)->clone()
+                                   << (Expr << _(Rhs)->front()))
+                     << (UnifyExpr << (Var ^ unify)
+                                   << (Expr << (RefTerm << _(Lhs))));
         },
 
-      // <ref> = <term>
+      // <ref> = any
       In(UnifyBody) *
           (T(AssignInfix)
            << ((T(AssignArg) << (T(RefTerm)[Lhs] << T(SimpleRef)([](auto& n) {
@@ -137,14 +175,13 @@ namespace rego
                                  }))) *
                T(AssignArg)[Rhs])) >>
         [](Match& _) {
-          LOG("<ref> = <term>");
+          LOG("<ref> = any");
           Node seq = NodeDef::create(Seq);
-          Location temp = _.fresh({"ref"});
-          seq->push_back(Local << (Var ^ temp) << Undefined);
-          seq->push_back(UnifyExpr << (Var ^ temp) << (Expr << _(Lhs)));
-          seq->push_back(
-            UnifyExpr << (Var ^ temp) << (Expr << wfi / _(Rhs) / Val));
-          return seq;
+          Location unify = _.fresh({"unify"});
+          return Seq << (Local << (Var ^ unify) << Undefined)
+                     << (UnifyExpr << (Var ^ unify) << (Expr << _(Lhs)))
+                     << (UnifyExpr << (Var ^ unify)
+                                   << (Expr << wfi / _(Rhs) / Val));
         },
 
       // <array> = <array>
diff --git a/src/passes/rules.cc b/src/passes/rules.cc
index ec31f3fb..f21341fa 100644
--- a/src/passes/rules.cc
+++ b/src/passes/rules.cc
@@ -182,16 +182,21 @@ namespace rego
 
       In(Policy) *
           (T(Group)
-           << (T(Var)[Id] * (T(Array) << (T(Group)[Key] * End)) *
+           << (T(Var)[Id] * (T(Array) << (T(Group)[Item] * End)) *
                T(UnifyBody)[UnifyBody])) >>
         [](Match& _) {
-          return Rule << (RuleHead << _(Id)
-                                   << (RuleHeadObj << _(Key)
-                                                   << (AssignOperator << Assign)
-                                                   << (Group << JSONTrue)))
+          return Rule << (RuleHead << _(Id) << (RuleHeadSet << _(Item)))
                       << _(UnifyBody) << ElseSeq;
         },
 
+      In(Policy) *
+          (T(Group)
+           << (T(Var)[Id] * (T(Array) << (T(Group)[Item] * End)) * End)) >>
+        [](Match& _) {
+          return Rule << (RuleHead << _(Id) << (RuleHeadSet << _(Item)))
+                      << Empty << ElseSeq;
+        },
+
       In(Policy) *
           (T(Group)
            << (T(Var)[Id] * (T(Array) << (T(Group)[Key] * End)) *
diff --git a/src/passes/skip_refs.cc b/src/passes/skip_refs.cc
index 52adb2da..4105a5d9 100644
--- a/src/passes/skip_refs.cc
+++ b/src/passes/skip_refs.cc
@@ -16,7 +16,29 @@ namespace
     std::string path;
   };
 
-  SkipPrefix skip_prefix(const SkipSet& skips, Node ref)
+  SkipPrefix skip_prefix_varseq(const SkipSet& skips, Node varseq)
+  {
+    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))
+      {
+        skip = i + 1;
+        longest_path = path;
+      }
+    }
+
+    return {skip, longest_path};
+  }
+
+  SkipPrefix skip_prefix_ref(const SkipSet& skips, Node ref)
   {
     Node head = (ref / RefHead)->front();
     std::string path = std::string(head->location().view());
@@ -62,10 +84,10 @@ namespace rego
 
     PassDef skip_refs = {
       In(RefTerm) * T(Ref)[Ref]([skips](auto& n) {
-        return skip_prefix(skips, *n.first).length > 0;
+        return skip_prefix_ref(skips, *n.first).length > 0;
       }) >>
         [skips](Match& _) {
-          SkipPrefix skip = skip_prefix(skips, _(Ref));
+          SkipPrefix skip = skip_prefix_ref(skips, _(Ref));
           Node refargseq = (_(Ref) / RefArgSeq);
           refargseq->erase(
             refargseq->begin(), refargseq->begin() + skip.length);
@@ -76,7 +98,19 @@ 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;
+      }) >>
+        [skips](Match& _) {
+          SkipPrefix skip = skip_prefix_varseq(skips, _(VarSeq));
+          Node varseq = (_(VarSeq));
+          varseq->erase(varseq->begin(), varseq->begin() + skip.length);
+
+          varseq->push_front(Var ^ skip.path);
+
+          return varseq;
+        }};
 
     skip_refs.pre(Rego, [skips](Node node) {
       Node skipseq = node / SkipSeq;
diff --git a/src/passes/skips.cc b/src/passes/skips.cc
index 2a98c036..03a063ef 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()
+  PassDef skips(const BuiltIns& builtins)
   {
     SkipMap skip_links = std::make_shared<std::map<std::string, Node>>();
 
@@ -124,7 +124,6 @@ namespace rego
       dir::topdown | dir::once,
     };
 
-    BuiltIns builtins = BuiltIns().register_standard_builtins();
     for (auto [location, _] : builtins)
     {
       std::string name = std::string(location.view());
diff --git a/src/passes/symbols.cc b/src/passes/symbols.cc
index 578f876f..5b9fa14d 100644
--- a/src/passes/symbols.cc
+++ b/src/passes/symbols.cc
@@ -174,6 +174,9 @@ namespace rego
       In(Expr) * (T(Term) << (T(Scalar) << (T(JSONInt) / T(JSONFloat))[Val])) >>
         [](Match& _) { return NumTerm << _(Val); },
 
+      In(Expr) * (T(Term) << (T(Set) / T(SetCompr))[Set]) >>
+        [](Match& _) { return _(Set); },
+
       In(RefArgBrack) * T(Var)[Var] >>
         [](Match& _) { return RefTerm << _(Var); },
 
diff --git a/src/passes/unify.cc b/src/passes/unify.cc
index 443af3d3..17b1f2df 100644
--- a/src/passes/unify.cc
+++ b/src/passes/unify.cc
@@ -18,7 +18,7 @@ namespace
 namespace rego
 {
   // Performs unification.
-  PassDef unify()
+  PassDef unify(const BuiltIns& builtins)
   {
     PassDef unify = {
       dir::topdown | dir::once,
@@ -51,11 +51,11 @@ namespace rego
           [](Match& _) { return err(_(Query), "no values in query"); },
       }};
 
-    unify.pre(Rego, [](Node node) {
+    unify.pre(Rego, [builtins](Node node) {
       Node query = wfi / node / Query;
       try
       {
-        Node result = Resolver::resolve_query(query);
+        Node result = Resolver::resolve_query(query, builtins);
         node->replace(query, result);
       }
       catch (const std::exception& e)
diff --git a/src/resolver.cc b/src/resolver.cc
index ee9a9f23..39b08b81 100644
--- a/src/resolver.cc
+++ b/src/resolver.cc
@@ -31,9 +31,9 @@ namespace
     ;
   // clang-format on
 
-  std::int64_t get_int(const Node& node)
+  BigInt get_int(const Node& node)
   {
-    return std::stoll(to_json(node));
+    return BigInt(node->location());
   }
 
   double get_double(const Node& node)
@@ -41,9 +41,9 @@ namespace
     return std::stod(to_json(node));
   }
 
-  Node do_arith(const Node& op, std::int64_t lhs, std::int64_t rhs)
+  Node do_arith(const Node& op, BigInt lhs, BigInt rhs)
   {
-    std::int64_t value;
+    BigInt value;
     if (op->type() == Add)
     {
       value = lhs + rhs;
@@ -58,15 +58,24 @@ namespace
     }
     else if (op->type() == Divide)
     {
-      if (rhs == 0)
+      return err(op, "unsupported math operation");
+      /*
+      In case we need integer division in the future.
+      if (rhs.is_zero())
       {
-        return err(op, "divide by zero");
+        return err(op, "divide by zero", "eval_builtin_error");
       }
 
       value = lhs / rhs;
+      */
     }
     else if (op->type() == Modulo)
     {
+      if (rhs.is_zero())
+      {
+        return err(op, "modulo by zero", "eval_builtin_error");
+      }
+
       value = lhs % rhs;
     }
     else
@@ -74,7 +83,7 @@ namespace
       return err(op, "unsupported math operation");
     }
 
-    return JSONInt ^ std::to_string(value);
+    return JSONInt ^ value.loc();
   }
 
   Node do_arith(const Node& op, double lhs, double rhs)
@@ -96,15 +105,14 @@ namespace
     {
       if (rhs == 0.0)
       {
-        return err(op, "divide by zero");
+        return err(op, "divide by zero", "eval_builtin_error");
       }
 
       value = lhs / rhs;
     }
     else if (op->type() == Modulo)
     {
-      // current behavior of OPA interpreter is to return undefined
-      return Undefined;
+      return err(op, "modulo on floating-point number", "eval_builtin_error");
     }
     else
     {
@@ -112,11 +120,12 @@ namespace
     }
 
     std::ostringstream oss;
-    oss << std::setprecision(8) << std::noshowpoint << value;
+    oss << std::setprecision(std::numeric_limits<double>::max_digits10 - 1)
+        << std::noshowpoint << value;
     return JSONFloat ^ oss.str();
   }
 
-  Node do_bool(const Node& op, std::int64_t lhs, std::int64_t rhs)
+  Node do_bool(const Node& op, BigInt lhs, BigInt rhs)
   {
     bool value;
     if (op->type() == Equals)
@@ -245,15 +254,15 @@ namespace
 
 namespace rego
 {
-  std::int64_t Resolver::get_int(const Node& node)
+  BigInt Resolver::get_int(const Node& node)
   {
     assert(node->type() == JSONInt);
     return ::get_int(node);
   }
 
-  Node Resolver::scalar(std::int64_t value)
+  Node Resolver::scalar(BigInt value)
   {
-    return JSONInt ^ std::to_string(value);
+    return JSONInt ^ value.loc();
   }
 
   double Resolver::get_double(const Node& node)
@@ -265,7 +274,8 @@ namespace rego
   Node Resolver::scalar(double value)
   {
     std::ostringstream oss;
-    oss << std::setprecision(8) << std::noshowpoint << value;
+    oss << std::setprecision(std::numeric_limits<double>::max_digits10 - 1)
+        << std::noshowpoint << value;
     return JSONFloat ^ oss.str();
   }
 
@@ -302,9 +312,8 @@ namespace rego
   {
     if (node->type() == JSONInt)
     {
-      std::int64_t value = get_int(node);
-      value *= -1;
-      return JSONInt ^ std::to_string(value);
+      BigInt value = get_int(node);
+      return JSONInt ^ value.negate().loc();
     }
     else if (node->type() == JSONFloat)
     {
@@ -342,7 +351,9 @@ namespace rego
     {
       Node lhs_number = maybe_lhs_number.value();
       Node rhs_number = maybe_rhs_number.value();
-      if (lhs_number->type() == JSONInt && rhs_number->type() == JSONInt)
+      if (
+        lhs_number->type() == JSONInt && rhs_number->type() == JSONInt &&
+        op->type() != Divide)
       {
         return do_arith(op, get_int(lhs_number), get_int(rhs_number));
       }
@@ -360,6 +371,12 @@ namespace rego
         return bininfix(op, maybe_lhs_set.value(), maybe_rhs_set.value());
       }
 
+      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(
         op->parent()->shared_from_this(),
         "Cannot perform arithmetic operations on non-numeric values");
@@ -533,6 +550,11 @@ namespace rego
   std::optional<Nodes> Resolver::apply_access(
     const Node& container, const Node& arg)
   {
+    if (is_undefined(container))
+    {
+      return std::nullopt;
+    }
+
     if (container->type() == Array)
     {
       Node index = arg;
@@ -548,8 +570,8 @@ namespace rego
 
       if (index->type() == JSONInt)
       {
-        auto i = get_int(index);
-        if (i >= 0 && static_cast<std::size_t>(i) < container->size())
+        auto i = get_int(index).to_size();
+        if (i < container->size())
         {
           Node value = container->at(i);
           if (value->type() == Expr)
@@ -690,9 +712,14 @@ namespace rego
 
   Node Resolver::set_intersection(const Node& lhs, const Node& rhs)
   {
-    if (lhs->type() != Set || rhs->type() != Set)
+    if (lhs->type() != Set)
+    {
+      return err(lhs, "intersection: both arguments must be sets");
+    }
+
+    if (rhs->type() != Set)
     {
-      return err("intersection: both arguments must be sets");
+      return err(rhs, "intersection: both arguments must be sets");
     }
 
     Node set = NodeDef::create(Set);
@@ -715,9 +742,14 @@ namespace rego
 
   Node Resolver::set_union(const Node& lhs, const Node& rhs)
   {
-    if (lhs->type() != Set || rhs->type() != Set)
+    if (lhs->type() != Set)
+    {
+      return err(lhs, "union: both arguments must be sets");
+    }
+
+    if (rhs->type() != Set)
     {
-      return err("intersection: both arguments must be sets");
+      return err(rhs, "union: both arguments must be sets");
     }
 
     std::map<std::string, Node> members;
@@ -746,9 +778,14 @@ namespace rego
 
   Node Resolver::set_difference(const Node& lhs, const Node& rhs)
   {
-    if (lhs->type() != Set || rhs->type() != Set)
+    if (lhs->type() != Set)
     {
-      return err("intersection: both arguments must be sets");
+      return err(lhs, "difference: both arguments must be sets");
+    }
+
+    if (rhs->type() != Set)
+    {
+      return err(rhs, "difference: both arguments must be sets");
     }
 
     Node set = NodeDef::create(Set);
@@ -1011,6 +1048,24 @@ namespace rego
     return false;
   }
 
+  bool Resolver::is_undefined(const Node& node)
+  {
+    if (node->type() == Undefined)
+    {
+      return true;
+    }
+
+    for (auto& child : *node)
+    {
+      if (is_undefined(child))
+      {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   bool Resolver::is_falsy(const Node& node)
   {
     if (node->type() != Term)
@@ -1024,7 +1079,7 @@ namespace rego
       value = value->front();
       return value->type() == JSONFalse;
     }
-    else if (value->type() == Undefined)
+    else if (is_undefined(value))
     {
       return true;
     }
@@ -1101,7 +1156,7 @@ namespace rego
     return results;
   }
 
-  Node Resolver::resolve_query(const Node& query)
+  Node Resolver::resolve_query(const Node& query, const BuiltIns& builtins)
   {
     Nodes defs = resolve_varseq(query->front());
     if (defs.size() != 1)
@@ -1116,7 +1171,7 @@ namespace rego
         rulebody,
         std::make_shared<std::vector<Location>>(),
         std::make_shared<std::vector<ValuesLookup>>(),
-        BuiltIns().register_standard_builtins(),
+        builtins,
         std::make_shared<NodeMap<Unifier>>());
       unifier->unify();
     }
diff --git a/src/unifier.cc b/src/unifier.cc
index 6dd61043..0efbadcb 100644
--- a/src/unifier.cc
+++ b/src/unifier.cc
@@ -286,24 +286,54 @@ namespace rego
 
   Node UnifierDef::bind_variables()
   {
+    LOG("bind and check variables:");
     return std::accumulate(
       m_variables.begin(),
       m_variables.end(),
-      NodeDef::create(JSONTrue),
+      Undefined ^ "undefined",
       [](Node result, auto& pair) {
         Variable& var = pair.second;
+        if (!var.is_unify() && !var.is_user_var())
+        {
+          return result;
+        }
+
         Node node = var.bind();
+
         if (node->type() == Error || node->type() == Undefined)
         {
+          LOG("> ", var.name().view(), ": Undefined or Error");
           return node;
         }
-        else if (node->type() == TermSet)
+
+        if (node->type() == TermSet)
         {
-          if (node->size() == 0 && (var.is_unify() || var.is_user_var()))
+          if (var.is_unify())
+          {
+            if (node->size() == 0)
+            {
+              LOG("> ", var.name().view(), ": Empty TermSet => false");
+              return JSONFalse ^ "false";
+            }
+            if (result->type() == Undefined)
+            {
+              LOG("> ", var.name().view(), ": TermSet => true");
+              return JSONTrue ^ "true";
+            }
+          }
+          else if (node->size() > 0 && result->type() == Undefined)
           {
-            return NodeDef::create(JSONFalse);
+            LOG("> ", var.name().view(), ": TermSet => true");
+            return JSONTrue ^ "true";
           }
         }
+        else if (result->type() == Undefined)
+        {
+          LOG("> ", var.name().view(), ": Term => true");
+          return JSONTrue ^ "true";
+        }
+
+        LOG("> ", var.name().view(), ": ", result->location().view());
 
         return result;
       });
@@ -332,10 +362,11 @@ namespace rego
     }
 
     LOG_MAP_VALUES(m_variables);
+
     LOG_UNINDENT();
+    Node result = bind_variables();
     LOG_HEADER("Complete", "=====");
 
-    Node result = bind_variables();
     this->pop_rule(this->m_rule);
     return result;
   }
@@ -678,6 +709,12 @@ namespace rego
     {
       Node container = args[0]->node();
 
+      if (container->type() == Error)
+      {
+        values.push_back(ValueDef::create(var, container, sources));
+        return values;
+      }
+
       if (container->type() == Term)
       {
         container = container->front();
@@ -1052,17 +1089,47 @@ namespace rego
         }
       }
 
-      for (auto& rule : rules)
+      auto peek_type = rules[0]->type();
+      if (peek_type == RuleSet)
+      {
+        // construct a set from all valid rules
+        auto maybe_node = resolve_ruleset(rules);
+        if (maybe_node)
+        {
+          values.push_back(ValueDef::create(*maybe_node));
+        }
+      }
+      else if (peek_type == RuleObj)
       {
-        if (rule->type() == RuleComp)
+        // construct an object from all valid rules
+        auto maybe_node = resolve_ruleobj(rules);
+        if (maybe_node)
         {
-          Node body = rule / Body;
-          Node val = rule / Val;
-          if (body->type() == Empty && val->type() == Term)
+          values.push_back(ValueDef::create(*maybe_node));
+        }
+      }
+      else
+      {
+        for (auto& rule : rules)
+        {
+          if (rule->type() == RuleComp)
           {
-            values.push_back(ValueDef::create(val->clone()));
+            Node body = rule / Body;
+            Node val = rule / Val;
+            if (body->type() == Empty && val->type() == Term)
+            {
+              values.push_back(ValueDef::create(val->clone()));
+            }
+            else
+            {
+              auto maybe_node = resolve_rulecomp(rule);
+              if (maybe_node)
+              {
+                values.push_back(ValueDef::create(maybe_node.value()));
+              }
+            }
           }
-          else
+          else if (rule->type() == DefaultRule)
           {
             auto maybe_node = resolve_rulecomp(rule);
             if (maybe_node)
@@ -1070,19 +1137,11 @@ namespace rego
               values.push_back(ValueDef::create(maybe_node.value()));
             }
           }
-        }
-        else if (rule->type() == DefaultRule)
-        {
-          auto maybe_node = resolve_rulecomp(rule);
-          if (maybe_node)
+          else
           {
-            values.push_back(ValueDef::create(maybe_node.value()));
+            values.push_back(ValueDef::create(rule));
           }
         }
-        else
-        {
-          values.push_back(ValueDef::create(rule));
-        }
       }
     }
     else
@@ -1109,7 +1168,7 @@ namespace rego
   {
     if (rulecomp->type() == DefaultRule)
     {
-      std::int64_t index = std::numeric_limits<std::int64_t>::max();
+      rank_t index = std::numeric_limits<rank_t>::max();
       return std::make_pair(
         index, DefaultTerm << (wfi / rulecomp / Term)->front());
     }
@@ -1119,7 +1178,7 @@ namespace rego
     Location rulename = (wfi / rulecomp / Var)->location();
     Node rulebody = wfi / rulecomp / Body;
     Node value = wfi / rulecomp / Val;
-    std::int64_t index = Resolver::get_int(wfi / rulecomp / Idx);
+    rank_t index = ValueDef::get_rank(wfi / rulecomp / Idx);
 
     Node body_result;
     if (rulebody->type() == Empty)
@@ -1188,7 +1247,7 @@ namespace rego
   {
     assert(rulefunc->type() == RuleFunc);
 
-    std::int64_t index = Resolver::get_int(wfi / rulefunc / Idx);
+    rank_t index = ValueDef::get_rank(wfi / rulefunc / Idx);
     Node rule = Resolver::inject_args(rulefunc, args);
     if (rule->type() == Error)
     {
@@ -1205,13 +1264,20 @@ namespace rego
     Node rulebody = wfi / rule / Body;
     Node body_result;
 
-    try
+    if (rulebody->type() == Empty)
     {
-      body_result = rule_unifier(rulename, rulebody)->unify();
+      body_result = JSONTrue;
     }
-    catch (const std::exception& e)
+    else
     {
-      return std::make_pair(index, err(rulefunc, e.what()));
+      try
+      {
+        body_result = rule_unifier(rulename, rulebody)->unify();
+      }
+      catch (const std::exception& e)
+      {
+        return std::make_pair(index, err(rulefunc, e.what()));
+      }
     }
 
     LOG("Rule func body result: ", to_json(body_result));
@@ -1343,6 +1409,10 @@ namespace rego
 
     for (const auto& rule : ruleobj)
     {
+      if (rule->type() == Error)
+      {
+        return rule;
+      }
       assert(rule->type() == RuleObj);
       Location rulename = (wfi / rule / Var)->location();
       Node rulebody = wfi / rule / Body;
diff --git a/src/value.cc b/src/value.cc
index 66acf4f1..eb4abe09 100644
--- a/src/value.cc
+++ b/src/value.cc
@@ -176,7 +176,7 @@ namespace rego
     }
   }
 
-  std::int64_t ValueDef::rank() const
+  rank_t ValueDef::rank() const
   {
     return m_rank;
   }
@@ -255,7 +255,7 @@ namespace rego
   Values ValueDef::filter_by_rank(const Values& values)
   {
     Values filtered;
-    std::int64_t min_index = std::numeric_limits<std::int64_t>::max();
+    rank_t min_index = std::numeric_limits<rank_t>::max();
     for (auto& value : values)
     {
       if (value->rank() == min_index)
@@ -272,4 +272,9 @@ namespace rego
 
     return filtered;
   }
+
+  rank_t ValueDef::get_rank(const Node& node)
+  {
+    return std::stoul(to_json(node));
+  }
 }
\ No newline at end of file
diff --git a/src/variable.cc b/src/variable.cc
index e1a8b39e..36a55609 100644
--- a/src/variable.cc
+++ b/src/variable.cc
@@ -24,11 +24,25 @@ namespace rego
     m_local(local), m_initialized(false), m_dependency_score(1)
   {
     Location name = (wfi / local / Var)->location();
-    std::string name_str = std::string(name.view());
-    m_unify = name_str.starts_with("unify$");
-    m_user_var = name_str.find('$') == std::string::npos ||
-      name_str[0] == '$' || name_str.starts_with("value$") ||
-      name_str.starts_with("out$");
+    m_unify = is_unify(name.view());
+    m_user_var = is_user_var(name.view());
+  }
+
+  bool Variable::is_unify(const std::string_view& name)
+  {
+    return name.starts_with("unify$");
+  }
+
+  bool Variable::is_user_var(const std::string_view& name)
+  {
+    if (name.starts_with("__") && name.ends_with("__"))
+    {
+      // OPA test local variables use this convention
+      return true;
+    }
+
+    return name.find('$') == std::string::npos || name[0] == '$' ||
+      name.starts_with("value$") || name.starts_with("out$");
   }
 
   std::string Variable::str() const
diff --git a/tools/driver.cc b/tools/driver.cc
index 1c1bfc5f..fbef5759 100644
--- a/tools/driver.cc
+++ b/tools/driver.cc
@@ -2,5 +2,6 @@
 
 int main(int argc, char** argv)
 {
-  return rego::driver().run(argc, argv);
+  auto builtins = rego::BuiltIns().register_standard_builtins();
+  return rego::driver(builtins).run(argc, argv);
 }


Test patch:
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 8033ff10..ae7a47da 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -21,3 +21,19 @@ add_test(NAME rego_test COMMAND rego_test regocpp.yaml WORKING_DIRECTORY $<TARGE
 
 add_custom_command(TARGET rego_test POST_BUILD
                    COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/regocpp.yaml $<TARGET_FILE_DIR:rego_test>/regocpp.yaml)
+
+if(REGOCPP_OPA_TESTS)
+  set( OPA_TEST_DIRS
+  arithmetic
+  numbersrange
+  rand
+  sets
+  )
+
+  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}
+             WORKING_DIRECTORY $<TARGET_FILE_DIR:rego_test>)
+  endforeach()
+endif()
diff --git a/tests/bigint.yaml b/tests/bigint.yaml
new file mode 100644
index 00000000..59178809
--- /dev/null
+++ b/tests/bigint.yaml
@@ -0,0 +1,545 @@
+cases:
+- note: bigint/4-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -2 + -15
+    tsub := -1 - 11
+    tmul := 2 * 6
+    tdiv := 12 / 1
+    tmod := 9 % 3
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -17
+    - b: -12
+    - c: 12
+    - d: 12
+    - e: 0
+- note: bigint/12-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -3796 + 4047
+    tsub := 2527 - -1226
+    tmul := -3125 * 3702
+    tdiv := -524 / -56
+    tmod := 687 % 50
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 251
+    - b: 3753
+    - c: -11568750
+    - d: 9
+    - e: 37
+- note: bigint/20-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -253534 + -214311
+    tsub := -316587 - -767603
+    tmul := -933216 * -906316
+    tdiv := 978567 / -734
+    tmod := 424268 % -358
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -467845
+    - b: 451016
+    - c: 845788592256
+    - d: -1333
+    - e: 38
+- note: bigint/28-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 187224985 + 235111630
+    tsub := -56892477 - 158391821
+    tmul := -214662484 * 182616187
+    tdiv := -262848308 / -3809
+    tmod := -165552611 % -15653
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 422336615
+    - b: -215284298
+    - c: -39200844320028508
+    - d: 69007
+    - e: -6483
+- note: bigint/36-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -688230433 + 31054002514
+    tsub := -35274741658 - -38047510147
+    tmul := -55178253689 * -54641715256
+    tdiv := -61977392955 / 96038
+    tmod := -51123083749 % 35648
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 30365772081
+    - b: 2772768489
+    - c: 3015034426397669579384
+    - d: -645342
+    - e: -1765
+- note: bigint/44-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -6899333747660 + -8842872597161
+    tsub := 12400742779527 - 5055018679138
+    tmul := 15079794816258 * 1828965571873
+    tdiv := -8685181016788 / -3616862
+    tmod := -15141896886520 % 79911
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -15742206344821
+    - b: 7345724100389
+    - c: 27580425549844813927911234
+    - d: 2401302
+    - e: -48088
+- note: bigint/52-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 3970974674333321 + 1183868731489189
+    tsub := -1306773870446791 - 2135967516916224
+    tmul := -2830536231688581 * 3989762850901955
+    tdiv := -1486256970724744 / 55020312
+    tmod := -2696136013616059 % -397012
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 5154843405822510
+    - b: -3442741387363015
+    - c: -11293168305323109549868524075855
+    - d: -27012877
+    - e: -347207
+- note: bigint/60-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 41773599968569527 + 256972885111227618
+    tsub := -33133622577533463 - 535552228125881215
+    tmul := 140316724549002105 * 573252767549507142
+    tdiv := 619917833155820801 / 298505345
+    tmod := -581763828044170325 % 787331740
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 298746485079797145
+    - b: -568685850703414678
+    - c: 80436950681197326061797920670533910
+    - d: 2076739473
+    - e: -237085025
+- note: bigint/68-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -219269288093390366375 + 237279609106797260417
+    tsub := 129304305984146709939 - -103562668154733234310
+    tmul := 281342872348703955611 * -289108473835076078232
+    tdiv := 240199983880422800079 / -8443560164
+    tmod := -195704950809131321607 % 14299972798
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 18010321013406894042
+    - b: 232866974138879944249
+    - c: -81338608449110426615237886022619291359752
+    - d: -28447713904
+    - e: -2487164265
+- note: bigint/76-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -73601964387600912712241 + -35547942890758820527781
+    tsub := 56683798372943695697396 - 68785290015560666109313
+    tmul := -55809396266881609920720 * 56608760578660197165529
+    tdiv := -54240195272317234092971 / -72150396195
+    tmod := 73304896208042208507548 % 265986125658
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -109149907278359733240022
+    - b: -12101491642616970411917
+    - c: -3159300751311473251898508034261635590406860880
+    - d: 751765730096
+    - e: 39717895946
+- note: bigint/84-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -3770282833395493479966825 + -13036198897811218100996326
+    tsub := 14247656513712034154300221 - 16442421965352878782520018
+    tmul := 11847584104585433619674345 * -7001535483633909883335716
+    tdiv := 1725644784760724795304475 / 2767901553815
+    tmod := 13521050904731553675061962 % 3790961730483
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -16806481731206711580963151
+    - b: -2194765451640844628219797
+    - c: -82951280503591997150638888956890668970114227406020
+    - d: 623448757555
+    - e: 2872267014180
+- note: bigint/92-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -4701409739626896234620265275 + 2015683128151140118934010463
+    tsub := -71257652988823746389847654 - 585249183404298230154338937
+    tmul := 2570310086863381613906923883 * -3802219791764982116367441191
+    tdiv := -4377092757029577279610569476 / 62581799234476
+    tmod := 2359233266745016070148116611 % -10865821293764
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -2685726611475756115686254812
+    - b: -656506836393121976544186591
+    - c: -9772883883245119935551758089351576506569738702715864653
+    - d: -69941944951596
+    - e: 1643447055155
+- note: bigint/100-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 898704336891950951315814143788 + -737766299263619008655959172
+    tsub := -492053430262444193292653635058 - -444021972597425982045488337451
+    tmul := -510553742872078257157305261751 * -481029277662489003973687690131
+    tdiv := -1011911760251675689499000501369 / 251825554957272
+    tmod := -1208793221578807073859110453792 % -862858776257787
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 897966570592687332307158184616
+    - b: -48031457665018211247165297607
+    - c: 245591298141635948118160001035350445078038802879783534479381
+    - d: -4018304498220483
+    - e: -745753639977752
+- note: bigint/108-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -273708470553750115105392852914301 + -315059721892516404547579125833626
+    tsub := 107097122437355894877254557832800 - -33262208624609894166488471775815
+    tmul := 319114479817246100069710570035931 * 88924327947816860173288896287771
+    tdiv := 136479415975755448989178603729304 / -9469107218625031
+    tmod := 90441310575963144352133991714412 % 6235232317982975
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -588768192446266519652971978747927
+    - b: 140359331061965789043743029608615
+    - c: 28377040656165776738288233937527135006577602780877843771985899801
+    - d: -14413123943438993
+    - e: 5471647214413612
+- note: bigint/116-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -38186376278202810305397877333302893 + -52945032522978037314098746270191448
+    tsub := -32856127070821154989820362324796923 - 13639397101784269566229282869963230
+    tmul := 28931117236699982747637511345571017 * 23740357730153795359453549739435836
+    tdiv := -71127083141770407364026065186467116 / 258673827617455984
+    tmod := 90204492730586503058144122023391 % 138477159260687720
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -91131408801180847619496623603494341
+    - b: -46495524172605424556049645194760153
+    - c: 686835072732276146688553894217049212967639947437702718961122852765212
+    - d: -274968224643731086
+    - e: 60739392743383511
+- note: bigint/124-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -8513667349489782260158066370721316119 + 5331575836568955489004929789316596490
+    tsub := -11905292564744124321236600981014202834 - 4206371910115533085790585221716346408
+    tmul := -8945336596770306171177231927387919886 * -20023099529154642690632639354892554044
+    tdiv := -20773506069714223846475508155330257089 / 2038209135087669963
+    tmod := 4177656220305119505561105701423531093 % -4300931980307879033
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -3182091512920826771153136581404719629
+    - b: -16111664474859657407027186202730549242
+    - c: 179113364998921311337223803413116789818565797836928796481598795894997318984
+    - d: -10192038546044828812
+    - e: 419591230663461080
+- note: bigint/132-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 3335464429887250311719481164055947807879 + -277508508820093292751851929832253975796
+    tsub := 4682024083545004092977151471172100630388 - 344915055434180935970446292794958815196
+    tmul := -4369753035041193071825102293136728535996 * 5141003474970440469443567420228556622184
+    tdiv := 1363345672746235266680263090023141184675 / -71697331398541185359
+    tmod := 285132741646440698217476362459634075167 % -4932669198205783533
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 3057955921067157018967629234223693832083
+    - b: 4337109028110823157006705178377141815192
+    - c: -22464915537909402502035411329151918313933584964898112286215726130575409216135264
+    - d: -19015291729169923507
+    - e: 681260851030700932
+- note: bigint/140-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 367944448272419608149400495092145560107699 + 1341772272115351099882382809224753162911992
+    tsub := -351473167968233922999121532572688731886193 - -1155354873305943911676906572591930849864351
+    tmul := 1079131679099249441816113576011814972818111 * 322308787064379785376676359356271670701955
+    tdiv := 1050342523092273506434911772263185262105238 / 937832119831322468043
+    tmod := -809454115361161329753823934538722257643004 % 434157085476005649016
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 1709716720387770708031783304316898723019691
+    - b: 803881705337709988677785040019242117978158
+    - c: 347813622573226606095760708613217508712836714978276293794376286084135591459907107005
+    - d: 1119968596598277147063
+    - e: -316188205373748227556
+- note: bigint/148-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 45666771552793209140688450281250923580766932 + 209985798274726718593008748819528254344943275
+    tsub := -20102538784115270330457727065473057062782517 - -238823404166208290081762290846120783254329792
+    tmul := 195974298702520522180370017433741391195328887 * 240247447395664266307473639542040309397363899
+    tdiv := -131654502911203855952038662132816189233446064 / 10180855922602978049040
+    tmod := -307689542671715403171257882440651459362152686 % -7691941305970685370950
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 255652569827519927733697199100779177925710207
+    - b: 218720865382093019751304563780647726191547275
+    - c: 47082325018435995030195235840997616378858752592779376444430656787485195629166240125650413
+    - d: -12931575096639148767756
+    - e: -7029127252375412155936
+- note: bigint/156-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -34886984404821451075481052816932726478048035908 + -11156471949567693407942303052710475761260430886
+    tsub := -3552750856597148061640116642396446917631882906 - -36765508746482568582703880687567730225523522974
+    tmul := -78952740904414312263600345867431202702068067075 * 49654691132527940552692336265928015572877676877
+    tdiv := 87942798193539690732028835457463101987201421151 / -203363565849823594274394
+    tmod := -49701011270147447237766652099591630974099347478 % -216730604477250178151122
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -46043456354389144483423355869643202239308466794
+    - b: 33212757889885420521063764045171283307891640068
+    - c: -3920373963675197364478926257411358824580913835524371088647537794817729491724445554451812524775
+    - d: -432441267569443417327924
+    - e: -201928471729234095265044
+- note: bigint/164-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 10951700754267580259186131164960823195887794740197 + -2389811640792656450116825577538092238598274098102
+    tsub := -17315333459956547021048523887639557875378325712756 - 18424391549106510668202972931164525245183622265370
+    tmul := 4758518536847612118873675643715676486376611270465 * 3177678027094974351149715017467170701767985062066
+    tdiv := 19142006419737844184608038227726569928662426379219 / -2694979620631635155534887
+    tmod := 22464516888404727649381049613001069087020276044407 % 55875217053020082773751
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 8561889113474923809069305587422730957289520642095
+    - b: -35739725009063057689251496818804083120561947978126
+    - c: 15121039796064784088034784314512143820467568891161577299889445856057373819379753255963818137680690
+    - d: -7102839024530894728105291
+    - e: 54240391374912448073470
+- note: bigint/172-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -1239219560216866782865439309210970113128789475298500 + -5855795074705731844699601698317136728515252433000353
+    tsub := -2406790042426351932761723889784667846652870707646580 - 1790739222042762666646065351682076738845707428975029
+    tmul := -927693506427961750705020020171152006168766682800971 * 2459322492067554514566189405154678206997802127888199
+    tdiv := -2353192149872123053678528709884738252559874408143868 / 13790802380975948429831720
+    tmod := -2453290415397912198141531219789738735838217485321924 % 53527125498925581350136462
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -7095014634922598627565041007528106841644041908298853
+    - b: -4197529264469114599407789241466744585498578136621609
+    - c: -2281497506103302795717499659566979910356917783820862711271073622338291720331989807130661580362456641229
+    - d: -170634897438476106525786599
+    - e: -4172292425649886600976086
+- note: bigint/180-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 736635144440945644544560837692528859403092890153246343 + -1089351037296043557369824200964108698667336851376432050
+    tsub := -332967893780520670408961348388297954576235971623386302 - 563098020547264561847764552086803214626520351939480579
+    tmul := -1206152417619110481327519838156318477966932742389800150 * 1390860682588510838560546166530222528625148851661613048
+    tdiv := 691312138190050671356179590920583842607841302567725032 / -52016036555250568592828156
+    tmod := 75467193283075781192752100180123451343707885169463821 % 420422038843118216113818646
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -352715892855097912825263363271579839264243961223185707
+    - b: -896065914327785232256725900475101169202756323562866881
+    - c: -1677589974875498591017395295594821206705186544883316317460379945119209171958782885513464972795642131352357200
+    - d: -13290365509793319606994397627
+    - e: 8961640720344367421823175
+- note: bigint/188-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 193504497578813808549037438875417067023131467998759040586 + -218923041330037561842609440206655593925205805671854384329
+    tsub := -16635971990941783246194211033679239774725419687494484150 - -313949401298087446062218486283338347918478724178667911247
+    tmul := 239716909339069620092477141567943385434125097492144793517 * 246029761131653585901734152222425983381178995358997408863
+    tdiv := 245419347427300933480173604954615865470829010190904397229 / -124927864139903244099390293
+    tmod := 82747423113346084694894435626069062972274627145477543617 % -9710243757121659067981232507
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -25418543751223753293572001331238526902074337673095343743
+    - b: 297313429307145662816024275249659108143753304491173427097
+    - c: 58977493943909557309482369103933017233752474807623012851682069672829957181554180861414531375369815784019160741171
+    - d: -1964488459936068585462267197762
+    - e: 8784546289155155420584106603
+- note: bigint/196-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -31381719152569640251454975425256833884668155153480743965803 + -63437501337060097663175701287724562552791399435068313805140
+    tsub := -82733094267270494562659856778919495350938349312570954387491 - 62001691641426543405965536391049521964812307903502507616211
+    tmul := -9298465480826942458880028438762453543662282120004994741743 * 73134627244378531503224000323562678790507862318859450431824
+    tdiv := 83709434885522094274931076134016452909622664058285446801630 / 311455688569094290599844299084
+    tmod := 44230064074606494276230074884143286847808723653596032177408 % -282115910069789933475118744402
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -94819220489629737914630676712981396437459554588549057770943
+    - b: -144734785908697037968625393169969017315750657216073462003702
+    - c: -680039806884999427718561577073207328168437729480403065828807865544282284798571210863123291938549018260101870708429232
+    - d: 268768360822383037156264798997
+    - e: 185188446178435701325780811310
+- note: bigint/204-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -25023922354660850191134336091024545224852198679800759036109007 + 18521656944782015207983551731803854395853006423877385685030871
+    tsub := 23922583600359551402243799022878224180682371332583032666727985 - 9880351522490909660919310237321557279694012934655953565186174
+    tmul := -25239650180280523076309959574097669358510256084680219982171321 * -17904193967932101174980338517342468653959350709680962616803333
+    tdiv := -22589500532947830281109871569582382602425791292991965455309779 / 3202258988702830210072036687303
+    tmod := -7272589057938122041831129185584642706043815939795890619441739 % -2249314409642173343215455949245
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -6502265409878834983150784359220690828999192255923373351078136
+    - b: 14042232077868641741324488785556666900988358397927079101541811
+    - c: 451895592510494911219605247273314765764568602512519377040613019090914197415019435021131175261306516834605860267273369812893
+    - d: -7054239089542965454831605375288
+    - e: -316412525306452432957910720359
+- note: bigint/212-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -6255307714551761308003193803762156830838612307455334186485368543 + -65892536229199026191214071560645340121388655642943615898680964
+    tsub := -5417941120710314897458475191380478869709122112415787474462600521 - 2246341561769342241986291200293286344083404500825803607303020282
+    tmul := -6471206257195861450551690504618948550199108013139856544527015750 * 1064515592363470645534479213953634771884395924986810291013745158
+    tdiv := -1208108802693799238539208934476757562964196723538368610122516464 / 36295554420397663814976454754741
+    tmod := 4862017583612423308287445617516555086412830093193554662930003054 % -64728821206768602037663742933142
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -6321200250780960334194407875322802170960000963098277802384049507
+    - b: -7664282682479657139444766391673765213792526613241591081765620803
+    - c: -6888699962185050227673752430565764221732726730128268986695997619711015034894953823457201351992391818686435273920941116752238500
+    - d: -33285310611341885515102883743617
+    - e: 16530088360251194106662164690072
+- note: bigint/220-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 964154629418795086212682855209261479634055018818601541305689841094 + -1522235493112025895970867433497306324768764802420405677432021196710
+    tsub := -885452044031715918633993855261791590532341908774002321374077723616 - 426904963277141193485469986377005038350533492510553767099588455917
+    tmul := 653716386439714399321463842033272050641079807222348685089209177053 * 8873349840646133459906461434154062268882459421371186845875329614
+    tdiv := -743587230014859016489839981630229795702802400763746231797982752198 / -591628665329605604681046428263819
+    tmod := -118817514693566986665361920457341717361471129784301749254205079117 % 598431104675766207248069009607577
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -558080863693230809758184578288044845134709783601804136126331355616
+    - b: -1312357007308857112119463841638796628882875401284556088473666179533
+    - c: 5800654193442605965432649344259559209088812838991207795726204877582622085945872513358392701329850262041371850980894492300060147542
+    - d: 1256847873658378119830591400700757
+    - e: -229867447767974616613646180427498
+- note: bigint/228-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -418629540260067180078377207117390254795622882372445090659100603254967 + -381278185777539733472236099826121380011258929969995192052562658483536
+    tsub := 182764039772223805653572497373514469329571661629562007827938805692743 - -126109033051807091559963781193432355382537215672893184521438769871678
+    tmul := -225237120080758693589000422975048933466910685791041308729390969891805 * 106561585073656347339027888214116483710522498511406668160731373141110
+    tdiv := -194942726351043343558628362321625560338963757112985694682824617599535 / 4091912534588290725145026574574721
+    tmod := -429796560109634576884497767143865222096526648187651938889075898176176 % 870727633289405965995219916655207
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: -799907726037606913550613306943511634806881812342440282711663261738503
+    - b: 308873072824030897213536278566946824712108877302455192349377575564421
+    - c: -24001624533231117941683919164558709818485246509332355642872799887269880184167361464994099140950994612721655434312050675055838859697603550
+    - d: -47640980764672570696243252854787982
+    - e: -13381591403188879081815318778689
+- note: bigint/236-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := -30339405259128826320371676477143698993343796573815996524417845829294351 + 104688599292429000714300283444823567875127199363520643982601892296975006
+    tsub := -44490570981537590899012122340378398923421348521165077299165008960603759 - 68529429636135486290804721826997958888091459673624581943216947903335757
+    tmul := 79754341600736016111746705453330145114105289328804738919288096361713471 * 65152214870832516721444134645799134637055145236930890078093265262979414
+    tdiv := 50780901411816507599004854293945100734566501448333723073581001132421203 / -147863808509682044529515298376380288
+    tmod := 3582179939697576964326055910334136801174876720124800582552296958781519 % -225198022027540975370394236776633299
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 74349194033300174393928606967679868881783402789704647458184046467680655
+    - b: -113020000617673077189816844167376357811512808194789659242381956863939516
+    - c: 5196172000852929494850683590287706987412208859683214470294095175794819136418656199328647023359300788882421285414132090957392823251755639485994
+    - d: -343430227610371613297784508459153108
+    - e: 79669604418830368256246356946031314
+- note: bigint/244-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 870502615624380020903725666476630459871285617011270931275765784537477418 + 23131426583620008820475912778769909777760814610230377450102030142980114605
+    tsub := 12164013413032420273521699439890978773160342711245624497425222595101065992 - -2354946185641931689672340156140578415690990626406436303188769996767356266
+    tmul := 26202679530882340431416644356116802601892235719976369919193297248310361452 * 11290259986740145243746516913682746819861182569700265712977102809312504381
+    tdiv := 2237197096512565586720153528753877252918919222618370701184925266033780653 / 2729890298080180624582116052634386464
+    tmod := -25979928370007114165133094262544676755458091924671119796932633635980797397 % 1655356948213795271247709819051399082
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 24001929199244388841379638445246540237632100227241648381377795927517592023
+    - b: 14518959598674351963194039596031557188851333337652060800613992591868422258
+    - c: 295835064252895928075050096384890236896235108877299467807629151514872304578990229895632118487505718716695616949811804818682679878486751469443521212
+    - d: 819519047371938034429005023757617974
+    - e: -1278137145215247169690553234822751
+- note: bigint/252-bit
+  modules:
+  - |
+    package bigint
+    
+    tadd := 6466055601279126416241593829827225642974873054416615134420424370024610277048 + 812377893613439962506223192889271172721150454313527119829598533028254192660
+    tsub := -5912796750055159349348285095168029999547805837934919256759875720338049163859 - 2100436268304949465469152169050553499388589256398614019764087298232731952205
+    tmul := 5786314899744711870314487313338784069375375316866554430539107712879654609997 * 3029613346087335900692057905494693632329204701910191321772183583923555603055
+    tdiv := 235328967186965560636092487672274298032698178001584420387677709515603751810 / 61042697860145807716747171190736990291
+    tmod := -1030904679897134708160285970453473199806033360699641489201333428182197161893 % 45845269018283096547057010054707163542
+  query: a = data.bigint.tadd; b = data.bigint.tsub; c = data.bigint.tmul; d = data.bigint.tdiv; e = data.bigint.tmod
+  want_result:
+    - a: 7278433494892566378747817022716496815696023508730142254250022903052864469708
+    - b: -8013233018360108814817437264218583498936395094333533276523963018570781116064
+    - c: 17530296844930584098686471133123526535894636896209113668872569260041333924699635889877017262804525726346271216292420744804783913117122283226890166740835
+    - d: 3855153448920703543655522463019737887
+    - e: -41806999527922083709215100619759029989
diff --git a/tests/helpers/testgen.py b/tests/helpers/testgen.py
new file mode 100644
index 00000000..1fec7664
--- /dev/null
+++ b/tests/helpers/testgen.py
@@ -0,0 +1,71 @@
+"""Script to generate test rego files."""
+
+from random import getrandbits, random
+from typing import List
+
+
+def operand(num_bits: int) -> int:
+    """Generates a random operand."""
+    result = getrandbits(num_bits)
+    if random() < 0.5:
+        result = -result
+
+    return result
+
+
+def bigint_testcase(num_bits: int) -> List[str]:
+    """Generates a test case for bigint."""
+    operators = {
+        '+': ("a", "tadd", lambda x, y: x + y),
+        '-': ("b", "tsub", lambda x, y: x - y),
+        '*': ("c", "tmul", lambda x, y: x * y),
+        '/': ("d", "tdiv", lambda x, y: x // y),
+        '%': ("e", "tmod", lambda x, y: x % y),
+    }
+
+    lines = []
+    lines.append("- note: bigint/{}-bit".format(num_bits))
+    lines.append("  modules:")
+    lines.append("  - |")
+    lines.append("    package bigint")
+    lines.append("    ")
+    query = []
+    want_result = []
+    for operator in operators:
+        lhs = operand(num_bits)
+        if operator == '/' or operator == '%':
+            rhs = operand(num_bits // 2)
+            if rhs == 0:
+                rhs = 1
+        else:
+            rhs = operand(num_bits)
+        var, name, op = operators[operator]
+        result = op(lhs, rhs)
+        lines.append("    {} := {} {} {}".format(name, lhs, operator, rhs))
+        query.append("{} = data.bigint.{}".format(var, name))
+        want_result.append("    - {}: {}".format(var, result))
+
+    lines.append("  query: {}".format("; ".join(query)))
+    lines.append("  want_result:")
+    for line in want_result:
+        lines.append(line)
+    return lines
+
+
+def bigint(path: str, bits: range):
+    """Generates a test rego file for bigint."""
+
+    with open(path, "w") as file:
+        file.write("cases:\n")
+        for num_bits in bits:
+            lines = bigint_testcase(num_bits)
+            file.write("\n".join(lines))
+            file.write("\n")
+
+
+def main():
+    bigint("bigint.yaml", range(4, 256, 8))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/tests/main.cc b/tests/main.cc
index a99d78c9..c95550db 100644
--- a/tests/main.cc
+++ b/tests/main.cc
@@ -1,8 +1,30 @@
 #include "test_case.h"
 
+#include <type_traits>
+
 const std::string Green = "\x1b[32m";
 const std::string Reset = "\x1b[0m";
 const std::string Red = "\x1b[31m";
+const std::string White = "\x1b[37m";
+
+using TestCases = std::map<std::string, std::vector<rego_test::TestCase>>;
+
+void load_testcases(
+  const std::filesystem::path& path,
+  const std::filesystem::path& debug_path,
+  TestCases& testcases)
+{
+  std::vector<rego_test::TestCase> test_cases =
+    rego_test::TestCase::load(path, debug_path);
+  for (auto test_case : test_cases)
+  {
+    if (!testcases.contains(test_case.category()))
+    {
+      testcases[test_case.category()] = std::vector<rego_test::TestCase>();
+    }
+    testcases[test_case.category()].push_back(test_case);
+  }
+}
 
 int main(int argc, char** argv)
 {
@@ -11,12 +33,23 @@ int main(int argc, char** argv)
   app.set_help_all_flag("--help-all", "Expand all help");
 
   std::vector<std::filesystem::path> case_paths;
-  app.add_option("case,-c,--case", case_paths, "Test case YAML files");
+  app.add_option(
+    "case,-c,--case", case_paths, "Test case YAML files or directories");
 
   std::filesystem::path debug_path;
   app.add_option(
     "-a,--ast", debug_path, "Output the AST (debugging for test case parser)");
 
+  bool enable_logging{false};
+  app.add_flag("-l,--logging", enable_logging, "Enable logging");
+
+  bool fail_first{false};
+  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");
+
   try
   {
     app.parse(argc, argv);
@@ -26,59 +59,85 @@ int main(int argc, char** argv)
     return app.exit(e);
   }
 
-  int failures = 0;
-  for (auto path : case_paths)
+  rego::Logger::enabled = enable_logging;
+
+  TestCases all_testcases;
+  for (auto file_or_dir : case_paths)
   {
-    std::vector<rego_test::TestCase> test_cases;
-    auto maybe_test_cases = rego_test::TestCase::load(path, debug_path);
-    if (maybe_test_cases.has_value())
+    if (std::filesystem::is_directory(file_or_dir))
     {
-      test_cases = *maybe_test_cases;
+      for (auto& p : std::filesystem::directory_iterator(file_or_dir))
+      {
+        load_testcases(p, debug_path, all_testcases);
+      }
+    }
+    else if (std::filesystem::exists(file_or_dir))
+    {
+      load_testcases(file_or_dir, debug_path, all_testcases);
     }
     else
     {
-      std::cout << "Unable to parse test cases in " << path << std::endl;
-      continue;
+      std::cerr << "Not a file: " << file_or_dir << std::endl;
+      return 1;
     }
+  }
 
-    std::cout << path << std::endl;
-
-    for (auto& test_case : test_cases)
+  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)
+      {
+        continue;
+      }
+
       try
       {
         auto start = std::chrono::steady_clock::now();
-        auto result = test_case.run(argv[0]);
+        auto result = testcase.run(argv[0], debug_path);
         auto end = std::chrono::steady_clock::now();
         const std::chrono::duration<double> elapsed = end - start;
 
         if (result.passed)
         {
-          std::cout << Green << "  PASS: " << Reset << test_case.note()
-                    << std::fixed << std::setw(62 - test_case.note().length())
+          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;
         }
         else
         {
           failures++;
-          std::cout << std::setw(70) << Red << "  FAIL: " << Reset
-                    << test_case.note() << std::fixed
-                    << std::setw(62 - test_case.note().length())
+          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 << "  " << result.error << std::endl;
+          std::cout << "(from " << testcase.filename() << ")" << std::endl;
+          if (fail_first)
+          {
+            break;
+          }
         }
       }
       catch (const std::exception& e)
       {
         failures++;
-        std::cout << std::setw(70) << Red << "  FAIL: " << Reset
-                  << test_case.note() << std::endl;
+        std::cout << Red << "  FAIL: " << Reset << testcase.note() << std::endl;
         std::cout << "  " << e.what() << std::endl;
-        continue;
+        std::cout << "(from " << testcase.filename() << ")" << std::endl;
+        if (fail_first)
+        {
+          break;
+        }
       }
     }
+    if (failures > 0 && fail_first)
+    {
+      break;
+    }
 
     std::cout << std::endl;
   }
diff --git a/tests/parse.cc b/tests/parse.cc
index caf30743..5f81aa75 100644
--- a/tests/parse.cc
+++ b/tests/parse.cc
@@ -26,6 +26,13 @@ namespace rego_test
     std::size_t indent;
   };
 
+  enum class Quote
+  {
+    None,
+    Single,
+    Double
+  };
+
   Parse parser()
   {
     Parse p(depth::file);
@@ -33,6 +40,7 @@ namespace rego_test
     indents->push_back(0);
     auto indent = std::make_shared<std::size_t>(0);
     auto string_indent = std::make_shared<std::size_t>(0);
+    auto quote = std::make_shared<Quote>(Quote::None);
 
     p("start",
       {
@@ -78,10 +86,20 @@ namespace rego_test
         ",[[:blank:]]*" >> [](auto& m) { m.term(); },
 
         // Double quote string
-        R"re2("([^"]*)")re2" >> [](auto& m) { m.add(String, 1); },
+        "\"" >>
+          [quote](auto& m) {
+            m.push(DoubleQuoteString, 1);
+            m.mode("multiline-string");
+            *quote = Quote::Double;
+          },
 
         // Single quote string
-        R"('([^'^\\]*)')" >> [](auto& m) { m.add(String, 1); },
+        "'" >>
+          [quote](auto& m) {
+            m.push(SingleQuoteString, 1);
+            m.mode("multiline-string");
+            *quote = Quote::Single;
+          },
 
         // KeyValue.
         ":[ ]*" >> [](auto& m) { m.add(Colon); },
@@ -114,6 +132,9 @@ namespace rego_test
 
         // Int.
         R"([[:digit:]]+\b)" >> [](auto& m) { m.add(Integer); },
+
+        // Space.
+        " " >> [](auto&) {},
       });
 
     p("indent",
@@ -199,12 +220,22 @@ namespace rego_test
                {
                  m.pop(LiteralString);
                }
-               else
+               else if (m.in(FoldedString))
                {
                  m.pop(FoldedString);
                }
              }
            }
+           else if (m.in(SingleQuoteString) || m.in(DoubleQuoteString))
+           {
+             if (*string_indent == 0)
+             {
+               *string_indent = *indent;
+             }
+             m.mode("multiline-string");
+             m.add(String);
+             return;
+           }
            else
            {
              if (*indent > indents->back())
@@ -242,9 +273,53 @@ namespace rego_test
            m.mode("indent");
          },
 
+       R"(\\")" >> [](auto& m) { m.extend(String); },
+
+       "\"" >>
+         [string_indent, quote](auto& m) {
+           if (*quote == Quote::Double)
+           {
+             m.term();
+             *quote = Quote::None;
+             *string_indent = 0;
+             m.pop(DoubleQuoteString);
+             m.term();
+             m.mode("start");
+           }
+           else
+           {
+             m.extend(String);
+           }
+         },
+
+       "'" >>
+         [string_indent, quote](auto& m) {
+           if (*quote == Quote::Single)
+           {
+             m.term();
+             *quote = Quote::None;
+             *string_indent = 0;
+             m.pop(SingleQuoteString);
+             m.term();
+             m.mode("start");
+           }
+           else
+           {
+             m.extend(String);
+           }
+         },
+
        // Character
        "." >> [](auto& m) { m.extend(String); }});
 
+    p.done([indents](auto& m) {
+      while (!indents->empty())
+      {
+        m.term({Block});
+        indents->pop_back();
+      }
+    });
+
     p.gen({
       Integer >> [](auto& rnd) { return std::to_string(rnd() % 100); },
       Float >>
diff --git a/tests/rego_test.cc b/tests/rego_test.cc
index 7cb146e9..b860d4f9 100644
--- a/tests/rego_test.cc
+++ b/tests/rego_test.cc
@@ -12,6 +12,14 @@ namespace rego_test
     return {
       In(Top) * T(File)[File] >> [](Match& _) { return Block << *_[File]; },
 
+      In(Block) * (T(Group) << (T(String) * T(Colon) * (T(Brace) << End))) >>
+        [](Match&) { return Node(); },
+
+      In(Block) *
+          ((T(Group) << (T(String) * T(Colon) * End)) *
+           (T(Group)[Group] << (T(String) * T(Colon)))) >>
+        [](Match& _) { return _(Group); },
+
       In(Group) * T(Brace)[Brace] >>
         [](Match& _) { return Block << *_[Brace]; },
 
@@ -44,14 +52,23 @@ namespace rego_test
           return Float ^ loc;
         },
 
-      (In(LiteralString) / In(FoldedString)) *
+      (In(LiteralString) / In(FoldedString) / In(SingleQuoteString) /
+       In(DoubleQuoteString)) *
           (T(Group) << T(String)[String]) >>
         [](Match& _) { return _(String); },
 
-      (In(LiteralString) / In(FoldedString)) * (T(Group) << T(Blank)) >>
+      (In(LiteralString) / In(FoldedString) / In(SingleQuoteString) /
+       In(DoubleQuoteString)) *
+          (T(Group) << T(Blank)) >>
         [](Match&) { return String ^ ""; },
 
       // errors
+      In(Group) * (T(Block)[Block] << End) >>
+        [](Match& _) { return err(_(Block), "Syntax error: empty block"); },
+
+      (In(Entry) / In(Block)) * (T(Group)[Group] << End) >>
+        [](Match& _) { return err(_(Group), "Syntax error: empty group"); },
+
       In(Group) * T(Hyphen)[Hyphen] >>
         [](Match& _) {
           return err(_(Hyphen), "Invalid sequence entry declaration");
@@ -68,6 +85,16 @@ namespace rego_test
       In(FoldedString) * T(Group)[Group] >>
         [](Match& _) { return err(_(Group), "Invalid folded string element"); },
 
+      In(SingleQuoteString) * T(Group)[Group] >>
+        [](Match& _) {
+          return err(_(Group), "Invalid single-quoted string element");
+        },
+
+      In(DoubleQuoteString) * T(Group)[Group] >>
+        [](Match& _) {
+          return err(_(Group), "Invalid double-quoted string element");
+        },
+
     };
   }
 
@@ -89,7 +116,9 @@ namespace rego_test
           return String ^ buf.str();
         },
 
-      In(Group) * (T(FoldedString) << (T(String)++[String])) >>
+      In(Group) *
+          ((T(FoldedString) / T(SingleQuoteString) / T(DoubleQuoteString))
+           << (T(String)++[String])) >>
         [](Match& _) {
           std::ostringstream buf;
           std::string sep = "";
@@ -155,6 +184,21 @@ namespace rego_test
           return KeyValue << (Key ^ key_loc) << _(Val);
         },
 
+      (In(Entry) / In(Block)) *
+          ((T(Group) << ((T(Scalar) << T(String))[Key] * End)) *
+           (T(Group) << (T(Colon) * Any[Val]))) >>
+        [](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 << _[Val]);
+        },
+
       In(Entry) * T(KeyValue)[KeyValue] >>
         [](Match& _) { return Group << (Block << _(KeyValue)); },
 
@@ -235,9 +279,9 @@ namespace rego_test
            << ((T(Key)[Key]([](auto& n) {
                  return name_equals(*n.first, {"want_result"});
                })) *
-               T(Mapping)[Mapping])) >>
+               T(Sequence)[Sequence])) >>
         [](Match& _) {
-          return KeyValue << _(Key) << (WantResult << *_[Mapping]);
+          return KeyValue << _(Key) << (WantResult << *_[Sequence]);
         },
 
       In(rego::List) *
@@ -287,6 +331,9 @@ namespace rego_test
                                << (rego::Term << _(Val));
         },
 
+      In(WantResult) * (T(Entry) << T(Mapping)[Mapping]) >>
+        [](Match& _) { return Seq << *_[Mapping]; },
+
       In(rego::Term) * T(Mapping)[Mapping] >>
         [](Match& _) {
           bool is_set = true;
diff --git a/tests/rego_test.h b/tests/rego_test.h
index e5b59d6c..6c3673d3 100644
--- a/tests/rego_test.h
+++ b/tests/rego_test.h
@@ -28,6 +28,8 @@ namespace rego_test
   inline const auto Null = TokenDef("yaml-null", flag::print);
   inline const auto LiteralString = TokenDef("yaml-literal-string");
   inline const auto FoldedString = TokenDef("yaml-folded-string");
+  inline const auto SingleQuoteString = TokenDef("yaml-single-quote-string");
+  inline const auto DoubleQuoteString = TokenDef("yaml-double-quote-string");
   inline const auto Scalar = TokenDef("yaml-scalar");
   inline const auto Data = TokenDef("yaml-data");
   inline const auto Input = TokenDef("yaml-input");
@@ -41,8 +43,8 @@ namespace rego_test
   inline const auto Square = TokenDef("yaml-square");
 
   inline const auto wf_parse_tokens = Block | String | Integer | Float | True |
-    False | Null | Colon | LiteralString | FoldedString | Hyphen | Blank |
-    Brace | Square;
+    False | Null | Colon | LiteralString | FoldedString | SingleQuoteString |
+    DoubleQuoteString | Hyphen | Blank | Brace | Square;
 
   // clang-format off
   inline const auto wf_parser =
@@ -51,14 +53,17 @@ namespace rego_test
     | (Block <<= Group++[1])
     | (LiteralString <<= Group++[1])
     | (FoldedString <<= Group++[1])
-    | (Brace <<= Group++[1])
-    | (Square <<= Group++[1])
+    | (SingleQuoteString <<= Group++[1])
+    | (DoubleQuoteString <<= Group++[1])
+    | (Brace <<= Group++)
+    | (Square <<= Group++)
     | (Group <<= wf_parse_tokens++[1])
     ;
   // clang-format on
 
   inline const auto wf_entry_tokens = Block | String | Integer | Float | True |
-    False | Null | Colon | Entry | LiteralString | FoldedString;
+    False | Null | Colon | Entry | LiteralString | FoldedString |
+    SingleQuoteString | DoubleQuoteString;
 
   // clang-format off
   inline const auto wf_pass_entry =
@@ -66,6 +71,8 @@ namespace rego_test
     | (Top <<= Block)
     | (LiteralString <<= String++[1])
     | (FoldedString <<= String++[1])
+    | (SingleQuoteString <<= String++[1])
+    | (DoubleQuoteString <<= String++[1])
     | (Entry <<= Block | Group)
     | (Group <<= wf_entry_tokens++[1])
     ;
diff --git a/tests/regocpp.yaml b/tests/regocpp.yaml
index 5b72bfba..faa16b6a 100644
--- a/tests/regocpp.yaml
+++ b/tests/regocpp.yaml
@@ -11,7 +11,7 @@ cases:
   note       : regocpp/scalars
   query      : data.scalars.greeting = x
   want_result:
-    x: Hello
+    - x: Hello
 - data:
     one:
       bar: Foo
@@ -55,15 +55,17 @@ cases:
     }
     f := e["dev"]
   note : regocpp/input_data
-  query: "[data.one, input.b, data.objects.sites[1]] = x"
+  query: "[data.one,
+    input.b, 
+    data.objects.sites[1]] = x"
   want_result:
-    x:
-    - bar: Foo
-      baz: 5
-      be: true
-      bop: 23.4
-    - "20"
-    - name: smoke1
+    - x:
+      - bar: Foo
+        baz: 5
+        be: true
+        bop: 23.4
+      - "20"
+      - name: smoke1
 - modules:
   - |
     package objects
@@ -88,12 +90,13 @@ cases:
   note : regocpp/objects
   query: data.objects.sites[1].name = x
   want_result:
-    x: smoke1
+    - x: smoke1
 - note: regocpp/query_math
-  query: x=5;y=x + (2 - 4 * 0.25) * -3 + 7.4
+  query: 'x=5;
+    y=x + (2 - 4 * 0.25) * -3 + 7.4'
   want_result:
-    x: 5
-    y: 9.4
+    - x: 5
+    - y: 9.4
 - note: regocpp/expressions
   modules:
   - |
@@ -112,7 +115,7 @@ cases:
     k := a - (b + -c) * (-d / 2) + 3.7
   query: data.expressions.k > 27 = x
   want_result:
-    x: true
+    - x: true
 - note: regocpp/bodies
   modules:
   - |
@@ -159,9 +162,9 @@ cases:
     d: true
   query: data.bodies.e = x
   want_result:
-    x:
-      one: 15
-      two: 15
+    - x:
+        one: 15
+        two: 15
 - note: regocpp/bodies_undef
   modules:
   - |
@@ -247,10 +250,10 @@ cases:
     }
   query: data.sets.string_set = x
   want_result:
-    x:
-      four: null
-      one: null
-      two: null
+    - x:
+        four: null
+        one: null
+        two: null
 - note: regocpp/sets_membership
   modules:
   - |
@@ -290,7 +293,7 @@ cases:
     }
   query: data.sets.m = x
   want_result:
-    x: true
+    - x: true
 - note: regocpp/var_keys
   modules:
   - |
@@ -315,7 +318,7 @@ cases:
     f := e["dev"]
   query: data.objects.f = x
   want_result:
-    x: false
+    - x: false
 - note: regocpp/multi/0
   input:
     a: 10
@@ -342,7 +345,7 @@ cases:
     }
   query: data.multi.a = x
   want_result:
-    x: 1
+    - x: 1
 - note: regocpp/multi/1
   input:
     a: 4
@@ -369,7 +372,7 @@ cases:
     }
   query: data.multi.a = x
   want_result:
-    x: 41
+    - x: 41
 - note: regocpp/multi/2
   input:
     a: 7
@@ -396,7 +399,7 @@ cases:
     }
   query: data.multi.a = x
   want_result:
-    x: 70
+    - x: 70
 - note: regocpp/functions/sum
   input:
     key: sum
@@ -436,9 +439,9 @@ cases:
     }
   query: "[data.functions.f(1, 2, 3), data.functions.f(1, -1, 3)] = x"
   want_result:
-    x:
-    - 6
-    - 4
+    - x:
+      - 6
+      - 4
 - note: regocpp/functions/prod
   input:
     key: prod
@@ -478,9 +481,9 @@ cases:
     }
   query: "[data.functions.f(1, 2, 3), data.functions.f(1, -1, 3)] = x"
   want_result:
-    x:
-    - 6
-    - -3
+    - x:
+      - 6
+      - -3
 - note: regocpp/functions/ref
   modules:
   - |
@@ -518,9 +521,9 @@ cases:
     }
   query: "[data.functions.h(1, 2).a, data.functions.h(3, 4).b] = x"
   want_result:
-    x:
-    - 1
-    - 4
+    - x:
+      - 1
+      - 4
 - note: regocpp/unify
   modules:
   - |
@@ -532,61 +535,61 @@ cases:
 
     a := {
       "foo": 1,
-        "bar": 2,
+      "bar": 2,
     }
 
     b := x + y {
-        some x, y
-        [1, x] = tuples[i]
-        {"foo": y, "bar": 2} = a
-        i != 0
+      some x, y
+      [1, x] = tuples[i]
+      {"foo": y, "bar": 2} = a
+      i != 0
     }
 
     c {
-        i != 0
-        x = tuples[i]
-        i = x[1] - 1
+      i != 0
+      x = tuples[i]
+      i = x[1] - 1
     }
 
     d {
       tuple := tuples[i]
-        tuple[0] >= 3
-        tuple[1] >= 5
+      tuple[0] >= 3
+      tuple[1] >= 5
     }
 
     e := key {
-        target = 2
-        some key
+      target = 2
+      some key
       a[key] = target
     }
 
     f := x + y {
-        letters[["a", x]]
-        letters[["c", y]]
+      letters[["a", x]]
+      letters[["c", y]]
     }
 
     g := j {
       x = mixed[i]
-        x[j] = 6
+      x[j] = 6
     }
 
     h := j {
       x = mixed[i]
-        x[j] = 7
+      x[j] = 7
     }
 
     output := [a, b, c, d, e, f, g, h]
   query: data.unify.output = x
   want_result:
-    x:
-    - {"bar": 2, "foo": 1}
-    - 3
-    - true
-    - true
-    - bar
-    - 4
-    - 3
-    - foo
+    - x:
+      - {"bar": 2, "foo": 1}
+      - 3
+      - true
+      - true
+      - bar
+      - 4
+      - 3
+      - foo
 - note: regocpp/compare
   modules:
   - |
@@ -604,7 +607,7 @@ cases:
     output := [a < b, c < d, e < f, g < h]
   query: data.compare.output = x
   want_result:
-    x: [true, false, false, true]
+    - x: [true, false, false, true]
 - note: regocpp/keywords
   data:
     sites:
@@ -644,6 +647,7 @@ cases:
     b: "20"
     c: 30.0
     d: true
+    key: none
   modules:
   - |
     package keywords
@@ -687,7 +691,7 @@ cases:
 
     f["a"] := a
     f["b"] := b
-    f["c"] if c
+    f["c"] := true if c
     f["sum"] := sum {
         sum := a + b
     }
@@ -704,12 +708,12 @@ cases:
     output := [c, d, e, f, app_to_hostnames_obj["mongodb"] == ["oxygen"]]
   query: data.keywords.output = x
   want_result:
-    x:
-    - 4
-    - "found"
-    - ["0", "a"]
-    - {"a": 3, "b": 5, "c": true, "sum": 8}
-    - true
+    - x:
+      - 4
+      - "found"
+      - ["0", "a"]
+      - {"a": 3, "b": 5, "c": true, "sum": 8}
+      - true
 - note: regocpp/imports
   modules:
   - |
@@ -748,10 +752,10 @@ cases:
     ]
   query: data.scalars.output = x
   want_result:
-    x:
-    - 305
-    - true
-    - 469
+    - x:
+      - 305
+      - true
+      - 469
 - note: regocpp/with
   modules:
   - |
@@ -835,12 +839,12 @@ cases:
     output := [d_sum, d_diff, outer, use_count, use_mock]
   query: data.bodies.output = x
   want_result:
-    x:
-    - 8
-    - 2
-    - [[100, 300], {"bar": 300, "foo": 200}]
-    - 3
-    - 0
+    - x:
+      - 8
+      - 2
+      - [[100, 300], {"bar": 300, "foo": 200}]
+      - 3
+      - 0
 - note: regocpp/every
   data:
     sites:
@@ -931,7 +935,7 @@ cases:
     }
   query: data.every.output = x
   want_result:
-    x: true
+    - x: true
 - note: regocpp/compr
   data:
     sites:
@@ -1000,7 +1004,7 @@ cases:
     }
   query: data.compr.output = x
   want_result:
-    x: true
+    - x: true
 - note: regocpp/setops
   modules:
   - |
@@ -1038,7 +1042,7 @@ cases:
     }
   query: data.setops.output = x
   want_result:
-    x: true
+    - x: true
 - note: regocpp/enumerate
   modules:
   - |
@@ -1058,6 +1062,6 @@ cases:
     }
   query: data.enumerate.a = x
   want_result:
-    x:
+    - x:
       - 3
       - "3"
diff --git a/tests/test_case.cc b/tests/test_case.cc
index cfc5c1f5..90808450 100644
--- a/tests/test_case.cc
+++ b/tests/test_case.cc
@@ -2,11 +2,10 @@
 
 #include "rego_test.h"
 
-namespace
+namespace rego_test
 {
-  using namespace rego_test;
 
-  void write_ast(
+  void TestCase::write_ast(
     const std::filesystem::path& debug_path,
     std::size_t index,
     const std::string& pass,
@@ -17,11 +16,6 @@ namespace
       return;
     }
 
-    if (!std::filesystem::is_directory(debug_path))
-    {
-      std::filesystem::create_directory(debug_path);
-    }
-
     std::filesystem::path output;
     if (index < 10)
     {
@@ -62,7 +56,8 @@ namespace
     return false;
   }
 
-  std::optional<Node> maybe_get_file(Node mapping, const std::string& name)
+  std::optional<Node> TestCase::maybe_get_file(
+    const Node& mapping, const std::string& name)
   {
     Location loc(name);
     Nodes defs = mapping->lookdown(loc);
@@ -81,8 +76,8 @@ namespace
     return std::nullopt;
   }
 
-  std::optional<std::string> maybe_get_string(
-    Node mapping, const std::string& name)
+  std::optional<std::string> TestCase::maybe_get_string(
+    const Node& mapping, const std::string& name)
   {
     Location loc(name);
     Nodes defs = mapping->lookdown(loc);
@@ -103,7 +98,7 @@ namespace
     return std::nullopt;
   }
 
-  std::string get_string(Node mapping, const std::string& name)
+  std::string TestCase::get_string(const Node& mapping, const std::string& name)
   {
     auto maybe_string = maybe_get_string(mapping, name);
     if (maybe_string.has_value())
@@ -114,7 +109,7 @@ namespace
     return "";
   }
 
-  Node get_node(Node mapping, const std::string& name)
+  Node TestCase::get_node(const Node& mapping, const std::string& name)
   {
     Location loc(name);
     Nodes defs = mapping->lookdown(loc);
@@ -131,7 +126,7 @@ namespace
     return {};
   }
 
-  bool get_bool(Node mapping, const std::string& name)
+  bool TestCase::get_bool(const Node& mapping, const std::string& name)
   {
     Location loc(name);
     Nodes defs = mapping->lookdown(loc);
@@ -152,7 +147,7 @@ namespace
     return false;
   }
 
-  std::vector<std::string> get_modules(Node mapping)
+  std::vector<std::string> TestCase::get_modules(const Node& mapping)
   {
     Location loc("modules");
     Nodes defs = mapping->lookdown(loc);
@@ -179,22 +174,22 @@ namespace
     return modules;
   }
 
-  using BindingMap = std::map<std::string, std::string>;
-
-  BindingMap to_binding_map(const Node& node)
+  BindingMap TestCase::to_binding_map(const Node& node) const
   {
     BindingMap bindings;
     for (auto& binding : *node)
     {
       std::string key = std::string((binding / rego::Var)->location().view());
-      std::string value = rego::to_json((binding / rego::Term));
+      std::string value =
+        rego::to_json((binding / rego::Term), m_sort_bindings);
       bindings[key] = value;
     }
 
     return bindings;
   }
 
-  void diff(std::string actual, std::string wanted, std::ostream& os)
+  void TestCase::diff(
+    const std::string& actual, const std::string& wanted, std::ostream& os)
   {
     std::set<std::size_t> errors;
     std::size_t index = 0;
@@ -214,8 +209,8 @@ namespace
     }
 
     os << "wanted: " << wanted << std::endl;
-    os << "actual: " << actual << std::endl;
-    os << "        ";
+    os << "  actual: " << actual << std::endl;
+    os << "          ";
     for (std::size_t i = 0; i < length; ++i)
     {
       if (errors.contains(i))
@@ -229,7 +224,8 @@ namespace
     }
   }
 
-  bool compare(Node actual, Node wanted, std::ostream& os)
+  bool TestCase::compare(
+    const Node& actual, const Node& wanted, std::ostream& os) const
   {
     BindingMap actual_bindings = to_binding_map(actual);
     BindingMap wanted_bindings = to_binding_map(wanted);
@@ -243,20 +239,41 @@ namespace
 
       if (actual_bindings[key] != value)
       {
-        diff(key + "= " + actual_bindings[key], key + "= " + value, os);
+        diff(key + " = " + actual_bindings[key], key + " = " + value, os);
         return false;
       }
     }
 
     return true;
   }
-}
 
-namespace rego_test
-{
-  std::optional<std::vector<TestCase>> TestCase::load(
+  bool TestCase::compare(
+    const std::string& actual,
+    const std::string& wanted,
+    std::ostream& os) const
+  {
+    if (actual == wanted)
+    {
+      return true;
+    }
+
+    diff(actual, wanted, os);
+    return false;
+  }
+
+  std::vector<TestCase> TestCase::load(
     const std::filesystem::path& path, const std::filesystem::path& debug_path)
   {
+    if (!debug_path.empty())
+    {
+      if (std::filesystem::is_directory(debug_path))
+      {
+        std::filesystem::remove_all(debug_path);
+      }
+
+      std::filesystem::create_directory(debug_path);
+    }
+
     auto ast = parser().parse(path);
     bool ok = wf_parser.build_st(ast, std::cerr);
     ok = wf_parser.check(ast, std::cerr) && ok;
@@ -270,6 +287,7 @@ namespace rego_test
     }
 
     auto passes = rego_test::passes();
+    std::vector<TestCase> test_cases;
 
     for (std::size_t i = 0; i < passes.size(); ++i)
     {
@@ -295,71 +313,89 @@ namespace rego_test
         buf << "Failed at pass " << pass_name << std::endl;
         ast->errors(buf);
         std::cerr << buf.str() << std::endl;
-        return std::nullopt;
+        return test_cases;
       }
     }
 
-    std::vector<TestCase> test_cases;
     Node case_seq = ast->front()->front()->back();
     for (Node entry : *case_seq)
     {
-      Node test_case = entry->front();
-      test_cases.push_back(
-        TestCase::create_from_node(test_case).filename(path));
+      auto maybe_testcase = TestCase::create_from_node(entry->front());
+      if (maybe_testcase.has_value())
+      {
+        test_cases.push_back(maybe_testcase->filename(path));
+      }
     }
 
     return test_cases;
   }
 
-  TestCase TestCase::create_from_node(const Node& test_case_map)
+  std::optional<TestCase> TestCase::create_from_node(const Node& test_case_map)
   {
-    TestCase test_case;
-    auto note = maybe_get_string(test_case_map, "note");
-    if (note.has_value())
-    {
-      test_case = test_case.note(*note);
-    }
-    else
+    try
     {
-      throw std::runtime_error("Note is required");
-    }
+      TestCase test_case;
+      auto note = maybe_get_string(test_case_map, "note");
+      if (note.has_value())
+      {
+        test_case = test_case.note(*note);
+      }
+      else
+      {
+        throw std::runtime_error("Note is required");
+      }
 
-    auto query = maybe_get_string(test_case_map, "query");
-    if (query.has_value())
-    {
-      test_case = test_case.query(*query);
-    }
-    else
-    {
-      throw std::runtime_error("Query is required");
-    }
+      auto query = maybe_get_string(test_case_map, "query");
+      if (query.has_value())
+      {
+        test_case = test_case.query(*query);
+      }
+      else
+      {
+        throw std::runtime_error("Query is required");
+      }
 
-    auto data = maybe_get_file(test_case_map, "data");
-    if (data.has_value())
-    {
-      test_case = test_case.data(*data);
-    }
+      auto data = maybe_get_file(test_case_map, "data");
+      if (data.has_value())
+      {
+        test_case = test_case.data(*data);
+      }
+
+      auto input = maybe_get_file(test_case_map, "input");
+      if (input.has_value())
+      {
+        test_case = test_case.input(*input);
+      }
 
-    auto input = maybe_get_file(test_case_map, "input");
-    if (input.has_value())
+      return test_case.modules(get_modules(test_case_map))
+        .input_term(get_string(test_case_map, "input_term"))
+        .want_defined(get_bool(test_case_map, "want_defined"))
+        .want_result(get_node(test_case_map, "want_result"))
+        .want_error_code(get_string(test_case_map, "want_error_code"))
+        .want_error(get_string(test_case_map, "want_error"))
+        .sort_bindings(get_bool(test_case_map, "sort_bindings"))
+        .strict_error(get_bool(test_case_map, "strict_error"));
+    }
+    catch (const std::exception& e)
     {
-      test_case = test_case.input(*input);
+      std::cerr << "Error: " << e.what() << std::endl;
+      std::cerr << test_case_map->location().str() << std::endl;
+      return std::nullopt;
     }
-
-    return test_case.modules(get_modules(test_case_map))
-      .input_term(get_string(test_case_map, "input_term"))
-      .want_defined(get_bool(test_case_map, "want_defined"))
-      .want_result(get_node(test_case_map, "want_result"))
-      .want_error_code(get_string(test_case_map, "want_error_code"))
-      .want_error(get_string(test_case_map, "want_error"))
-      .sort_bindings(get_bool(test_case_map, "sort_bindings"))
-      .strict_error(get_bool(test_case_map, "strict_error"));
   }
 
-  Result TestCase::run(const std::filesystem::path& executable_path) const
+  Result TestCase::run(
+    const std::filesystem::path& executable_path,
+    const std::filesystem::path& debug_path) const
   {
     rego::Interpreter interpreter;
     interpreter.executable(executable_path);
+    if (!debug_path.empty())
+    {
+      interpreter.debug_enabled(true);
+      interpreter.debug_path(debug_path);
+    }
+
     for (std::size_t i = 0; i < m_modules.size(); ++i)
     {
       std::string name = "module" + std::to_string(i);
@@ -377,20 +413,81 @@ namespace rego_test
 
     bool pass = true;
     std::ostringstream error;
-    Node actual = interpreter.raw_query(m_query);
+    Node actual;
+    try
+    {
+      actual = interpreter.raw_query(m_query);
+    }
+    catch (const std::exception& e)
+    {
+      return {false, e.what()};
+    }
+
+    if (actual->type() == ErrorSeq)
+    {
+      if (actual->size() > 1)
+      {
+        error << "expected one error, actual: " << actual << std::endl;
+        return {false, error.str()};
+      }
+      else if (actual->size() == 0)
+      {
+        error << actual << std::endl;
+        return {false, error.str()};
+      }
+      else
+      {
+        actual = actual->front();
+      }
+    }
+
     if (m_want_error.length() > 0)
     {
-      pass = false;
-      error << "Unsupported test option";
+      if (actual->type() != Error)
+      {
+        pass = false;
+        error << "wanted an error, actual: " << actual << std::endl;
+      }
+      else
+      {
+        std::string actual_error =
+          std::string((actual / ErrorMsg)->location().view());
+        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);
+        pass = pass_error && pass_code;
+      }
     }
     else if (m_want_error_code.length() > 0)
     {
-      pass = false;
-      error << "Unsupported test option";
+      std::string actual_code =
+        std::string((actual / ErrorCode)->location().view());
+      pass = compare(actual_code, m_want_error_code, error);
     }
     else if (m_want_result)
     {
-      pass = compare(actual, m_want_result, error);
+      if (actual->front()->type() != Undefined)
+      {
+        if (actual->type() == Error)
+        {
+          pass = false;
+          error << "wanted a result, received: " << std::endl;
+          error << "  error: " << (actual / ErrorMsg)->location().view()
+                << std::endl;
+          error << "  code: " << (actual / ErrorCode)->location().view()
+                << std::endl;
+        }
+        else
+        {
+          pass = compare(actual, m_want_result, error);
+        }
+      }
+      else
+      {
+        pass = false;
+        error << "wanted a result, but was undefined";
+      }
     }
     else if (m_want_defined)
     {
@@ -420,9 +517,24 @@ namespace rego_test
     return m_note;
   }
 
+  const std::string& TestCase::category() const
+  {
+    return m_category;
+  }
+
   TestCase& TestCase::note(const std::string& note)
   {
     m_note = note;
+    auto pos = m_note.find('/');
+    if (pos == std::string::npos)
+    {
+      m_category = "";
+    }
+    else
+    {
+      m_category = m_note.substr(0, pos);
+    }
+
     return *this;
   }
 
diff --git a/tests/test_case.h b/tests/test_case.h
index 4df3d2ed..6fdb7799 100644
--- a/tests/test_case.h
+++ b/tests/test_case.h
@@ -9,6 +9,8 @@ namespace rego_test
 {
   using namespace rego;
 
+  using BindingMap = std::map<std::string, std::string>;
+
   struct Result
   {
     bool passed;
@@ -19,11 +21,16 @@ namespace rego_test
   {
   public:
     /** Loads a test case from a YAML file */
-    static std::optional<std::vector<TestCase>> load(
+    static std::vector<TestCase> load(
       const std::filesystem::path& path,
       const std::filesystem::path& debug_path = "");
 
-    Result run(const std::filesystem::path& executable_path) const;
+    Result run(
+      const std::filesystem::path& executable_path,
+      const std::filesystem::path& debug_path) const;
+
+    /** name of the test case category. */
+    const std::string& category() const;
 
     /** name of file that case was loaded from */
     const std::filesystem::path& filename() const;
@@ -78,9 +85,34 @@ namespace rego_test
     TestCase& strict_error(bool strict_error);
 
   private:
-    static TestCase create_from_node(const Node& test_case_map);
+    BindingMap to_binding_map(const Node& node) const;
+    bool compare(
+      const Node& actual, const Node& wanted, std::ostream& os) const;
+    bool compare(
+      const std::string& actual,
+      const std::string& wanted,
+      std::ostream& os) const;
+
+    static void write_ast(
+      const std::filesystem::path& debug_path,
+      std::size_t index,
+      const std::string& pass,
+      const Node& ast);
+    static std::optional<Node> maybe_get_file(
+      const Node& mapping, const std::string& name);
+    static std::optional<std::string> maybe_get_string(
+      const Node& mapping, const std::string& name);
+    static std::string get_string(const Node& mapping, const std::string& name);
+    static Node get_node(const Node& mapping, const std::string& name);
+    static bool get_bool(const Node& mapping, const std::string& name);
+    static std::vector<std::string> get_modules(const Node& mapping);
+    static void diff(
+      const std::string& actual, const std::string& wanted, std::ostream& os);
+    static std::optional<TestCase> create_from_node(const Node& test_case_map);
+
     std::filesystem::path m_filename;
     std::string m_note;
+    std::string m_category;
     std::string m_query;
     std::vector<std::string> m_modules;
     Node m_data;